From b280ab555cda4a2626e4465628a86511c2e53565 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 10:49:49 +0800 Subject: [PATCH 01/60] Initial Commit for Publish Settings workflow --- localization/i18n/list.txt | 2 + resources/images/menu_publish.svg | 1 + src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/PresetBundle.cpp | 288 ++++- src/libslic3r/PresetBundle.hpp | 23 +- src/libslic3r/PublishSettings.cpp | 105 ++ src/libslic3r/PublishSettings.hpp | 56 + src/slic3r/CMakeLists.txt | 4 + src/slic3r/GUI/ConfigValueFormatter.cpp | 245 ++++ src/slic3r/GUI/ConfigValueFormatter.hpp | 31 + src/slic3r/GUI/MainFrame.cpp | 20 + src/slic3r/GUI/Plater.cpp | 155 ++- src/slic3r/GUI/Plater.hpp | 4 + src/slic3r/GUI/PublishSettingsDialog.cpp | 1077 +++++++++++++++++ src/slic3r/GUI/PublishSettingsDialog.hpp | 183 +++ src/slic3r/GUI/Tab.cpp | 6 + src/slic3r/GUI/Tab.hpp | 3 +- src/slic3r/GUI/UnsavedChangesDialog.cpp | 223 +--- tests/libslic3r/test_3mf.cpp | 177 +++ .../libslic3r/test_preset_bundle_loading.cpp | 535 ++++++++ 20 files changed, 2897 insertions(+), 243 deletions(-) create mode 100644 resources/images/menu_publish.svg create mode 100644 src/libslic3r/PublishSettings.cpp create mode 100644 src/libslic3r/PublishSettings.hpp create mode 100644 src/slic3r/GUI/ConfigValueFormatter.cpp create mode 100644 src/slic3r/GUI/ConfigValueFormatter.hpp create mode 100644 src/slic3r/GUI/PublishSettingsDialog.cpp create mode 100644 src/slic3r/GUI/PublishSettingsDialog.hpp diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 1614bc453b..c36a3aba40 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -136,6 +136,7 @@ src/slic3r/GUI/BackgroundSlicingProcess.cpp src/slic3r/GUI/BedShapeDialog.cpp src/slic3r/GUI/BedShapeDialog.hpp src/slic3r/GUI/ConfigManipulation.cpp +src/slic3r/GUI/ConfigValueFormatter.cpp src/slic3r/GUI/DeviceManager.cpp src/slic3r/GUI/DeviceErrorDialog.cpp src/slic3r/GUI/ExtraRenderers.cpp @@ -175,6 +176,7 @@ src/slic3r/GUI/ProgressStatusBar.cpp src/slic3r/GUI/PlateSettingsDialog.cpp src/slic3r/GUI/PrivacyUpdateDialog.cpp src/slic3r/GUI/PublishDialog.cpp +src/slic3r/GUI/PublishSettingsDialog.cpp src/slic3r/GUI/SavePresetDialog.cpp src/slic3r/GUI/Search.cpp src/slic3r/GUI/Selection.cpp diff --git a/resources/images/menu_publish.svg b/resources/images/menu_publish.svg new file mode 100644 index 0000000000..e444623e33 --- /dev/null +++ b/resources/images/menu_publish.svg @@ -0,0 +1 @@ + diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 812d28e088..16bee2d70c 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -350,6 +350,8 @@ set(lisbslic3r_sources Preset.hpp PrincipalComponents2D.cpp PrincipalComponents2D.hpp + PublishSettings.cpp + PublishSettings.hpp PrintApply.cpp PrintBase.cpp PrintBase.hpp diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 01cbc43bc2..9e0fc61323 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3,6 +3,7 @@ #include "PresetBundle.hpp" #include "PrintConfig.hpp" +#include "PublishSettings.hpp" #include "libslic3r.h" #include "I18N.hpp" #include "Utils.hpp" @@ -11,7 +12,7 @@ #include "libslic3r_version.h" #include -#include +#include #include #include #include @@ -40,6 +41,8 @@ namespace Slic3r { +// Project-level options imported from a loaded 3MF into project_config. s_project_options_published +// below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync. static std::vector s_project_options { "flush_volumes_vector", "flush_volumes_matrix", @@ -71,6 +74,24 @@ static std::vector s_project_options { "enable_filament_dynamic_map" }; +// Project options applied when loading a "published" 3MF project: the full s_project_options +// minus the filament/purge keys. A published file must not port the author's filament data +// (colors, colour types, filament/map/AMS slot state, purge/prime/flush volumes, nozzle +// volume types, filament switcher state) to the receiver's project_config, which feeds the +// scene colors, AMS slot colors and purge data. Only the plate/bed geometry keys cross over; +// normal (non-published) 3MF loads keep importing the author's flush data via s_project_options. +// +// KEEP IN SYNC with s_project_options above: when a new project option is added there, decide +// here whether it is plate/bed geometry (add it to this list) or filament/purge/mapping/device +// state (it must NOT be added). curr_bed_type is deliberately NOT in this list: the receiver +// keeps its own bed type when loading a published project. The published-mode project_config +// assertions in tests/libslic3r/test_preset_bundle_loading.cpp guard both directions. +static std::vector s_project_options_published { + "wipe_tower_x", + "wipe_tower_y", + "wipe_tower_rotation_angle" +}; + //Orca: add custom as default const char *PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; const char *PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; @@ -4426,10 +4447,14 @@ static void convert_filament_preset_name(std::string& machine_name, std::string& } // Load a config file from a boost property_tree. This is a private method called from load_config_file. // is_external == false on if called from ConfigWizard -void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version, bool selected) +void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version, bool selected, PublishedConfig *published_config) { PrinterTechnology printer_technology = Preset::printer_technology(config); + // A "published" 3MF project keeps the user's currently-selected presets and overlays only + // the author-selected published keys onto the edited presets. + const bool is_published = published_config != nullptr && published_config->published; + auto clear_compatible_printers = [](DynamicPrintConfig& config){ ConfigOption *opt_compatible = config.optptr("compatible_printers"); if (opt_compatible != nullptr) { @@ -4570,6 +4595,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool switch (Preset::printer_technology(config)) { case ptFFF: { + // A "published" 3MF project keeps the user's currently-selected presets, so the + // print / printer / filament presets are NOT loaded from the file. Only the + // project config values and the published keys are applied below. + if (!is_published) { //BBS: add different settings logic BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); std::vector print_different_keys_vector; @@ -4722,9 +4751,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool this->filament_presets[i] = loaded->name; } } + } // !is_published // 4) Load the project config values (the per extruder wipe matrix etc). - this->project_config.apply_only(config, s_project_options); + // In published mode the receiver must not inherit the author's filament/purge data, + // so only the plate/bed geometry project keys are applied. + this->project_config.apply_only(config, is_published ? s_project_options_published : s_project_options); break; } @@ -4743,9 +4775,258 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool this->update_compatible(PresetSelectCompatibleType::Never); this->update_multi_material_filament_presets(); + // A "published" 3MF project overlays only the author-selected published keys onto the + // user's currently-selected (edited) process preset. Scalar keys are applied directly; + // vector (multi-extruder) keys are applied only when the edited preset has a matching + // vector size. Keys that cannot be applied are collected for notification; legacy + // filament/printer keys in a published file fall through into skipped_keys. + if (is_published) { + std::vector skipped_keys; + std::set applied_keys; + // 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 + // PublishSettings.hpp). Defense-in-depth: a hand-crafted 3MF could set + // published_keys to these regardless of the dialog, so skip them here too. + const std::set &structural_keys = publish_structural_keys(); + // The printer overlay is restricted to the publishable retraction/z-hop allowlist. + // Printer-class keys outside it are contract-excluded: never applied and never + // reported as skipped (a hand-crafted 3MF listing machine_start_gcode or + // nozzle_diameter must not apply them and must not spam the warning). + const std::set &printer_allowlist = publishable_printer_keys(); + const std::vector &printer_options = Preset::printer_options(); + const std::set printer_option_set(printer_options.begin(), printer_options.end()); + std::set contract_excluded_keys; + auto apply_published = [&](DynamicPrintConfig &target, const std::set *allowlist) { + for (const std::string &key : published_config->published_keys) { + if (applied_keys.count(key) != 0) + continue; // already applied + // A '#' suffix denotes a variant (per-extruder/per-filament) key; resolve the base key. + const std::string base_key = key.substr(0, key.find('#')); + // Structural keys are intentionally never applied (not "skipped due to + // mismatch"), so bail out before the applied/skipped bookkeeping. + if (structural_keys.count(base_key) != 0) + continue; + if (allowlist != nullptr && + printer_option_set.count(base_key) != 0 && + allowlist->count(base_key) == 0) { + // Printer-class key outside the publishable allowlist: contract-excluded. + contract_excluded_keys.insert(base_key); + continue; + } + const ConfigOption *src_opt = config.option(base_key); + if (src_opt == nullptr) + continue; // key not present in the loaded config; record later + if (src_opt->is_vector()) { + // Vector key: apply only when the edited preset has a matching vector size. + const ConfigOption *dst_opt = target.option(base_key); + if (dst_opt == nullptr || !dst_opt->is_vector() || + static_cast(src_opt)->size() != static_cast(dst_opt)->size()) + continue; // cannot apply; will be reported as skipped + // A '#' variant index must be in range: ConfigOptionVector::set_at would + // otherwise resize the destination vector, corrupting the receiver's preset. + if (key.size() > base_key.size()) { + const size_t idx = static_cast(std::atoi(key.c_str() + base_key.size() + 1)); + if (idx >= static_cast(src_opt)->size()) + continue; // out-of-range variant: cannot apply; reported as skipped + } + target.apply_only(config, {key}, true); + applied_keys.insert(key); + } else { + // A scalar key cannot carry a '#N' variant suffix; a hand-crafted file + // listing one is reported as skipped instead of being silently marked applied. + if (key.find('#') != std::string::npos) + continue; + // Scalar key: apply only if present on the user's machine. + if (target.option(base_key) == nullptr) + continue; // not present on the edited preset; will be reported as skipped + target.apply_only(config, {key}, true); + applied_keys.insert(key); + } + } + }; + apply_published(this->prints.get_edited_preset().config, nullptr); + apply_published(this->printers.get_edited_preset().config, &printer_allowlist); + + // Material pass: apply the author's material-qualified keys onto the receiver's + // matching filament presets. The file config carries the author's per-slot identity + // (filament_type / filament_vendor remain in config; filament_ids was moved into a + // local earlier) and the per-slot material retraction values. + if (!published_config->material_keys.empty()) { + const ConfigOptionStrings *file_types = config.option("filament_type"); + const ConfigOptionStrings *file_vendors = config.option("filament_vendor"); + auto identity_matches = [](const std::string &id, const std::string &type, const std::string &vendor, + const std::string &slot_id, const std::string &slot_type, const std::string &slot_vendor) { + // When both sides carry a filament_id, equality is required; otherwise fall + // back to filament_type, with filament_vendor as an additional qualifier only + // when both sides have a non-empty vendor. + if (!id.empty() && !slot_id.empty()) + return id == slot_id; + if (type.empty() || type != slot_type) + return false; + if (!vendor.empty() && !slot_vendor.empty()) + return vendor == slot_vendor; + return true; + }; + for (const PublishedMaterialEntry &entry : published_config->material_keys) { + // 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); + // a legacy entry (slot -1) uses the first matching author slot and applies to + // every matching receiver preset. + auto slot_identity = [&filament_ids, file_types, file_vendors](size_t slot, std::string &id, std::string &type, std::string &vendor) { + id = (slot < filament_ids.size()) ? filament_ids[slot] : std::string(); + type = (file_types && slot < file_types->size()) ? file_types->get_at(slot) : std::string(); + vendor = (file_vendors && slot < file_vendors->size()) ? file_vendors->get_at(slot) : std::string(); + }; + bool author_found = false; + size_t author_slot = 0; + size_t author_ordinal = 0; + if (entry.slot >= 0) { + // Collect every author slot carrying this identity, in slot order; the + // entry's slot must be among them, and its position is the ordinal used + // to pick the receiver's matching preset. + std::vector matching_author_slots; + for (size_t slot = 0; slot < filament_ids.size(); ++slot) { + std::string slot_id, slot_type, slot_vendor; + slot_identity(slot, slot_id, slot_type, slot_vendor); + if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, + slot_id, slot_type, slot_vendor)) + matching_author_slots.emplace_back(slot); + } + const auto ordinal_it = std::find(matching_author_slots.begin(), matching_author_slots.end(), size_t(entry.slot)); + if (ordinal_it != matching_author_slots.end()) { + author_slot = size_t(entry.slot); + author_ordinal = size_t(ordinal_it - matching_author_slots.begin()); + author_found = true; + } + // Out of range, or the slot does not carry this identity: silent skip below. + } else { + // Legacy: the first author slot whose identity matches. + for (size_t slot = 0; slot < filament_ids.size(); ++slot) { + std::string slot_id, slot_type, slot_vendor; + slot_identity(slot, slot_id, slot_type, slot_vendor); + if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, + slot_id, slot_type, slot_vendor)) { + author_slot = slot; + author_found = true; + break; + } + } + } + if (!author_found) + // No author slot carries this material: nothing to apply, nothing to report. + continue; + + const std::string material_label = entry.filament_id.empty() ? entry.filament_type : entry.filament_id; + auto report_skipped = [&skipped_keys, &material_label](const std::string &key, const std::string &slot_qualifier = std::string()) { + skipped_keys.emplace_back("material:" + material_label + + (slot_qualifier.empty() ? std::string() : " " + slot_qualifier) + + " (" + key + ")"); + }; + + // Collect the receiver's matching filament presets (distinct by preset name). + std::vector matched_preset_names; + std::set fallback_matched_names; + for (const std::string &preset_name : this->filament_presets) { + Preset *preset = this->filaments.find_preset(preset_name); + if (preset == nullptr) + continue; + const std::string slot_id = preset->filament_id; + // Null-guard the identity reads: a malformed user preset may lack + // filament_type / filament_vendor entirely (hand-edited preset file). + const ConfigOptionStrings *slot_types = preset->config.option("filament_type"); + const ConfigOptionStrings *slot_vendors = preset->config.option("filament_vendor"); + const std::string slot_type = (slot_types && !slot_types->values.empty()) ? slot_types->get_at(0) : std::string(); + const std::string slot_vendor = (slot_vendors && !slot_vendors->values.empty()) ? slot_vendors->get_at(0) : std::string(); + if (!identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, + slot_id, slot_type, slot_vendor)) + continue; + if (std::find(matched_preset_names.begin(), matched_preset_names.end(), preset_name) == matched_preset_names.end()) + matched_preset_names.emplace_back(preset_name); + if (entry.filament_id.empty() || slot_id.empty()) + fallback_matched_names.insert(preset_name); + } + if (matched_preset_names.empty()) { + // No receiver material matches this entry: report each key as skipped. + for (const std::string &key : entry.keys) + report_skipped(key); + continue; + } + if (fallback_matched_names.size() > 1) { + // The type fallback matched more than one distinct receiver preset: never + // guess which one the author meant. + for (const std::string &key : entry.keys) + report_skipped(key); + continue; + } + + // Slotted entries target the receiver's matching preset at the author's + // ordinal; legacy entries apply to every matching receiver preset. + std::vector apply_to_preset_names; + if (entry.slot >= 0) { + if (author_ordinal >= matched_preset_names.size()) { + // The receiver has fewer matching presets than the author's ordinal: + // this slot's values cannot be placed, report each key. + const std::string slot_qualifier = "slot " + std::to_string(entry.slot); + for (const std::string &key : entry.keys) + report_skipped(key, slot_qualifier); + continue; + } + apply_to_preset_names.emplace_back(matched_preset_names[author_ordinal]); + } else { + apply_to_preset_names = matched_preset_names; + } + + for (const std::string &key : entry.keys) { + const std::string base_key = key.substr(0, key.find('#')); + if (structural_keys.count(base_key) != 0) + continue; // structural: silent + const ConfigOption *src_opt = config.option(base_key); + if (src_opt == nullptr || !src_opt->is_vector() || + author_slot >= static_cast(src_opt)->size()) { + report_skipped(key); + continue; + } + for (const std::string &preset_name : apply_to_preset_names) { + Preset *preset = this->filaments.find_preset(preset_name); + if (preset == nullptr) + continue; + ConfigOption *dst_opt = preset->config.option(base_key); + // 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. + if (dst_opt == nullptr || !dst_opt->is_vector() || + static_cast(dst_opt)->empty() || + dst_opt->type() != src_opt->type()) { + report_skipped(key); + continue; + } + static_cast(dst_opt)->set_at(src_opt, 0, author_slot); + } + } + } + } + + for (const std::string &key : published_config->published_keys) { + if (applied_keys.count(key) != 0) + continue; + const std::string base_key = key.substr(0, key.find('#')); + // Structural keys are silently ignored, never reported as skipped: a hand-crafted + // 3MF must not trigger the "could not be applied" warning for them. + if (structural_keys.count(base_key) != 0) + continue; + // Printer-class keys outside the publishable allowlist are contract-excluded too. + if (contract_excluded_keys.count(base_key) != 0) + continue; + skipped_keys.emplace_back(key); + } + published_config->skipped_keys = std::move(skipped_keys); + } + //BBS //const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; const std::string physical_printer; + if (!is_published) { if (this->printers.get_edited_preset().is_external || physical_printer.empty()) { this->physical_printers.unselect_printer(); } else { @@ -4756,6 +5037,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool else this->physical_printers.unselect_printer(); } + } //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 685687975b..c8af2e4b71 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -3,6 +3,7 @@ #include "Preset.hpp" #include "AppConfig.hpp" +#include "PublishSettings.hpp" #include "enum_bitmask.hpp" #include @@ -166,6 +167,22 @@ struct PresetBundleMetadata } }; +// Configuration describing a "published" 3MF project: the file carries a flag plus a list of +// author-selected setting keys. When loading such a project the user's currently-selected +// presets are kept and only the published keys are overlaid onto the edited presets. +struct PublishedConfig +{ + bool published = false; + std::vector published_keys; + // Material-qualified published keys chosen by the author for the materials used in the + // project; applied on load only to the receiver's filament presets whose material + // identity matches (see PublishedMaterialEntry in PublishSettings.hpp). + std::vector material_keys; + // 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 skipped_keys; +}; + // Bundle of Print + Filament + Printer presets. class PresetBundle { @@ -414,8 +431,8 @@ public: // Load configuration that comes from a model file containing configuration, such as 3MF et al. // This method is called by the Plater. - void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver()) - { this->load_config_file_config(name, true, std::move(config), file_version); } + void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver(), PublishedConfig *published_config = nullptr) + { this->load_config_file_config(name, true, std::move(config), file_version, false, published_config); } // Load an external config file containing the print, filament and printer presets. // Instead of a config file, a G-code may be loaded containing the full set of parameters. @@ -544,7 +561,7 @@ private: // Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path. // and the external config is just referenced, not stored into user profile directory. // If it is not an external config, then the config will be stored into the user profile directory. - void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false); + void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, PublishedConfig *published_config = nullptr); /*ConfigSubstitutions load_config_file_config_bundle( const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/ diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp new file mode 100644 index 0000000000..4c0a851696 --- /dev/null +++ b/src/libslic3r/PublishSettings.cpp @@ -0,0 +1,105 @@ +#include "PublishSettings.hpp" + +#include "PresetBundle.hpp" +#include "Preset.hpp" + +#include + +namespace Slic3r { + +const std::set& publish_structural_keys() +{ + // Structural / non-publishable keys. The *_settings_id keys are also part of + // PresetCollection::skipped_in_dirty (Preset.cpp) and are excluded there too. + // This mirrors the structural keys stripped from configs in Preset.cpp + // (profile_print_params_same) plus other keys that must never be published + // because they would rewrite the user's preset inheritance/structure. + static const std::set structural_keys = { + "printer_settings_id", "filament_settings_id", "print_settings_id", + "sla_print_settings_id", "sla_material_settings_id", + "compatible_printers", "compatible_prints", + "compatible_printers_condition", "compatible_prints_condition", + "default_filament_profile", "default_print_profile", + "default_sla_print_profile", "default_sla_material_profile", + "extruder_count", "bed_shape", + "inherits", "inherits_group", + "printer_technology", "printer_model", "printer_variant", + "physical_printer_settings_id", "filament_ids", + "different_settings_to_system" + }; + return structural_keys; +} + +// 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 +// lists, so any key shown there must be publishable here (and vice versa). +const std::vector& publishable_printer_retraction_options() +{ + static const std::vector options = { + { "retraction_length", "printer_extruder_retraction#length" }, + { "retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart" }, + { "retraction_speed", "printer_extruder_retraction#retraction-speed" }, + { "deretraction_speed", "printer_extruder_retraction#deretraction-speed" }, + { "retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold" }, + { "retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change" }, + { "wipe", "printer_extruder_retraction#wipe-while-retracting" }, + { "wipe_distance", "printer_extruder_retraction#wipe-distance" }, + { "retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe" }, + { "retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe" }, + }; + return options; +} + +// The printer tab's "Z-Hop" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order. KEEP IN +// SYNC with that optgroup, same as publishable_printer_retraction_options(). +const std::vector& publishable_printer_z_hop_options() +{ + static const std::vector options = { + { "retract_lift_enforce", "printer_extruder_z_hop#on-surfaces" }, + { "z_hop_types", "printer_extruder_z_hop#z-hop-type" }, + { "z_hop", "printer_extruder_z_hop#z-hop-height" }, + { "travel_slope", "printer_extruder_z_hop#traveling-angle" }, + { "retract_lift_above", "printer_extruder_z_hop#only-lift-z-above" }, + { "retract_lift_below", "printer_extruder_z_hop#only-lift-z-below" }, + }; + return options; +} + +const std::set& publishable_printer_keys() +{ + // The union of the printer tab's "Retraction" and "Z-Hop" optgroups. The "Retraction when + // switching material" keys are intentionally excluded: toolchange retraction is + // device/profile territory, not a publishable behavior tweak. + static const std::set printer_keys = [] { + std::set keys; + for (const PublishablePrinterOption &opt : publishable_printer_retraction_options()) + keys.insert(opt.key); + for (const PublishablePrinterOption &opt : publishable_printer_z_hop_options()) + keys.insert(opt.key); + return keys; + }(); + return printer_keys; +} + +std::vector collect_dirty_settings_keys(const PresetBundle& bundle) +{ + std::vector keys; + + auto append_dirty = [&keys](const std::vector& dirty) { + for (const std::string& key : dirty) { + if (std::find(keys.begin(), keys.end(), key) == keys.end()) + keys.push_back(key); + } + }; + + // Print and printer presets each track a single edited preset; filaments may span + // multiple slots (multi-material). Union the dirty keys of each collection's edited + // preset; this feeds only the Publish dialog's pre-check. + append_dirty(bundle.prints.current_dirty_options(true)); + append_dirty(bundle.printers.current_dirty_options(true)); + append_dirty(bundle.filaments.current_dirty_options(true)); + + return keys; +} + +} // namespace Slic3r diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp new file mode 100644 index 0000000000..ecc939d883 --- /dev/null +++ b/src/libslic3r/PublishSettings.hpp @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include + +namespace Slic3r { +class PresetBundle; + +// Structural / non-publishable setting keys, shared by the Publish dialog and the published-3MF +// overlay path in PresetBundle::load_config_file_config. These keys must never be published +// because they would rewrite the user's preset inheritance/structure. This is the single +// source of truth for the denylist. +const std::set& publish_structural_keys(); + +// One option row of the printer tab's "Retraction" / "Z-Hop" optgroups (TabPrinter::build_fff, +// Tab.cpp). Key and icon id are kept together so the tab can later be migrated onto these +// lists; publishable_printer_keys() is their union, and the published-3MF loader/dialog must +// never accept printer keys outside it. +struct PublishablePrinterOption { + const char *key; // config key, e.g. "retraction_length" + const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length" +}; + +// The printer tab's "Retraction" optgroup options, in tab order. +const std::vector& publishable_printer_retraction_options(); +// The printer tab's "Z-Hop" optgroup options, in tab order. +const std::vector& publishable_printer_z_hop_options(); + +// Printer-class retraction / z-hop keys that are publishable: the union of +// publishable_printer_retraction_options() and publishable_printer_z_hop_options(). The +// published-3MF overlay applies printer keys only when their base key is in this allowlist; +// any other printer-class key in a published file is contract-excluded (never applied, never +// reported as skipped). +const std::set& publishable_printer_keys(); + +// Returns the union of setting keys that differ from the base/system preset across the current +// print, printer and filament presets (feeds the Publish dialog's pre-check). +std::vector collect_dirty_settings_keys(const PresetBundle& bundle); + +// A material-qualified set of published setting keys, chosen by the author for one of the +// materials used in the project. The identity fields let the receiver apply the keys only +// when a matching material is selected: filament_id is the most precise (stable across +// machines/vendors when present, empty for user presets); filament_type + filament_vendor +// are the fallback. Keys are base keys (no "#N" variant suffix). +struct PublishedMaterialEntry { + std::string filament_type; // material family, e.g. "PLA" (may be empty) + std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty) + std::string filament_id; // stable material id, e.g. "GFL99" (may be empty) + // 0-based author filament slot this entry's values came from; -1 = legacy/unspecified + // (files written before the slot field). Slotted entries apply to the receiver's Nth + // matching preset (N = the slot's ordinal among the author's matching slots); legacy + // entries apply to every matching receiver preset. + int slot{-1}; + std::vector keys; +}; +} diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b98396f943..4d103ff749 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -93,6 +93,8 @@ set(SLIC3R_GUI_SOURCES GUI/CloneDialog.hpp GUI/ConfigManipulation.cpp GUI/ConfigManipulation.hpp + GUI/ConfigValueFormatter.cpp + GUI/ConfigValueFormatter.hpp GUI/ConfigWizard.cpp GUI/ConfigWizard.hpp GUI/ConfigWizard_private.hpp @@ -437,6 +439,8 @@ set(SLIC3R_GUI_SOURCES GUI/Project.hpp GUI/PublishDialog.cpp GUI/PublishDialog.hpp + GUI/PublishSettingsDialog.cpp + GUI/PublishSettingsDialog.hpp GUI/PurgeModeDialog.cpp GUI/PurgeModeDialog.hpp GUI/RammingChart.cpp diff --git a/src/slic3r/GUI/ConfigValueFormatter.cpp b/src/slic3r/GUI/ConfigValueFormatter.cpp new file mode 100644 index 0000000000..f3ea539d4f --- /dev/null +++ b/src/slic3r/GUI/ConfigValueFormatter.cpp @@ -0,0 +1,245 @@ +#include "ConfigValueFormatter.hpp" + +#include +#include +#include +#include + +#include +#include + +#include "libslic3r/Config.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "I18N.hpp" +#include "GUI.hpp" +#include "Field.hpp" + +namespace Slic3r { +namespace GUI { + +std::string get_pure_opt_key(const std::string& opt_key) +{ + std::string pure_key = opt_key; + const int pos = pure_key.find("#"); + if (pos > 0) + boost::erase_tail(pure_key, pure_key.size() - pos); + return pure_key; +} + +wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill, int idx) +{ + const ConfigOptionDef& def = config.def()->options.at(opt_key); + const std::vector& names = def.enum_labels;//ConfigOptionEnum::get_enum_names(); + int val = 0; + + if (idx >= 0) + val = dynamic_cast(config.option(opt_key))->get_at(idx); + else + val = config.option(opt_key)->getInt(); + + // Each infill doesn't use all list of infill declared in PrintConfig.hpp. + // So we should "convert" val to the correct one + if (is_infill) { + for (auto key_val : *def.enum_keys_map) + if (int(key_val.second) == val) { + auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first); + if (it == def.enum_values.end()) + return ""; + return from_u8(_utf8(names[it - def.enum_values.begin()])); + } + return _L("Undefined"); + } + return from_u8(_utf8(names[val])); +} + +wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config) +{ + const std::string pure_key = get_pure_opt_key(opt_key); + auto option = config.option(pure_key); + + if (!option || option->is_nil()) + return _L("N/A"); + + const ConfigOptionDef* opt = config.def()->get(pure_key); + return opt->full_label.empty() ? opt->label : opt->full_label; +} + +wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config) +{ + int orig_opt_idx = -1; + int opt_idx = -1; + int pos = opt_key.find("#"); + std::string temp_str = opt_key; + if (pos > 0) { + boost::erase_head(temp_str, pos + 1); + orig_opt_idx = static_cast(atoi(temp_str.c_str())); + } + opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0; + const std::string pure_key = get_pure_opt_key(opt_key); + auto option = config.option(pure_key); + if (!option) { + return _L("N/A"); + } + auto opt_vector = dynamic_cast(option); + + if (option->is_scalar() && config.option(pure_key)->is_nil() || + option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)) + return _L("N/A"); + + wxString out; + + const ConfigOptionDef* opt = config.def()->get(pure_key); + bool is_nullable = opt->nullable; + + switch (opt->type) { + case coInt: + return from_u8((boost::format("%1%") % config.opt_int(pure_key)).str()); + case coInts: { + if (is_nullable) { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str()); + } + else { + auto values = config.opt(pure_key); + if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) { + return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str()); + } + else { + std::string value_str; + for (int i = 0; i < values->size(); i++) { + value_str += std::to_string(values->get_at(i)); + if (i != values->size() - 1) { + value_str += ","; + } + } + return from_u8(value_str); + } + } + return _L("Undefined"); + } + case coBool: + return config.opt_bool(pure_key) ? "true" : "false"; + case coBools: { + if (is_nullable) { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return values->get_at(opt_idx) ? "true" : "false"; + } + else { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return values->get_at(opt_idx) ? "true" : "false"; + } + return _L("Undefined"); + } + case coPercent: + return from_u8((boost::format("%1%%%") % int(config.optptr(pure_key)->getFloat())).str()); + case coPercents: { + if (is_nullable) { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str()); + } + else { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str()); + } + return _L("Undefined"); + } + case coFloat: + return double_to_string(config.opt_float(pure_key)); + case coFloats: { + if (is_nullable) { + auto values = config.opt(pure_key); + if (opt_idx < values->size()) + return double_to_string(values->get_at(opt_idx)); + } + else { + auto values = config.opt(pure_key); + if (values && opt_idx < values->size()) + return double_to_string(values->get_at(opt_idx)); + } + return _L("Undefined"); + } + case coString: + return from_u8(config.opt_string(pure_key)); + case coStrings: { + const ConfigOptionStrings* strings = config.opt(pure_key); + if (strings) { + if (pure_key == "compatible_printers" || pure_key == "compatible_prints") { + if (strings->empty()) + return _L("All"); + for (size_t id = 0; id < strings->size(); id++) + out += from_u8(strings->get_at(id)) + "\n"; + out.RemoveLast(1); + return out; + } + if (!strings->empty() && opt_idx < strings->values.size()) + return from_u8(strings->get_at(opt_idx)); + } + break; + } + case coFloatOrPercent: { + const ConfigOptionFloatOrPercent* opt = config.opt(pure_key); + if (opt) + out = double_to_string(opt->value) + (opt->percent ? "%" : ""); + return out; + } + case coEnum: { + return get_string_from_enum(pure_key, config, + pure_key == "top_surface_pattern" || + pure_key == "bottom_surface_pattern" || + pure_key == "internal_solid_infill_pattern" || + pure_key == "sparse_infill_pattern" || + pure_key == "ironing_pattern" || + pure_key == "support_ironing_pattern" || + pure_key == "support_pattern" || + pure_key == "support_interface_pattern") + ; + } + case coEnums: { + return get_string_from_enum(pure_key, config, + pure_key == "top_surface_pattern" || + pure_key == "bottom_surface_pattern" || + pure_key == "internal_solid_infill_pattern" || + pure_key == "sparse_infill_pattern" || + pure_key == "ironing_pattern" || + pure_key == "support_ironing_pattern" || + pure_key == "support_pattern" || + pure_key == "support_interface_pattern" + , opt_idx); + } + case coPoint: { + Vec2d val = config.opt(pure_key)->value; + return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); + } + case coPoints: { + //BBS: add bed_exclude_area + if (pure_key == "printable_area" || pure_key == "thumbnails") { + ConfigOptionPoints points = *config.option(pure_key); + //BuildVolume build_volume = {points.values, 0.}; + return get_thumbnails_string(points.values); + } + else if (pure_key == "bed_exclude_area") { + return get_thumbnails_string(config.option(pure_key)->values); + } + else if (pure_key == "head_wrap_detect_zone") { + return get_thumbnails_string(config.option(pure_key)->values); + } + else if (pure_key == "wrapping_exclude_area") { + return get_thumbnails_string(config.option(pure_key)->values); + } + Vec2d val = config.opt(pure_key)->get_at(opt_idx); + return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); + } + default: + break; + } + return out; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/ConfigValueFormatter.hpp b/src/slic3r/GUI/ConfigValueFormatter.hpp new file mode 100644 index 0000000000..e7ed549930 --- /dev/null +++ b/src/slic3r/GUI/ConfigValueFormatter.hpp @@ -0,0 +1,31 @@ +#ifndef slic3r_ConfigValueFormatter_hpp_ +#define slic3r_ConfigValueFormatter_hpp_ + +#include + +#include + +namespace Slic3r { + +class DynamicPrintConfig; + +namespace GUI { + +// Return the value of the given option (identified by opt_key, which may contain +// a "#" suffix) formatted as a human readable string. +wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config); + +// Return the full label of the given option (identified by opt_key, which may contain +// a "#" suffix). Returns "N/A" when the option is not set. +wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config); + +// Strip the "#" suffix (if any) from the given option key. +std::string get_pure_opt_key(const std::string& opt_key); + +// Return the localized label of the currently selected value of an enum option. +wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_ConfigValueFormatter_hpp_ diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 5ef81a32e1..6aa7932c84 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -54,6 +54,7 @@ #include "GUI_App.hpp" #include "UnsavedChangesDialog.hpp" +#include "PublishSettingsDialog.hpp" #include "MsgDialog.hpp" #include "Notebook.hpp" #include "GUI_Factories.hpp" @@ -2821,6 +2822,25 @@ void MainFrame::init_menubar_as_editor() [this](){return m_plater != nullptr && can_save_as(); }, this); #endif + // BBS: publish settings + 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()); + }; + +#ifndef __APPLE__ + append_menu_item(fileMenu, wxID_ANY, _L("Publish Settings") + dots, _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 Settings") + dots, _L("Export a 3MF file with the selected settings embedded"), + publish_handler, "", nullptr, + [this](){return can_export_model(); }, this); +#endif + fileMenu->AppendSeparator(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 8299125353..847d98f398 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1,12 +1,10 @@ #include "Plater.hpp" #include "../Utils/NetworkAgent.hpp" -#include "../Utils/NetworkAgentFactory.hpp" #include "libslic3r/Config.hpp" #include "libslic3r_version.h" #include #include -#include #include #include #include @@ -16,8 +14,6 @@ #include #include #include -#include -#include #include #include #include @@ -69,7 +65,6 @@ #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Model.hpp" -#include "libslic3r/SLA/Hollowing.hpp" #include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/SLA/ReprojectPointsOnMesh.hpp" #include "libslic3r/Polygon.hpp" @@ -78,16 +73,15 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PublishSettings.hpp" #include "slic3r/Utils/CrealityPrint.hpp" #include "libslic3r/ClipperUtils.hpp" -#include "libslic3r/ObjColorUtils.hpp" // For stl export #include "libslic3r/CSGMesh/ModelToCSGMesh.hpp" #include "libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp" #include "GUI.hpp" #include "GUI_App.hpp" -#include "GuiColor.hpp" #include "GUI_ObjectList.hpp" #ifdef __WXGTK__ #include "LinuxDisplayBackend.hpp" @@ -123,7 +117,6 @@ #include "SendMultiMachinePage.hpp" #include "SendToPrinter.hpp" #include "PublishDialog.hpp" -#include "ModelMall.hpp" #include "ConfigWizard.hpp" #include "SyncAmsInfoDialog.hpp" #include "../Utils/ASCIIFolding.hpp" @@ -149,7 +142,6 @@ #include "ParamsDialog.hpp" #include "ImageDPIFrame.hpp" #include "Widgets/Label.hpp" -#include "Widgets/RoundedRectangle.hpp" #include "Widgets/RadioGroup.hpp" #include "Widgets/CheckBox.hpp" #include "Widgets/Button.hpp" @@ -5459,7 +5451,7 @@ struct Plater::priv std::vector 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); - wxString get_export_file(GUI::FileType file_type); + wxString get_export_file(GUI::FileType file_type, const wxString& title = {}); // BBS void load_auxiliary_files(); @@ -7214,6 +7206,64 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } + // BBS: a "published" 3MF project carries a flag plus a list of author-selected + // setting keys. When present, keep the user's currently-selected presets and + // overlay only the published keys onto the edited presets on load. + PublishedConfig published_config; + if (model.model_info != nullptr) { + auto published_it = model.model_info->metadata_items.find("published"); + if (published_it != model.model_info->metadata_items.end() && + (published_it->second == "true" || published_it->second == "1")) { + published_config.published = true; + auto keys_it = model.model_info->metadata_items.find("published_keys"); + if (keys_it != model.model_info->metadata_items.end()) { + try { + auto j = nlohmann::json::parse(keys_it->second); + if (j.is_array()) + for (const auto &k : j) + if (k.is_string()) + published_config.published_keys.emplace_back(k.get()); + } catch (...) { + // Ignore malformed published_keys; the project still loads normally. + } + } + + auto material_keys_it = model.model_info->metadata_items.find("published_material_keys"); + if (material_keys_it != model.model_info->metadata_items.end()) { + try { + auto jm = nlohmann::json::parse(material_keys_it->second); + if (jm.is_array()) + for (const auto &m : jm) { + // Malformed entries are skipped individually. + if (!m.is_object()) + continue; + PublishedMaterialEntry entry; + const auto mat_it = m.find("material"); + if (mat_it != m.end() && mat_it->is_object()) { + const auto &mat = *mat_it; + if (mat.contains("filament_type") && mat["filament_type"].is_string()) + entry.filament_type = mat["filament_type"].get(); + if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) + entry.filament_vendor = mat["filament_vendor"].get(); + if (mat.contains("filament_id") && mat["filament_id"].is_string()) + entry.filament_id = mat["filament_id"].get(); + } + if (m.contains("slot") && m["slot"].is_number_integer()) + entry.slot = m["slot"].get(); + const auto entry_keys_it = m.find("keys"); + if (entry_keys_it != m.end() && entry_keys_it->is_array()) + for (const auto &k : *entry_keys_it) + if (k.is_string()) + entry.keys.emplace_back(k.get()); + published_config.material_keys.emplace_back(std::move(entry)); + } + } catch (...) { + // Ignore malformed published_material_keys; the project still loads normally. + } + } + } + } + if (load_config) { if (!config.empty()) { Preset::normalize(config); @@ -7317,7 +7367,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (wipe_tower_y_opt) file_wipe_tower_y = *wipe_tower_y_opt; - preset_bundle->load_config_model(filename.string(), std::move(config), file_version); + preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config); + + // BBS: notify the user about published settings that could not be applied. + if (!published_config.skipped_keys.empty()) { + NotificationManager *notify_manager = q->get_notification_manager(); + std::string message = _u8L("Some published settings could not be applied:"); + for (const std::string &key : published_config.skipped_keys) + message += "\n-" + key; + 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) { @@ -8179,7 +8238,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) return output_file; } -wxString Plater::priv::get_export_file(GUI::FileType file_type) +wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title) { wxString wildcard; switch (file_type) { @@ -8222,7 +8281,7 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type) case FT_3MF: { output_file.replace_extension("3mf"); - dlg_title = _L("Save file as"); + dlg_title = title.empty() ? _L("Save file as") : title; break; } case FT_OBJ: @@ -16105,6 +16164,76 @@ void Plater::export_core_3mf() export_3mf(path_u8, SaveStrategy::Silence); } +// Export the current project as a "published" 3MF. This is a pure export: unlike save_project(), +// it never touches the project's file name, dirty state, backup path or title, and the +// published metadata is attached to the model only for the duration of the export so the +// in-memory project stays exactly as it was (a later Save Project produces a normal 3MF). +int Plater::export_published_3mf(const std::vector& published_keys, const std::vector& material_keys) +{ + wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:")); + if (path.empty() || path == "") + return wxID_CANCEL; + + nlohmann::json j = nlohmann::json::array(); + for (const std::string& key : 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} }); + + Model& model = this->model(); + // Remember the previous metadata state so it can be restored after the export, keeping the + // in-memory project pristine (the published flag lives only in the exported file). + const bool had_model_info = (model.model_info != nullptr); + const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end()); + const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end()); + const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find("published_material_keys") != model.model_info->metadata_items.end()); + const std::string prev_published = had_published ? model.model_info->metadata_items.at("published") : std::string(); + const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at("published_keys") : std::string(); + const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at("published_material_keys") : std::string(); + if (model.model_info == nullptr) + model.model_info = std::make_shared(); + model.model_info->metadata_items["published"] = "1"; + model.model_info->metadata_items["published_keys"] = j.dump(); + model.model_info->metadata_items["published_material_keys"] = jm.dump(); + + // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence: + // without it export_3mf() calls set_project_filename() on success, which would make this + // pure export the current project file. Silence keeps the project state untouched, exactly + // like export_core_3mf(). + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence; + bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); + if (full_pathnames) + save_strategy = save_strategy | SaveStrategy::FullPathSources; + + const int ret = export_3mf(into_path(path), save_strategy); + + // Restore the previous metadata state (both on success and on failure). + if (!had_model_info) { + model.model_info = nullptr; + } else { + if (had_published) + model.model_info->metadata_items["published"] = prev_published; + else + model.model_info->metadata_items.erase("published"); + if (had_published_keys) + model.model_info->metadata_items["published_keys"] = prev_published_keys; + else + model.model_info->metadata_items.erase("published_keys"); + if (had_material_keys) + model.model_info->metadata_items["published_material_keys"] = prev_material_keys; + else + model.model_info->metadata_items.erase("published_material_keys"); + } + + if (ret < 0) { + MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), + _L("Publish Settings"), wxOK | wxICON_WARNING).ShowModal(); + return wxID_CANCEL; + } + return wxID_YES; +} + Preset *get_printer_preset(const MachineObject *obj) { if (!obj) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index b60c0eb242..5a83e5e4e1 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -495,6 +495,10 @@ public: void export_gcode_3mf(bool export_all = false); void send_gcode_finish(wxString name); void export_core_3mf(); + // Export the current project as a "published" 3MF: embeds the author-selected settings + // (published_keys / published_material_keys) into the file's metadata. A pure export: the + // in-memory project (filename, dirty state, model_info metadata) is left untouched. + int export_published_3mf(const std::vector& published_keys, const std::vector& material_keys); static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function notify_func = {}); void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL); //BBS: remove amf diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp new file mode 100644 index 0000000000..36b7372d4a --- /dev/null +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -0,0 +1,1077 @@ +#include "PublishSettingsDialog.hpp" + +#include "GUI_App.hpp" +#include "MainFrame.hpp" +#include "MsgDialog.hpp" +#include "I18N.hpp" +#include "Tab.hpp" +#include "ConfigValueFormatter.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/TextInput.hpp" +#include "Widgets/DialogButtons.hpp" +#include "Widgets/StaticLine.hpp" +#include "Widgets/StateColor.hpp" + +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PublishSettings.hpp" + +#include +#include +#include +#include +#include + +// Custom-painted collapse chevron: a vector path (down when expanded, right +// when collapsed) drawn in the dialog's secondary-text grey. Vector drawing +// keeps it crisp at any DPI (the earlier 16px bitmap chevron looked +// blurry/wide). +class CollapseChevron : public wxWindow +{ +public: + explicit CollapseChevron(wxWindow* parent) : wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + { + SetBackgroundColour(parent->GetBackgroundColour()); + DisableFocusFromKeyboard(); + SetMinSize(FromDIP(wxSize(10, 10))); + Bind(wxEVT_PAINT, &CollapseChevron::on_paint, this); + } + + void SetCollapsed(bool collapsed) + { + if (m_collapsed == collapsed) + return; + m_collapsed = collapsed; + Refresh(); + } + +private: + void on_paint(wxPaintEvent&) + { + wxPaintDC dc(this); + wxGraphicsContext* ctx = wxGraphicsContext::Create(dc); + if (ctx == nullptr) + return; + ctx->SetAntialiasMode(wxANTIALIAS_DEFAULT); + // Same grey as the row value labels, dark-mode aware. + wxPen pen(StateColor::darkModeColorFor(wxColour("#6B6B6B")), FromDIP(1.5), wxPENSTYLE_SOLID); + pen.SetCap(wxCAP_ROUND); + pen.SetJoin(wxJOIN_ROUND); + ctx->SetPen(pen); + + const wxSize sz = GetClientSize(); + const double cx = sz.x / 2.0; + const double cy = sz.y / 2.0; + const double r = std::min(sz.x, sz.y) * 0.32; + + wxGraphicsPath path = ctx->CreatePath(); + if (m_collapsed) { + // Right-pointing chevron ">". + path.MoveToPoint(cx - r, cy - r); + path.AddLineToPoint(cx + r, cy); + path.AddLineToPoint(cx - r, cy + r); + } else { + // Down-pointing chevron "v". + path.MoveToPoint(cx - r, cy - r); + path.AddLineToPoint(cx, cy + r); + path.AddLineToPoint(cx + r, cy - r); + } + ctx->StrokePath(path); + delete ctx; + } + + bool m_collapsed{false}; +}; + +namespace Slic3r { namespace GUI { +namespace { + +// Identity of a filament slot: the stable material id when present, else the +// type+vendor pair. Used to emit the PublishedMaterialEntry identity fields. +struct MaterialIdentity +{ + std::string type; + std::string vendor; + std::string id; +}; + +// Menu ids for show_menu(). Dedicated range above the standard ids so the popup cannot +// collide with application-level bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). +enum { + kPublishSelectAll = wxID_HIGHEST + 1, + kPublishDeselectAll, + kPublishSelectVisible, + kPublishDeselectVisible, + kPublishFilterSelected, + kPublishFilterNonSelected +}; + +MaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& full) +{ + MaterialIdentity identity; + if (const auto* types = full.opt("filament_type")) + if (slot < types->size()) + identity.type = types->get_at(slot); + if (const auto* vendors = full.opt("filament_vendor")) + if (slot < vendors->size()) + identity.vendor = vendors->get_at(slot); + if (const auto* ids = full.opt("filament_ids")) + if (slot < ids->size()) + identity.id = ids->get_at(slot); + return identity; +} + +// "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in +// PresetBundle::load_vendor_configs_from_json (PresetBundle.cpp) and +// PresetCollection::set_custom_preset_alias (Preset.cpp). +std::string material_display_name(const std::string& preset_name) +{ + const size_t at = preset_name.find_first_of('@'); + if (at == std::string::npos) + return preset_name; + std::string bare = preset_name.substr(0, at); + boost::trim_right(bare); + return bare.empty() ? preset_name : bare; +} + +// Human-readable section title for a filament slot: the resolved preset name, +// falling back to the filament type, then to the generic "Material". +wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPrintConfig& full) +{ + if (slot < bundle->filament_presets.size()) { + const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot]); + if (preset != nullptr && !preset->name.empty()) + return from_u8(material_display_name(preset->name)); + } + const MaterialIdentity identity = material_identity(slot, full); + if (!identity.type.empty()) + return from_u8(identity.type); + return _L("Material"); +} + +} // namespace + +PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) + : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), + wxID_ANY, + _L("Publish Settings"), + wxDefaultPosition, + wxDefaultSize, + wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER) + , m_search(this, "search", 16) + , m_menu(this, "filter", 16) +{ + SetBackgroundColour(*wxWHITE); + + build_option_model(); + + // --- filter bar: search box, All/None, menu button --- + wxPanel* f_bar = new wxPanel(this, wxID_ANY); + f_bar->SetBackgroundColour(GetBackgroundColour()); + wxBoxSizer* f_sizer = new wxBoxSizer(wxHORIZONTAL); + + m_filter_box = new TextInput(f_bar, "", "", "", wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER); + m_filter_box->SetIcon(m_search.bmp()); + m_filter_box->SetMinSize(FromDIP(wxSize(200, 24))); + m_filter_box->SetSize(FromDIP(wxSize(-1, 24))); + m_filter_box->SetFocus(); + m_filter_ctrl = m_filter_box->GetTextCtrl(); + m_filter_ctrl->SetFont(Label::Body_13); + m_filter_ctrl->SetSize(wxSize(-1, FromDIP(16))); // centers text vertically + m_filter_ctrl->SetHint(_L("Type to filter...")); + m_filter_ctrl->Bind(wxEVT_TEXT, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); }); + m_filter_ctrl->Bind(wxEVT_TEXT_ENTER, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); }); + m_filter_ctrl->Bind(wxEVT_SET_FOCUS, [this](auto& e) { + apply_filter(m_filter_ctrl->GetValue()); + e.Skip(); + }); + m_filter_ctrl->Bind(wxEVT_KILL_FOCUS, [this](auto& e) { + apply_filter(m_filter_ctrl->GetValue()); + e.Skip(); + }); + f_sizer->Add(m_filter_box, 1, wxEXPAND); + Bind(wxEVT_SET_FOCUS, [this](auto&) { m_filter_box->SetFocus(); }); + + m_fb_sizer = new wxBoxSizer(wxHORIZONTAL); + auto create_btn = [this, f_bar](wxString title, bool select) { + auto btn = new wxStaticText(f_bar, wxID_ANY, title); + btn->SetForegroundColour("#009687"); + btn->SetCursor(wxCURSOR_HAND); + btn->SetFont(Label::Body_13); + btn->Bind(wxEVT_LEFT_DOWN, [this, select](wxMouseEvent&) { select_all(select); }); + m_fb_sizer->Add(btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + }; + f_sizer->Add(m_fb_sizer, 0, wxALIGN_CENTER_VERTICAL); + create_btn(_L("All"), true); + create_btn(_L("None"), false); + + m_menu_button = new wxStaticBitmap(f_bar, wxID_ANY, m_menu.bmp()); + m_menu_button->SetCursor(wxCURSOR_HAND); + m_menu_button->Bind(wxEVT_LEFT_DOWN, &PublishSettingsDialog::show_menu, this); + f_sizer->Add(m_menu_button, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + + f_bar->SetSizerAndFit(f_sizer); + + wxBoxSizer* w_sizer = new wxBoxSizer(wxVERTICAL); + + wxStaticText* msg = new wxStaticText(this, wxID_ANY, _L("Select which settings to embed in the 3MF file")); + msg->SetFont(Label::Body_13); + msg->Wrap(-1); + w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + + w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); + w_sizer->Add(m_scroll, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); + + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); + + dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + // At least one checked, enabled (publishable) key is required. A gated + // material row is disabled even if its value is pre-checked, so it must + // not count. + for (const Row& row : m_rows) + if (row.check->GetValue() && row.check->IsEnabled()) { + EndModal(wxID_OK); + return; + } + MessageDialog(this, _L("No settings selected. Please select at least one setting to publish."), _L("Publish Settings"), + wxOK | wxICON_WARNING) + .ShowModal(); + }); + dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); + + w_sizer->Add(dlg_btns, 0, wxEXPAND); + + SetSizerAndFit(w_sizer); + SetMinSize(FromDIP(wxSize(600, 500))); + SetSize(FromDIP(wxSize(600, 500))); // initial size only; the dialog is resizable + wxGetApp().UpdateDlgDarkUI(this); +} + +PublishSettingsDialog::~PublishSettingsDialog() {} + +void PublishSettingsDialog::build_option_model() +{ + // Structural / non-publishable keys, shared with the published-3MF overlay + // path (see libslic3r/PublishSettings.hpp). + const std::set& denylist = publish_structural_keys(); + // Base keys already added in the print/printer sections. Printer rows share + // this set: a base key appears once (per-extruder "#N" variants collapse to + // the first occurrence - acceptable MVP; the per-extruder context is lost). + std::set added; + + PresetBundle* bundle = wxGetApp().preset_bundle; + DynamicPrintConfig full = bundle->full_config(); + + m_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + m_scroll->SetScrollRate(0, 10); + m_scroll->SetBackgroundColour(GetBackgroundColour()); + m_list_sizer = new wxBoxSizer(wxVERTICAL); + m_scroll->SetSizer(m_list_sizer); + m_scroll->DisableFocusFromKeyboard(); + m_scroll->Bind(wxEVT_RIGHT_DOWN, &PublishSettingsDialog::show_menu, this); + + // "no matching rows" info label, shown by apply_filter(). + m_info = new wxStaticText(m_scroll, wxID_ANY, ""); + m_info->SetFont(Label::Body_13); + m_list_sizer->Add(m_info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10)); + m_info->Hide(); + m_info_nonsel = _L("No selected items..."); + m_info_allsel = _L("All items selected..."); + m_info_empty = _L("No matching items..."); + + // Shared per-option label/value computation; returns false when the option + // must be skipped (denylisted / unknown / empty label). value is the pure + // stringified value; unit is the translated sidetext (may be empty). + auto option_text = [&denylist, &full](const std::string& opt_id, const std::string& pure_key, wxString& label, wxString& value, + wxString& unit) -> bool { + if (denylist.count(pure_key) > 0) + return false; + const ConfigOptionDef* def = print_config_def.get(pure_key); + if (def == nullptr) + return false; + label = _(def->full_label.empty() ? def->label : def->full_label); + if (label.IsEmpty()) + return false; + value = get_string_value(opt_id, full); + unit = _(def->sidetext); + return true; + }; + + // Find-or-create a main category; builds its header row UI on first use. + // Material sections are additionally matched by identity and slot so two + // identities (or slots) that happen to share a title stay separate. + auto category_index_for = [this, &full](const wxString& title, Section section, const std::string& icon_name, size_t group, + const MaterialIdentity& identity = MaterialIdentity(), size_t slot = 0) -> size_t { + for (size_t i = 0; i < m_categories.size(); ++i) { + if (m_categories[i].title != title || m_categories[i].section != section) + continue; + if (section == Section::Material && + (m_categories[i].filament_id != identity.id || m_categories[i].filament_type != identity.type || + m_categories[i].filament_vendor != identity.vendor || m_categories[i].filament_slot != slot)) + continue; + return i; + } + Category cat; + cat.title = title; + cat.section = section; + cat.group = group; + cat.icon_name = icon_name; + cat.filament_type = identity.type; + cat.filament_vendor = identity.vendor; + cat.filament_id = identity.id; + cat.filament_slot = slot; + if (!icon_name.empty()) { + ScalableBitmap icon_bmp(m_scroll, icon_name, 18); + cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + } + if (section == Section::Material) { + // Material header: [master (title)][slim tri-state select-all]. + // The master is a 2-state opt-in that carries the material title; + // the tri-state is label-less and gates on the master. + cat.master_check = new wxCheckBox(m_scroll, wxID_ANY, title); + cat.master_check->SetFont(Label::Head_14); + cat.master_check->SetToolTip(_L("Export this material")); + cat.header = new wxCheckBox(m_scroll, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); + cat.header->SetFont(Label::Head_14); + cat.header->SetToolTip(_L("Select/deselect all settings in this material")); + } else { + cat.header = new wxCheckBox(m_scroll, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); + cat.header->SetFont(Label::Head_14.Bold()); + } + const size_t new_index = m_categories.size(); + cat.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_category(new_index); }); + // [icon][chevron][(chip)(master)(tri-state) | checkbox]: the chevron + // collapses/expands the category, the checkbox is the select-all. + auto header_sizer = new wxBoxSizer(wxHORIZONTAL); + if (cat.icon != nullptr) + header_sizer->Add(cat.icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + header_sizer->Add(cat.chevron, 0, wxALIGN_CENTER_VERTICAL); + if (section == Section::Material) { + // Per-slot colour chip, before the master title. + std::string hex; + if (const auto* colours = full.opt("filament_colour")) + if (slot < colours->size()) + hex = colours->get_at(slot); + wxBitmap* chip_bmp = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12)); + // chip_bmp points into get_extruder_color_icon's static BitmapCache + // and must NOT be deleted; the wxStaticBitmap takes its own copy. + header_sizer->Add(new wxStaticBitmap(m_scroll, wxID_ANY, *chip_bmp), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + header_sizer->Add(cat.master_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + header_sizer->Add(cat.header, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); + } else { + header_sizer->Add(cat.header, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(4)); + } + // A wrapper sizer splits the vertical separation (TOP 10) from the + // horizontal indent (LEFT|RIGHT 22), so the top gap collapses with the + // header when it is hidden. + auto wrap = new wxBoxSizer(wxVERTICAL); + wrap->Add(header_sizer, 0, wxTOP, FromDIP(10)); + cat.item = m_list_sizer->Add(wrap, 0, wxLEFT | wxRIGHT, FromDIP(22)); + // Register the new category with its section group: the group's visibility + // and select-all logic iterates section.categories. + m_sections[group].categories.push_back(new_index); + m_categories.push_back(std::move(cat)); + return new_index; + }; + + // Find-or-create a subcategory (optgroup) heading within a category. + auto subcategory_index_for = [this](size_t cat_index, const wxString& title, const wxString& icon) -> size_t { + Category& cat = m_categories[cat_index]; + for (size_t i = 0; i < cat.subs.size(); ++i) + if (cat.subs[i].title == title) + return i; + Subcategory sub; + sub.title = title; + if (!title.IsEmpty()) { + // Same look as the Tab's optgroup headers (incl. its icon), plus a + // collapse chevron. A click on the chevron bitmap does not reach the + // StaticLine, so both are bound to the same toggle (LEFT_UP so the + // StaticLine's label acts as the click target too). + sub.header = new ::StaticLine(m_scroll, false, title, icon); + sub.header->SetFont(Label::Head_14); + sub.header->SetForegroundColour("#363636"); + sub.header->SetCursor(wxCURSOR_HAND); + const size_t new_index = cat.subs.size(); + auto toggle = [this, cat_index, new_index] { toggle_subcategory(cat_index, new_index); }; + sub.header->Bind(wxEVT_LEFT_UP, [toggle](wxMouseEvent&) { toggle(); }); + sub.chevron = create_chevron(m_scroll, wxEVT_LEFT_UP, toggle); + auto header_sizer = new wxBoxSizer(wxHORIZONTAL); + header_sizer->Add(sub.chevron, 0, wxALIGN_CENTER_VERTICAL); + header_sizer->Add(sub.header, 1, wxEXPAND | wxLEFT, FromDIP(4)); + // A wrapper sizer splits the vertical separation (TOP|BOTTOM 6) from + // the horizontal indent (LEFT|RIGHT 38), so the gaps collapse with + // the header when it is hidden. + auto wrap = new wxBoxSizer(wxVERTICAL); + wrap->Add(header_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6)); + sub.item = m_list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38)); + } + cat.subs.push_back(std::move(sub)); + return cat.subs.size() - 1; + }; + + // Creates the row UI (checkbox + white ellipsized value + grey unit) and + // registers the row into the given category/subcategory. + auto add_row_ui = [this](const std::string& key, const wxString& label, const wxString& value, const wxString& unit, size_t cat_index, + size_t sub_index) { + Row row; + row.key = key; + row.label = label; + row.value = value; + row.unit = unit; + row.category = m_categories[cat_index].title; + row.subcategory = m_categories[cat_index].subs[sub_index].title; + row.section = m_categories[cat_index].section; + row.section_title = m_sections[m_categories[cat_index].group].title; + const size_t row_index = m_rows.size(); + m_rows.push_back(std::move(row)); + + Row& r = m_rows[row_index]; + r.check = new wxCheckBox(m_scroll, wxID_ANY, label, wxDefaultPosition, wxDefaultSize); + r.check->SetFont(Label::Body_13); + // Value in the Tab's text color (near-black light / #EFEFF0 dark); the + // unit is the grey secondary text. + r.value_label = new wxStaticText(m_scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); + r.value_label->SetFont(Label::Body_13); + r.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); + r.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit); + if (!unit.IsEmpty()) { + r.unit_label = new wxStaticText(m_scroll, wxID_ANY, unit); + r.unit_label->SetFont(Label::Body_13); + r.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); + } + + auto row_sizer = new wxBoxSizer(wxHORIZONTAL); + row_sizer->Add(r.check, 0, wxALIGN_CENTER_VERTICAL); + row_sizer->Add(r.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + if (r.unit_label != nullptr) + row_sizer->Add(r.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + r.item = m_list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(54)); + + m_categories[cat_index].rows.push_back(row_index); + m_categories[cat_index].subs[sub_index].rows.push_back(row_index); + }; + + // --- Phase 1: printer per-extruder retraction settings (displayed first, + // mirroring the sidebar's Printer group). The printer tab's + // "Extruder"/"Extruder N" pages carry the per-extruder retraction options. + { + size_t g = section_group_for(Section::Printer); + for (Tab* tab : wxGetApp().tabs_list) { + if (tab->m_type != Preset::TYPE_PRINTER) + continue; + for (const PageShp& page : tab->m_pages) { + if (!page->title().StartsWith("Extruder")) + continue; + const wxString page_title = Tab::translate_category(page->title(), tab->m_type); + for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { + // Allowlist on the untranslated optgroup title; the "Retraction + // when switching material" group is intentionally skipped. + if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") + continue; + const wxString subcategory = page_title + L" \u00B7 " + _(optgroup->title); + for (const auto& opt : optgroup->opt_map()) { + const std::string& opt_id = opt.first; + const std::string& pure_key = opt.second.first; + // Per-extruder "#N" variants collapse to the first base key. The row stores + // the BASE key (whole-vector semantics on load: the size-guarded apply + // copies the author's full vector), while the "#0" opt_id is only used to + // display the first extruder's value. + if (!added.insert(pure_key).second) + continue; + wxString label, value, unit; + if (!option_text(opt_id, pure_key, label, value, unit)) + continue; + size_t cat_index = category_index_for(_L("Retraction & Z-hop"), Section::Printer, "custom-gcode_extruder", g); + size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon); + add_row_ui(pure_key, label, value, unit, cat_index, sub_index); + } + } + } + } + } + + // --- Phase 2: per-material sections synthesized from the filament tab's + // "Setting Overrides" page, under the Filament group. + { + size_t g = section_group_for(Section::Material); + Tab* filament_tab = nullptr; + for (Tab* tab : wxGetApp().tabs_list) + if (tab->m_type == Preset::TYPE_FILAMENT) { + filament_tab = tab; + break; + } + if (filament_tab != nullptr) { + const Page* overrides_page = nullptr; + for (const PageShp& page : filament_tab->m_pages) + if (page->title() == "Setting Overrides") { + overrides_page = page.get(); + break; + } + + if (overrides_page != nullptr) { + // One section per filament slot: a 4-slot printer (e.g. 1 PLA + + // 3 PETG) shows 4 sections, each disambiguated by its colour chip + // and slot number. The "· Slot N" title suffix keeps the category + // titles unique across slots. + for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { + const MaterialIdentity identity = material_identity(slot, full); + const wxString title = material_title(slot, bundle, full) + L" \u00B7 " + + wxString::Format(_L("Slot %d"), static_cast(slot) + 1); + // A material section must not repeat a key; the same key may + // appear in other material sections - that is intended. + std::set material_added; + + for (const ConfigOptionsGroupShp& optgroup : overrides_page->m_optgroups) { + // Allowlist on the untranslated optgroup title; the + // "Ironing" group is intentionally skipped. + if (optgroup->title != "Retraction" && optgroup->title != "Retraction when switching material") + continue; + for (const auto& opt : optgroup->opt_map()) { + // Row keys are base keys (no "#N"): the load side + // matches the material and uses the author's slot. + const std::string& opt_id = opt.first; + std::string base = opt_id.substr(0, opt_id.find('#')); + if (!material_added.insert(base).second) + continue; + // Show the value of this slot; fall back to slot 0 if out of range. + std::string value_opt_id = base + "#" + std::to_string(slot); + if (const ConfigOption* opt_cfg = full.option(base)) + if (const auto* vec = dynamic_cast(opt_cfg)) + if (vec->size() > 0 && slot >= vec->size()) + value_opt_id = base + "#0"; + wxString label, value, unit; + if (!option_text(value_opt_id, base, label, value, unit)) + continue; + size_t cat_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, identity, slot); + size_t sub_index = subcategory_index_for(cat_index, _(optgroup->title), optgroup->icon); + add_row_ui(base, label, value, unit, cat_index, sub_index); + } + } + } + } + } + } + + // --- Phase 3: process (print) settings; grouping matches the process tab. + { + size_t g = section_group_for(Section::Print); + for (Tab* tab : wxGetApp().tabs_list) { + if (tab->m_type != Preset::TYPE_PRINT) + continue; + const auto& icon_map = tab->get_category_icon_map(); + for (const PageShp& page : tab->m_pages) { + wxString category = Tab::translate_category(page->title(), tab->m_type); + // Page icon, keyed by the untranslated page title (per-Tab map). + std::string icon_name; + auto icon_it = icon_map.find(page->title()); + if (icon_it != icon_map.end()) + icon_name = icon_it->second; + + for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { + for (const auto& opt : optgroup->opt_map()) { + // opt_map() key is the opt_id (may carry "#N"); the value is + // (pure_opt_key, opt_index). + const std::string& opt_id = opt.first; + const std::string& pure_key = opt.second.first; + // A key may appear in more than one page/group; keep the first. + if (!added.insert(pure_key).second) + continue; + wxString label, value, unit; + if (!option_text(opt_id, pure_key, label, value, unit)) + continue; + size_t cat_index = category_index_for(category, Section::Print, icon_name, g); + size_t sub_index = subcategory_index_for(cat_index, _(optgroup->title), optgroup->icon); + add_row_ui(opt_id, label, value, unit, cat_index, sub_index); + } + } + } + } + } + + // Pre-check the dirty (modified) settings and mark them bold. The base-key + // match covers all sections; collect_dirty_settings_keys already unions the + // prints, printers and filaments of the bundle. + std::set dirty_base; + for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) { + auto n = key.find('#'); + dirty_base.insert(n == std::string::npos ? key : key.substr(0, n)); + } + for (Row& row : m_rows) { + std::string base = row.key.substr(0, row.key.find('#')); + row.dirty = dirty_base.count(base) > 0; + if (row.dirty) { + row.check->SetValue(true); + set_row_bold(row, true); + } + } + + // Wire the section group tri-state headers (chevrons/StaticLine toggles were + // bound at creation in section_group_for). + for (size_t s = 0; s < m_sections.size(); ++s) { + if (m_sections[s].header != nullptr) { + m_sections[s].header->Bind(wxEVT_CHECKBOX, [this, s](wxCommandEvent&) { on_section_toggle(s); }); + update_section_header(m_sections[s]); + } + } + + // Wire the 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); }); + 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, c](wxCommandEvent&) { update_category_header(m_categories[c]); }); + update_category_header(m_categories[c]); + } + + // Material sections start gated (master OFF): their rows and tri-state are + // disabled until the author opts the material in. + for (size_t c = 0; c < m_categories.size(); ++c) + if (m_categories[c].section == Section::Material) + on_master_toggle(c); + + // No filter is active at startup: every row matches until the user types. + for (Row& row : m_rows) + row.matches_filter = true; + apply_visibility(); + + m_scroll->FitInside(); + m_list_sizer->Layout(); +} + +size_t PublishSettingsDialog::section_group_for(Section kind) +{ + for (size_t i = 0; i < m_sections.size(); ++i) + if (m_sections[i].kind == kind) + return i; + + SectionGroup section; + section.kind = kind; + const size_t new_index = m_sections.size(); + + switch (kind) { + case Section::Printer: + section.title = _L("Printer"); + section.icon_name = "printer"; + break; + case Section::Material: + section.title = _L("Filament"); + section.icon_name = "filament"; + break; + case Section::Print: + section.title = _L("Process"); + section.icon_name = "process"; + break; + } + + if (!section.icon_name.empty()) { + ScalableBitmap icon_bmp(m_scroll, section.icon_name, 18); + section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + } + + section.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_section(new_index); }); + + auto header_sizer = new wxBoxSizer(wxHORIZONTAL); + if (section.icon != nullptr) + header_sizer->Add(section.icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); + header_sizer->Add(section.chevron, 0, wxALIGN_CENTER_VERTICAL); + + if (kind == Section::Material) { + // Filament group: clickable StaticLine title, no tri-state (each + // material below opts in individually). + section.header_line = new ::StaticLine(m_scroll, false, section.title); + section.header_line->SetFont(Label::Head_14.Bold()); + section.header_line->SetForegroundColour("#363636"); + section.header_line->SetCursor(wxCURSOR_HAND); + section.header_line->SetToolTip(_L("Enable each material below to export its settings")); + auto toggle = [this, new_index] { toggle_section(new_index); }; + section.header_line->Bind(wxEVT_LEFT_UP, [toggle](wxMouseEvent&) { toggle(); }); + header_sizer->Add(section.header_line, 1, wxEXPAND | wxLEFT, FromDIP(4)); + } else { + // Printer/Process: tri-state select-all carries the title. + section.header = new wxCheckBox(m_scroll, wxID_ANY, section.title, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); + section.header->SetFont(Label::Head_14.Bold()); + header_sizer->Add(section.header, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(4)); + } + + // A wrapper sizer splits the larger vertical separation (TOP 14) from the + // shallow horizontal indent (LEFT|RIGHT 6), so the top gap collapses with + // the header when it is hidden. wxEXPAND lets the Filament StaticLine's + // separator span the width like the subcategory headers. + auto wrap = new wxBoxSizer(wxVERTICAL); + wrap->Add(header_sizer, 0, wxEXPAND | wxTOP, FromDIP(14)); + section.item = m_list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(6)); + m_sections.push_back(std::move(section)); + return new_index; +} + +void PublishSettingsDialog::on_category_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; + 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_category_header(cat); +} + +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_category_header(cat); +} + +void PublishSettingsDialog::on_section_toggle(size_t section_index) +{ + SectionGroup& section = m_sections[section_index]; + if (section.header == nullptr) + return; // defensive: the Filament group has no select-all + // All-or-none over every enabled row in the group's categories. + bool all_checked = true; + for (size_t c : section.categories) { + for (size_t r : m_categories[c].rows) + if (m_rows[r].check->IsEnabled() && !m_rows[r].check->GetValue()) { + all_checked = false; + break; + } + if (!all_checked) + break; + } + const bool value = !all_checked; + for (size_t c : section.categories) + for (size_t r : m_categories[c].rows) + if (m_rows[r].check->IsEnabled()) + m_rows[r].check->SetValue(value); + for (size_t c : section.categories) + update_category_header(m_categories[c]); + update_section_header(section); +} + +void PublishSettingsDialog::update_section_header(SectionGroup& section) +{ + if (section.header == nullptr) + return; // Filament group has no tri-state. + int checked = 0; + int total = 0; + for (size_t c : section.categories) { + for (size_t r : m_categories[c].rows) { + if (!m_rows[r].check->IsEnabled()) + continue; // gated material rows don't count (defensive) + ++total; + if (m_rows[r].check->GetValue()) + ++checked; + } + } + if (total == 0 || checked == 0) + section.header->Set3StateValue(wxCHK_UNCHECKED); + else if (checked == total) + section.header->Set3StateValue(wxCHK_CHECKED); + else + section.header->Set3StateValue(wxCHK_UNDETERMINED); +} + +void PublishSettingsDialog::update_category_header(Category& category) +{ + // 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(category.rows.size())) + category.header->Set3StateValue(wxCHK_CHECKED); + else + category.header->Set3StateValue(wxCHK_UNDETERMINED); +} + +void PublishSettingsDialog::set_row_bold(Row& row, bool bold) +{ + // Real set/clear: rebase on the dialog's body font so that clearing bold + // restores the exact original font (the old CheckList::SetBold was one-way). + row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13); +} + +void PublishSettingsDialog::apply_filter(const wxString& filter_text) +{ + Freeze(); + wxString filter = filter_text.Lower(); + + // Pseudo filters (menu only): show only checked ("::sel") or only + // unchecked ("::nonsel") rows. + const bool pseudo = (filter == "::sel" || filter == "::nonsel"); + m_fb_sizer->Show(!pseudo); + + // Update the per-row match flags only; actual visibility is computed by + // apply_visibility() (which also respects the collapse state). + if (pseudo) { + if (m_filter_ctrl->GetValue().Lower() != filter) { + m_filter_ctrl->SetValue(filter); + m_filter_ctrl->SetSelection(0, -1); + } + const bool want_checked = (filter == "::sel"); + for (Row& row : m_rows) + row.matches_filter = row.check->IsEnabled() && row.check->GetValue() == want_checked; + } else { + const bool clear = filter.IsEmpty(); + for (Row& row : m_rows) { + row.matches_filter = clear || (row.section_title + " " + row.category + " " + row.subcategory + " " + row.label + " " + + row.value + " " + row.unit) + .Lower() + .Contains(filter); + } + } + + // The info label reflects the filter result only; a collapsed section + // hiding its matches is a user choice, not "no match". + m_info->Show(); + for (const Row& row : m_rows) { + if (row.matches_filter) { + m_info->Hide(); + break; + } + } + if (m_info->IsShown()) + m_info->SetLabel(pseudo ? (filter == "::sel" ? m_info_nonsel : m_info_allsel) : m_info_empty); + + apply_visibility(); + m_scroll->FitInside(); + Layout(); + Thaw(); +} + +void PublishSettingsDialog::apply_visibility() +{ + Freeze(); + for (SectionGroup& section : m_sections) { + // The section header stays visible whenever any row in the group matches + // (a section header never depends on its own collapsed state). + bool section_any = false; + for (size_t c : section.categories) { + for (size_t r : m_categories[c].rows) + if (m_rows[r].matches_filter) { + section_any = true; + break; + } + if (section_any) + break; + } + section.item->Show(section_any); + section.chevron->SetCollapsed(section.collapsed); + + for (size_t c : section.categories) { + Category& cat = m_categories[c]; + + // The category header stays visible whenever it has any match and + // the section is not collapsed, so it can always be re-expanded. + bool cat_any = false; + for (size_t r : cat.rows) + if (m_rows[r].matches_filter) { + cat_any = true; + break; + } + cat.item->Show(cat_any && !section.collapsed); + cat.chevron->SetCollapsed(cat.collapsed); + + for (Subcategory& sub : cat.subs) { + if (sub.header != nullptr) { + // The subcategory header visibility depends on its rows' matches + // and on its ancestors, but NOT on its own collapsed state. + bool sub_any = false; + for (size_t r : sub.rows) + if (m_rows[r].matches_filter) { + sub_any = true; + break; + } + sub.item->Show(sub_any && !section.collapsed && !cat.collapsed); + sub.chevron->SetCollapsed(sub.collapsed); + } + // Rows are hidden by the filter and by any collapsed ancestor. + for (size_t r : sub.rows) + m_rows[r].item->Show(m_rows[r].matches_filter && !section.collapsed && !cat.collapsed && !sub.collapsed); + } + } + } + Thaw(); +} + +void PublishSettingsDialog::toggle_section(size_t section_index) +{ + m_sections[section_index].collapsed = !m_sections[section_index].collapsed; + apply_visibility(); + m_scroll->FitInside(); + m_list_sizer->Layout(); +} + +void PublishSettingsDialog::toggle_category(size_t category_index) +{ + m_categories[category_index].collapsed = !m_categories[category_index].collapsed; + apply_visibility(); + m_scroll->FitInside(); + m_list_sizer->Layout(); +} + +void PublishSettingsDialog::toggle_subcategory(size_t category_index, size_t subcategory_index) +{ + m_categories[category_index].subs[subcategory_index].collapsed = !m_categories[category_index].subs[subcategory_index].collapsed; + apply_visibility(); + m_scroll->FitInside(); + m_list_sizer->Layout(); +} + +CollapseChevron* PublishSettingsDialog::create_chevron(wxWindow* parent, + const wxEventTypeTag& event_type, + std::function toggle) +{ + CollapseChevron* chevron = new CollapseChevron(parent); + chevron->SetCursor(wxCURSOR_HAND); + // The tag type (not wxEventType) keeps Bind's EventTag template deduced as + // wxEventTypeTag; wxEvent& is used so the helper works with + // any mouse event tag, and the toggle itself does not inspect the event. + chevron->Bind(event_type, [toggle](wxEvent&) { toggle(); }); + return chevron; +} + +void PublishSettingsDialog::select_all(bool value) +{ + // "All" does not auto-enable gated material sections; "None" leaves a gated + // row's preserved value untouched. + for (Row& row : m_rows) + if (row.check->IsEnabled()) + row.check->SetValue(value); + for (Category& cat : m_categories) + update_category_header(cat); +} + +void PublishSettingsDialog::select_visible(bool value) +{ + wxString filter = m_filter_ctrl->GetValue().Lower(); + // In a pseudo-filter view the rows being toggled would all disappear; + // drop the filter afterwards so the result stays visible. + bool clear_pseudo = (!value && filter == "::nonsel") || (value && filter == "::sel"); + + // Toggle the rows that are visible under the *current* filter. + for (Row& row : m_rows) + if (row.check->IsShown() && row.check->IsEnabled()) + row.check->SetValue(value); + + if (clear_pseudo) { + // Note: SetValue() may fire wxEVT_TEXT on some platforms, which + // re-enters apply_filter() - that is fine, the rows above were already + // toggled and the trailing call below is idempotent. + m_filter_ctrl->SetValue(""); + apply_filter(""); // resync visibility, headers and the All/None bar + } + for (Category& cat : m_categories) + update_category_header(cat); +} + +void PublishSettingsDialog::show_menu(wxMouseEvent& evt) +{ + bool filtering = !m_filter_ctrl->GetValue().IsEmpty(); + bool list_empty = m_info->IsShown(); + + wxMenu m; + m.Append(kPublishSelectAll, _L("Select All"))->Enable(!filtering); + m.Append(kPublishDeselectAll, _L("Deselect All"))->Enable(!filtering); + m.AppendSeparator(); + m.Append(kPublishSelectVisible, _L("Select visible"))->Enable(!list_empty && filtering); + m.Append(kPublishDeselectVisible, _L("Deselect visible"))->Enable(!list_empty && filtering); + m.AppendSeparator(); + m.Append(kPublishFilterSelected, _L("Filter selected")); + m.Append(kPublishFilterNonSelected, _L("Filter nonSelected")); + + m.Bind( + wxEVT_MENU, + [this](wxCommandEvent& e) { + switch (e.GetId()) { + case kPublishSelectAll: select_all(true); break; + case kPublishDeselectAll: select_all(false); break; + case kPublishSelectVisible: select_visible(true); break; + case kPublishDeselectVisible: select_visible(false); break; + case kPublishFilterSelected: apply_filter("::sel"); break; + case kPublishFilterNonSelected: apply_filter("::nonsel"); break; + default: break; + } + }, + kPublishSelectAll, kPublishFilterNonSelected); + + wxWindow* src = dynamic_cast(evt.GetEventObject()); + if (!src) + return; + wxPoint screen_pos = src->ClientToScreen(evt.GetPosition()); + wxPoint local_pos = ScreenToClient(screen_pos); + PopupMenu(&m, local_pos); +} + +std::vector PublishSettingsDialog::GetPublishedKeys() const +{ + std::vector out; + // Process and printer sections both travel through published_keys (the load-side + // overlay applies process keys to the prints edited preset and the allowlisted + // printer keys to the printers edited preset). Material keys use a separate API. + for (const Row& row : m_rows) + if ((row.section == Section::Print || row.section == Section::Printer) && row.check->GetValue()) + out.push_back(row.key); + return out; +} + +std::vector PublishSettingsDialog::GetPublishedMaterialKeys() const +{ + std::vector out; + for (const Category& cat : m_categories) { + // Only opted-in materials export their keys. + if (cat.section != Section::Material || !cat.master) + 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(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()) + out.push_back(std::move(entry)); + } + return out; +} + +void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) +{ + // The remaining bitmap icons are rescaled; the collapse chevrons are + // vector-drawn and repaint themselves. + m_search.msw_rescale(); + m_menu.msw_rescale(); + m_filter_box->SetIcon(m_search.bmp()); + m_menu_button->SetBitmap(m_menu.bmp()); + SetMinSize(FromDIP(wxSize(600, 500))); + m_scroll->FitInside(); + m_list_sizer->Layout(); + Refresh(); +} + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp new file mode 100644 index 0000000000..ff9cb88e4b --- /dev/null +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -0,0 +1,183 @@ +#ifndef slic3r_GUI_PublishSettingsDialog_hpp_ +#define slic3r_GUI_PublishSettingsDialog_hpp_ + +#include "GUI_Utils.hpp" +#include "wxExtensions.hpp" + +#include "libslic3r/PublishSettings.hpp" + +#include +#include +#include +#include +#include +#include + +// Forward declarations (all are global classes, see Widgets/TextInput.hpp, +// Widgets/StaticLine.hpp and the CollapseChevron definition in the .cpp). +class TextInput; +class StaticLine; +class CollapseChevron; + +namespace Slic3r { namespace GUI { + +// Dialog that lets a model author select which settings get embedded in a 3MF. +// Settings are grouped the way the tabs show them: the process (print) pages, +// the printer's per-extruder retraction settings, and one section per material +// used in the project (filament overrides). Each main category has a select-all +// tri-state header, subcategory (optgroup) headings, one row per setting +// (checkbox + grey value label), and both header levels are collapsible (chevron +// toggle). A search filter and an All/None / select-visible menu are provided. +// Modified (dirty) settings are pre-checked and shown bold. On OK, the print +// rows become the "published_keys" list and the material rows become the +// per-material "published_material_keys". +class PublishSettingsDialog : public DPIDialog +{ +public: + PublishSettingsDialog(wxWindow* parent = nullptr); + ~PublishSettingsDialog(); + + // The selected print-section setting keys (in display order). Keys may + // contain '#'. + std::vector GetPublishedKeys() const; + + // The selected keys grouped per material: one entry per material section + // with at least one checked key. Keys are base keys (no "#N" suffix). + std::vector GetPublishedMaterialKeys() const; + +protected: + void on_dpi_changed(const wxRect& suggested_rect) override; + +private: + // Which part of the settings the row/category came from. + enum class Section { Print, Printer, Material }; + + // 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. + struct Row + { + std::string key; + wxString category; + wxString subcategory; + wxString label; + wxString value; + wxString unit; + wxString section_title; // top-level group title, for filter matching + Section section{Section::Print}; + bool dirty{false}; // matches a dirty base key: pre-checked + bold + bool matches_filter{false}; // survives the active filter (computed by apply_filter) + wxCheckBox* check{nullptr}; + wxStaticText* value_label{nullptr}; + wxStaticText* unit_label{nullptr}; + wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in m_list_sizer + }; + + // A subcategory (optgroup) heading. Rows store indices into m_rows. + struct Subcategory + { + wxString title; + ::StaticLine* header{nullptr}; // null when the title is empty + bool collapsed{false}; + CollapseChevron* chevron{nullptr}; + wxSizerItem* item{nullptr}; // sizer item of the header h-sizer in m_list_sizer + std::vector rows; + }; + + // A main category with its select-all tri-state header. + struct Category + { + wxString title; + Section section{Section::Print}; + size_t group{0}; // index into m_sections + std::string icon_name; // bitmap name; empty = no icon + wxStaticBitmap* icon{nullptr}; // 18px category icon (null when icon_name empty) + wxCheckBox* header{nullptr}; // select-all tri-state + // 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}; + bool collapsed{false}; + CollapseChevron* chevron{nullptr}; + wxSizerItem* item{nullptr}; // sizer item of the header h-sizer in m_list_sizer + // Material identity, only for Section::Material categories. + std::string filament_type; + std::string filament_vendor; + std::string filament_id; + // The author's 0-based filament slot this material section represents. + size_t filament_slot{0}; + std::vector subs; + std::vector rows; // flattened rows, for the tri-state math + }; + + // A top-level section group mirroring the editor sidebar. Categories are + // nested inside. + struct SectionGroup + { + wxString title; // _L("Printer") / _L("Filament") / _L("Process") + Section kind{Section::Print}; // maps 1:1 to the display group + std::string icon_name; // "printer" / "filament" / "process" + wxStaticBitmap* icon{nullptr}; // 18px, like Category::icon + wxCheckBox* header{nullptr}; // tri-state select-all; nullptr for the Filament group + ::StaticLine* header_line{nullptr}; // Filament group's clickable title + CollapseChevron* chevron{nullptr}; + wxSizerItem* item{nullptr}; + bool collapsed{false}; + std::vector categories; // indices into m_categories + }; + + void build_option_model(); + void apply_filter(const wxString& filter_text); + 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); + // Collapse/expand a category or subcategory; resyncs visibility + chevrons. + void toggle_category(size_t category_index); + void toggle_subcategory(size_t category_index, size_t subcategory_index); + // Find-or-create the top-level section group for a Section kind (builds its + // header row on first use). + size_t section_group_for(Section kind); + // Top-level section group: select-all tri-state toggled / collapse/expand / + // header resync. + void on_section_toggle(size_t section_index); + void toggle_section(size_t section_index); + void update_section_header(SectionGroup& section); + // Single pass over categories/subs/rows: shows an item iff it is not hidden + // by the filter and (for subs/rows) by a collapsed ancestor. Flips the + // header chevrons. Only reads matches_filter; never re-runs filter matching. + void apply_visibility(); + // Creates a collapse chevron with a hand cursor; clicking it (with the given + // mouse event, LEFT_DOWN for categories / LEFT_UP for subcategories, + // matching the header's own binding) invokes the toggle. + CollapseChevron* create_chevron(wxWindow* parent, const wxEventTypeTag& event_type, std::function toggle); + + wxScrolledWindow* m_scroll{nullptr}; + wxBoxSizer* m_list_sizer{nullptr}; // vertical sizer of the scrolled window + wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer + TextInput* m_filter_box{nullptr}; + wxTextCtrl* m_filter_ctrl{nullptr}; + wxStaticBitmap* m_menu_button{nullptr}; + wxStaticText* m_info{nullptr}; + wxString m_info_nonsel; + wxString m_info_allsel; + wxString m_info_empty; + + ScalableBitmap m_search; + ScalableBitmap m_menu; + + std::vector m_rows; + std::vector m_categories; + // Fixed display order enforced by phase order in build_option_model(): + // Printer, then Filament, then Process. + std::vector m_sections; +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_GUI_PublishSettingsDialog_hpp_ diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1a31355d0e..935ab58592 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5675,6 +5675,9 @@ if (is_marlin_flavor) optgroup->append_single_option_line("extruder_offset", "printer_extruder_basic_information#extruder-offset-position", extruder_idx); //BBS: don't show retract related config menu in machine page + // Keep this optgroup's options in sync with publishable_printer_retraction_options() + // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union + // with the Z-Hop optgroup below. optgroup = page->new_optgroup(L("Retraction"), L"param_retraction"); optgroup->append_single_option_line("retraction_length", "printer_extruder_retraction#length", extruder_idx); optgroup->append_single_option_line("retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart", extruder_idx); @@ -5688,6 +5691,9 @@ if (is_marlin_flavor) // Orca optgroup->append_single_option_line("retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe", extruder_idx); + // Keep this optgroup's options in sync with publishable_printer_z_hop_options() + // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union + // with the Retraction optgroup above. optgroup = page->new_optgroup(L("Z-Hop"), L"param_extruder_lift_enforcement"); optgroup->append_single_option_line("retract_lift_enforce", "printer_extruder_z_hop#on-surfaces", extruder_idx); optgroup->append_single_option_line("z_hop_types", "printer_extruder_z_hop#z-hop-type", extruder_idx); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 19eb0b849d..1d3f8daf2a 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -30,7 +30,6 @@ #include //#include "BedShapeDialog.hpp" -#include "Event.hpp" #include "wxExtensions.hpp" #include "ConfigManipulation.hpp" #include "OptionsGroup.hpp" @@ -38,7 +37,6 @@ //BBS: GUI refactor #include "Notebook.hpp" #include "ParamsPanel.hpp" -#include "Widgets/RoundedRectangle.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/CheckBox.hpp" // ORCA @@ -472,6 +470,7 @@ protected: std::string m_last_sparse_infill_rotate_template_value; ConfigManipulation get_config_manipulation(); friend class EditGCodeDialog; + friend class PublishSettingsDialog; }; class TabPrint : public Tab diff --git a/src/slic3r/GUI/UnsavedChangesDialog.cpp b/src/slic3r/GUI/UnsavedChangesDialog.cpp index 5c69466cfa..dd1bf43253 100644 --- a/src/slic3r/GUI/UnsavedChangesDialog.cpp +++ b/src/slic3r/GUI/UnsavedChangesDialog.cpp @@ -12,6 +12,7 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Color.hpp" #include "format.hpp" +#include "ConfigValueFormatter.hpp" #include "GUI_App.hpp" #include "Plater.hpp" #include "Tab.hpp" @@ -571,14 +572,6 @@ void DiffModel::Clear() } -static std::string get_pure_opt_key(std::string opt_key) -{ - const int pos = opt_key.find("#"); - if (pos > 0) - boost::erase_tail(opt_key, opt_key.size() - pos); - return opt_key; -} - // ---------------------------------------------------------------------------- // DiffViewCtrl // ---------------------------------------------------------------------------- @@ -1212,32 +1205,6 @@ bool UnsavedChangesDialog::save(PresetCollection* dependent_presets, bool show_s return true; } -wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1) -{ - const ConfigOptionDef& def = config.def()->options.at(opt_key); - const std::vector& names = def.enum_labels;//ConfigOptionEnum::get_enum_names(); - int val = 0; - - if (idx >= 0) - val = dynamic_cast(config.option(opt_key))->get_at(idx); - else - val = config.option(opt_key)->getInt(); - - // Each infill doesn't use all list of infill declared in PrintConfig.hpp. - // So we should "convert" val to the correct one - if (is_infill) { - for (auto key_val : *def.enum_keys_map) - if (int(key_val.second) == val) { - auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first); - if (it == def.enum_values.end()) - return ""; - return from_u8(_utf8(names[it - def.enum_values.begin()])); - } - return _L("Undefined"); - } - return from_u8(_utf8(names[val])); -} - // BBS #if 0 static size_t get_id_from_opt_key(std::string opt_key) @@ -1251,194 +1218,6 @@ static size_t get_id_from_opt_key(std::string opt_key) } #endif -static wxString get_full_label(std::string opt_key, const DynamicPrintConfig& config) -{ - opt_key = get_pure_opt_key(opt_key); - auto option = config.option(opt_key); - - if (!option || option->is_nil()) - return _L("N/A"); - - const ConfigOptionDef* opt = config.def()->get(opt_key); - return opt->full_label.empty() ? opt->label : opt->full_label; -} - -static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& config) -{ - int orig_opt_idx = -1; - int opt_idx = -1; - int pos = opt_key.find("#"); - std::string temp_str = opt_key; - if (pos > 0) { - boost::erase_head(temp_str, pos + 1); - orig_opt_idx = static_cast(atoi(temp_str.c_str())); - } - opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0; - opt_key = get_pure_opt_key(opt_key); - auto option = config.option(opt_key); - if (!option) { - return _L("N/A"); - } - auto opt_vector = dynamic_cast(option); - - if (option->is_scalar() && config.option(opt_key)->is_nil() || - option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)) - return _L("N/A"); - - wxString out; - - const ConfigOptionDef* opt = config.def()->get(opt_key); - bool is_nullable = opt->nullable; - - switch (opt->type) { - case coInt: - return from_u8((boost::format("%1%") % config.opt_int(opt_key)).str()); - case coInts: { - if (is_nullable) { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str()); - } - else { - auto values = config.opt(opt_key); - if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) { - return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str()); - } - else { - std::string value_str; - for (int i = 0; i < values->size(); i++) { - value_str += std::to_string(values->get_at(i)); - if (i != values->size() - 1) { - value_str += ","; - } - } - return from_u8(value_str); - } - } - return _L("Undefined"); - } - case coBool: - return config.opt_bool(opt_key) ? "true" : "false"; - case coBools: { - if (is_nullable) { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return values->get_at(opt_idx) ? "true" : "false"; - } - else { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return values->get_at(opt_idx) ? "true" : "false"; - } - return _L("Undefined"); - } - case coPercent: - return from_u8((boost::format("%1%%%") % int(config.optptr(opt_key)->getFloat())).str()); - case coPercents: { - if (is_nullable) { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str()); - } - else { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str()); - } - return _L("Undefined"); - } - case coFloat: - return double_to_string(config.opt_float(opt_key)); - case coFloats: { - if (is_nullable) { - auto values = config.opt(opt_key); - if (opt_idx < values->size()) - return double_to_string(values->get_at(opt_idx)); - } - else { - auto values = config.opt(opt_key); - if (values && opt_idx < values->size()) - return double_to_string(values->get_at(opt_idx)); - } - return _L("Undefined"); - } - case coString: - return from_u8(config.opt_string(opt_key)); - case coStrings: { - const ConfigOptionStrings* strings = config.opt(opt_key); - if (strings) { - if (opt_key == "compatible_printers" || opt_key == "compatible_prints") { - if (strings->empty()) - return _L("All"); - for (size_t id = 0; id < strings->size(); id++) - out += from_u8(strings->get_at(id)) + "\n"; - out.RemoveLast(1); - return out; - } - if (!strings->empty() && opt_idx < strings->values.size()) - return from_u8(strings->get_at(opt_idx)); - } - break; - } - case coFloatOrPercent: { - const ConfigOptionFloatOrPercent* opt = config.opt(opt_key); - if (opt) - out = double_to_string(opt->value) + (opt->percent ? "%" : ""); - return out; - } - case coEnum: { - return get_string_from_enum(opt_key, config, - opt_key == "top_surface_pattern" || - opt_key == "bottom_surface_pattern" || - opt_key == "internal_solid_infill_pattern" || - opt_key == "sparse_infill_pattern" || - opt_key == "ironing_pattern" || - opt_key == "support_ironing_pattern" || - opt_key == "support_pattern" || - opt_key == "support_interface_pattern") - ; - } - case coEnums: { - return get_string_from_enum(opt_key, config, - opt_key == "top_surface_pattern" || - opt_key == "bottom_surface_pattern" || - opt_key == "internal_solid_infill_pattern" || - opt_key == "sparse_infill_pattern" || - opt_key == "ironing_pattern" || - opt_key == "support_ironing_pattern" || - opt_key == "support_pattern" || - opt_key == "support_interface_pattern" - , opt_idx); - } - case coPoint: { - Vec2d val = config.opt(opt_key)->value; - return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); - } - case coPoints: { - //BBS: add bed_exclude_area - if (opt_key == "printable_area" || opt_key == "thumbnails") { - ConfigOptionPoints points = *config.option(opt_key); - //BuildVolume build_volume = {points.values, 0.}; - return get_thumbnails_string(points.values); - } - else if (opt_key == "bed_exclude_area") { - return get_thumbnails_string(config.option(opt_key)->values); - } - else if (opt_key == "head_wrap_detect_zone") { - return get_thumbnails_string(config.option(opt_key)->values); - } - else if (opt_key == "wrapping_exclude_area") { - return get_thumbnails_string(config.option(opt_key)->values); - } - Vec2d val = config.opt(opt_key)->get_at(opt_idx); - return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str()); - } - default: - break; - } - return out; -} - void UnsavedChangesDialog::update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header) { PresetCollection* presets = dependent_presets; diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index c839149f5f..4eccf0e9fa 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -11,6 +11,8 @@ #include "test_utils.hpp" +#include + #include #include @@ -497,3 +499,178 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { delete plate; } } + +// The "Publish" feature stores a published flag plus a JSON array of author-selected setting keys +// in model.model_info->metadata_items. This locks the serialization contract: both keys must survive +// a store_bbs_3mf -> load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior — +// keeping the user's currently-selected presets and overlaying only the published keys onto them — +// is exercised headlessly in "Published 3MF overlays only the author-selected process keys onto the +// edited preset" in test_preset_bundle_loading.cpp.) +SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") { + GIVEN("a model carrying published 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(); + + model.model_info = std::make_shared(); + model.model_info->metadata_items["published"] = "1"; + model.model_info->metadata_items["published_keys"] = R"(["layer_height","wall_thickness"])"; + + // store_bbs_3mf stages Metadata/project_settings.config through the model's backup path; + // point it at a writable temp dir (the default lives under a read-only root in CI). + ScopedTemporaryDir backup_dir("orca_pub"); + 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 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 published metadata round-trips unchanged") { + REQUIRE(loaded); + REQUIRE(dst_model.model_info != nullptr); + REQUIRE(dst_model.model_info->metadata_items["published"] == "1"); + REQUIRE(dst_model.model_info->metadata_items["published_keys"] == R"(["layer_height","wall_thickness"])"); + + // The published_keys value is a JSON array of setting keys; it must parse back to + // the same keys that were selected. + nlohmann::json keys = nlohmann::json::parse(dst_model.model_info->metadata_items["published_keys"]); + REQUIRE(keys.is_array()); + REQUIRE(keys.size() == 2); + REQUIRE(keys[0] == "layer_height"); + REQUIRE(keys[1] == "wall_thickness"); + } + release_PlateData_list(dst_plates); + } + } +} + +// A project saved without the Publish metadata (i.e. a normal 3MF) must load identically: the +// loader must not fabricate a "published" flag or published_keys for files that never carried them. +SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { + GIVEN("a model without any published 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(); + + ScopedTemporaryDir backup_dir("orca_legacy"); + 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 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("no published key is fabricated") { + REQUIRE(loaded); + if (dst_model.model_info != nullptr) { + REQUIRE(dst_model.model_info->metadata_items.count("published") == 0); + REQUIRE(dst_model.model_info->metadata_items.count("published_keys") == 0); + } + } + release_PlateData_list(dst_plates); + } + } +} + +// The "Publish" feature can also store material-qualified setting keys, one entry per material +// the author uses, in model.model_info->metadata_items. This locks the serialization contract +// for that entry list: the JSON must survive a store_bbs_3mf -> load_bbs_3mf round-trip +// verbatim, exactly like the plain published_keys array. +SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") { + GIVEN("a model carrying 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":0,"keys":["filament_retraction_length","filament_z_hop"]}])"; + + model.model_info = std::make_shared(); + model.model_info->metadata_items["published_material_keys"] = material_keys_json; + + ScopedTemporaryDir backup_dir("orca_pub_mat"); + 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 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 published material keys 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 to one material entry carrying the nested identity + // object, the author slot ordinal and the key list, so the loader can match it + // to the receiver's filaments. + nlohmann::json entries = nlohmann::json::parse(material_keys_json); + REQUIRE(entries.is_array()); + REQUIRE(entries.size() == 1); + REQUIRE(entries[0]["material"]["filament_type"] == "PLA"); + REQUIRE(entries[0]["material"]["filament_vendor"] == "Generic"); + REQUIRE(entries[0]["material"]["filament_id"] == "GFL99"); + REQUIRE(entries[0]["slot"] == 0); + REQUIRE(entries[0]["keys"].is_array()); + REQUIRE(entries[0]["keys"].size() == 2); + REQUIRE(entries[0]["keys"][0] == "filament_retraction_length"); + } + release_PlateData_list(dst_plates); + } + } +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 844ccb6a8b..fc61083896 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -7,12 +7,20 @@ #include "test_utils.hpp" +#include + using namespace Slic3r; namespace { namespace fs = boost::filesystem; +// Whether a key is listed in a vector of keys (published_keys / skipped_keys). +bool contains_key(const std::vector &keys, const std::string &key) +{ + return std::find(keys.begin(), keys.end(), key) != keys.end(); +} + void write_print_preset(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits = {}) { DynamicPrintConfig config(default_config); @@ -540,3 +548,530 @@ TEST_CASE("A printer specific filament supersedes the generic library filament w CHECK(is_compatible_with_printer(generic_lib, PresetWithVendorProfile(*printer_c, nullptr))); } +// A "published" 3MF keeps the user's currently-selected presets and overlays only the +// author-selected process keys onto the edited preset. Mirrors the GUI load path +// (src/slic3r/GUI/Plater.cpp): Preset::normalize before load_config_model, then the +// published overlay in PresetBundle::load_config_file_config. +TEST_CASE("Published 3MF overlays only the author-selected process keys onto the edited preset", "[Preset][Bundle][Published]") +{ + // The file config the GUI builds from a .3mf's project settings. + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // The loader derives the filament count from filament_colour and throws when it is + // empty ("Invalid configuration file"); a 3mf always carries it. + config.opt("filament_colour")->values = { "#FF0000" }; + // Process scalar key. + config.opt_float("layer_height") = 0.28; + // Process vector key, size 2 to match the edited preset's resized vector. + config.opt("wiping_volumes_extruders")->values = { 140., 150. }; + // Process vector key, size 2: deliberately mismatched against the edited preset. + config.opt("post_process")->values = { "script-a", "script-b" }; + // A filament key: published files may still carry legacy filament keys. + config.opt("nozzle_temperature")->values = { 220 }; + // A structural (denylisted) key: must be silently ignored even if a hand-crafted + // file lists it as published. full_print_config() omits the *_settings_id keys (they + // have no static counterpart), while a real 3mf project config carries it, so create + // it explicitly. + config.opt_string("print_settings_id", true) = "file process"; + // Project-level filament/purge data: must NOT cross over in published mode. + config.opt("flush_multiplier")->values = { 2., 2. }; + // A project-level option, to pin the project_config.apply_only() invariant. + config.opt("wipe_tower_x")->values = { 100. }; + // The author's bed type must NOT cross over either: the receiver keeps its own. + config.option("curr_bed_type")->setInt(BedType::btPC); + return config; + }; + + const std::vector published_keys = { + "layer_height", "wiping_volumes_extruders", "post_process", "nozzle_temperature", "print_settings_id" + }; + + PresetBundle bundle; + const std::string pre_load_name = bundle.prints.get_edited_preset().name; + const size_t pre_load_size = bundle.prints.size(); + + // The edited presets are the overlay targets: recognizable pre-load values. + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.1; + bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values = { 10., 20. }; + bundle.prints.get_edited_preset().config.opt("post_process")->values = { "existing-script" }; + bundle.prints.get_edited_preset().config.opt_string("print_settings_id") = "user process"; + // Capture the ctor-seeded project_config values; the assertions below check that the + // published load leaves them untouched rather than hardcoding the defaults. + const std::vector seed_filament_colour = bundle.project_config.opt("filament_colour")->values; + const std::vector seed_flush_multiplier = bundle.project_config.opt("flush_multiplier")->values; + const int seed_bed_type = bundle.project_config.option("curr_bed_type")->getInt(); + + DynamicPrintConfig config = make_file_config(); + // The GUI normalizes the config before load; do the same so only the production path is exercised. + Preset::normalize(config); + + PublishedConfig pub; + pub.published = true; + pub.published_keys = published_keys; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // a) The process scalar is overlaid onto the edited preset. + CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); + + // b) A matching-size process vector is applied; a size-mismatched one is neither applied + // nor reported as skipped by accident — it lands in skipped_keys. + CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 140., 150. }); + CHECK(bundle.prints.get_edited_preset().config.opt("post_process")->values == std::vector{ "existing-script" }); + CHECK(contains_key(pub.skipped_keys, "post_process")); + + // c) A filament key is never applied anywhere and is reported as skipped (warning). + CHECK(bundle.prints.get_edited_preset().config.option("nozzle_temperature") == nullptr); + CHECK(contains_key(pub.skipped_keys, "nozzle_temperature")); + + // d) A structural key is silently ignored: neither applied nor reported as skipped. + CHECK(bundle.prints.get_edited_preset().config.opt_string("print_settings_id") == "user process"); + CHECK_FALSE(contains_key(pub.skipped_keys, "print_settings_id")); + + // Applied keys are not reported as skipped. + CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); + CHECK_FALSE(contains_key(pub.skipped_keys, "wiping_volumes_extruders")); + + // e) project_config.apply_only() still runs, but in published mode only the plate/bed + // geometry crosses: the file's filament/purge data must NOT port to the receiver. + // Filament colors do not port; project_config keeps its ctor-seeded values. + CHECK(bundle.project_config.opt("filament_colour")->values != std::vector{ "#FF0000" }); + CHECK(bundle.project_config.opt("filament_colour")->values == seed_filament_colour); + // Purge data does not port either, and update_multi_material_filament_presets() cannot + // resurrect it (flush_multiplier stays at the ctor seed). + CHECK(bundle.project_config.opt("flush_multiplier")->values == seed_flush_multiplier); + // The author's bed type does not cross over: the receiver keeps its own. + CHECK(bundle.project_config.option("curr_bed_type")->getInt() == seed_bed_type); + // Plate/bed geometry still crosses. + CHECK(bundle.project_config.opt("wipe_tower_x")->values == std::vector{ 100. }); + + // The published path keeps the user's currently-selected presets: the edited process + // preset is the same preset as before the load. + CHECK(bundle.prints.get_edited_preset().name == pre_load_name); + CHECK(bundle.prints.size() == pre_load_size); + + // f) Non-published control: with published=false the overlay is disabled. The file's + // presets are loaded and selected instead (the user's preset is not kept) and no + // skipped_keys are produced. + PresetBundle control_bundle; + const size_t control_pre_size = control_bundle.prints.size(); + PublishedConfig control_pub; + control_pub.published = false; + control_pub.published_keys = published_keys; + DynamicPrintConfig control_config = make_file_config(); + Preset::normalize(control_config); + control_bundle.load_config_model("test.3mf", std::move(control_config), Semver(), &control_pub); + + CHECK(control_pub.skipped_keys.empty()); + CHECK(control_bundle.prints.size() > control_pre_size); + // The file's layer_height reached the edited preset through the normal preset import, + // not through the published overlay. + CHECK_THAT(control_bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); +} + +// The published printer overlay is restricted to the publishable retraction/z-hop allowlist +// (publishable_printer_keys). Matching-size retraction vectors apply; mismatched vectors are +// reported as skipped; any other printer-class key (e.g. machine_start_gcode) is +// contract-excluded: never applied and never reported. +TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys onto the edited printer preset", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + Preset::normalize(config); + // Matching size (the receiver's default printer has one extruder). + config.opt("retraction_length")->values = { 1.4 }; + // Size 2: mismatched against the single-extruder receiver. + config.opt("retraction_speed")->values = { 45., 55. }; + // Printer-class but outside the allowlist: must be silently contract-excluded. + config.opt_string("machine_start_gcode") = "G28 ; from file"; + + PresetBundle bundle; + bundle.printers.get_edited_preset().config.opt("retraction_length")->values = { 0.8 }; + bundle.printers.get_edited_preset().config.opt_string("machine_start_gcode") = "G28 ; user"; + + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "retraction_length", "retraction_speed", "machine_start_gcode" }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Matching-size retraction vector applied to the edited printer preset. + CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 1.4 }); + // Mismatched vector not applied and reported as skipped. + CHECK(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values == std::vector{ 30. }); + CHECK(contains_key(pub.skipped_keys, "retraction_speed")); + // Contract-excluded printer key: silently ignored, absent from skipped_keys. + CHECK(bundle.printers.get_edited_preset().config.opt_string("machine_start_gcode") == "G28 ; user"); + CHECK_FALSE(contains_key(pub.skipped_keys, "machine_start_gcode")); +} + +// A published 3MF can carry material-qualified keys; on load they are applied to the +// receiver's filament presets whose material identity matches the author's (filament_id when +// both sides have one, filament_type + vendor fallback otherwise). +TEST_CASE("Published 3MF applies material retraction keys onto the receiver's matching filament presets", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // Two filament slots; filament_diameter drives the normalized per-slot vector sizes. + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + // Keep the multi-extruder consistency validation happy for a 2-slot config. + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + // Author per-slot material identity (filament_ids feeds the loader's local copy). + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PETG" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFT99" }; + // Author per-slot retraction values. These are per-filament override keys that are not + // members of the static PrintRegionConfig, so full_print_config() omits them and they + // must be created explicitly (as nullable, matching the real 3MF project config). + config.option("filament_retraction_length", true)->values = { 0.9, 1.2 }; + config.option("filament_z_hop", true)->values = { 0.2, 0.3 }; + return config; + }; + + PresetBundle bundle; + // Receiver materials with matching stable ids. + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.filament_id = "GFL99"; + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt_string("filament_vendor", 0u) = "Generic"; + // In-memory preset configs carry the per-filament retraction keys as nullable options + // (the type real filament presets hold), so access them through the nullable type. + pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + pla.config.opt("filament_settings_id")->values = { "receiver-pla" }; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.filament_id = "GFT99"; + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt_string("filament_vendor", 0u) = "Generic"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + // The z-hop key must exist on the receiver preset for the overlay to apply into it. + petg.config.opt("filament_z_hop", true)->values = { 0.1 }; + bundle.filament_presets = { "My PLA", "My PETG" }; + + PublishedMaterialEntry pla_entry; + pla_entry.filament_type = "PLA"; + pla_entry.filament_vendor = "Generic"; + pla_entry.filament_id = "GFL99"; + pla_entry.slot = 0; // the author's PLA slot + pla_entry.keys = { "filament_retraction_length", "filament_settings_id" }; + PublishedMaterialEntry petg_entry; + petg_entry.filament_type = "PETG"; + petg_entry.filament_vendor = "Generic"; + petg_entry.filament_id = "GFT99"; + petg_entry.slot = 1; // the author's PETG slot + petg_entry.keys = { "filament_retraction_length", "filament_z_hop" }; + // A material that does not exist on the author's side: whole entry skipped, no reporting. + PublishedMaterialEntry abs_entry; + abs_entry.filament_type = "ABS"; + abs_entry.filament_id = "GFX99"; + abs_entry.keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { pla_entry, petg_entry, abs_entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Per-slot scalar copy: the author's slot value lands in the matching receiver preset. + CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); + CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_z_hop")->values == std::vector{ 0.3 }); + // Structural keys inside a material entry are silently ignored: the receiver's own + // filament_settings_id is left untouched and nothing is reported for it. + CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_settings_id")->values == std::vector{ "receiver-pla" }); + CHECK_FALSE(contains_key(pub.skipped_keys, "material:GFL99 (filament_settings_id)")); + // Everything applied; the unknown material entry produced no skipped entry. + CHECK(pub.skipped_keys.empty()); +} + +// 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. +TEST_CASE("Published 3MF reports material keys with no unique receiver match as skipped", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // Three author slots: PLA, PETG, ABS. + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF" }; + config.opt("filament_type")->values = { "PLA", "PETG", "ABS" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFT99", "GFA99" }; + config.option("filament_retraction_length", true)->values = { 0.9, 1.2, 1.5 }; + return config; + }; + + PresetBundle bundle; + // Two receiver presets of the SAME type with no filament_id: the type fallback is ambiguous. + Preset &pla_a = add_inmemory_preset(bundle.filaments, "My PLA A"); + pla_a.config.opt_string("filament_type", 0u) = "PLA"; + pla_a.config.opt_string("filament_vendor", 0u) = "Generic"; + pla_a.config.opt("filament_retraction_length", true)->values = { 0.5 }; + Preset &pla_b = add_inmemory_preset(bundle.filaments, "My PLA B"); + pla_b.config.opt_string("filament_type", 0u) = "PLA"; + pla_b.config.opt_string("filament_vendor", 0u) = "Generic"; + pla_b.config.opt("filament_retraction_length", true)->values = { 0.5 }; + // A unique PETG receiver preset: the single type fallback is unambiguous. + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt_string("filament_vendor", 0u) = "Generic"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + bundle.filament_presets = { "My PLA A", "My PLA B", "My PETG" }; + + auto make_entry = [](const std::string &type, const std::string &key) { + PublishedMaterialEntry entry; + entry.filament_type = type; + entry.filament_vendor = "Generic"; + entry.keys = { key }; + return entry; + }; + + PublishedMaterialEntry pla_entry = make_entry("PLA", "filament_retraction_length"); + pla_entry.slot = 0; // the author's PLA slot + PublishedMaterialEntry petg_entry = make_entry("PETG", "filament_retraction_length"); + petg_entry.slot = 1; // the author's PETG slot + PublishedMaterialEntry abs_entry = make_entry("ABS", "filament_retraction_length"); // author slot exists, no receiver match + abs_entry.slot = 2; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { pla_entry, petg_entry, abs_entry }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Ambiguous type fallback: neither PLA preset is touched, reported as skipped. + CHECK(bundle.filaments.find_preset("My PLA A")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filaments.find_preset("My PLA B")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(contains_key(pub.skipped_keys, "material:PLA (filament_retraction_length)")); + // Unambiguous single fallback: applied. + CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); + CHECK_FALSE(contains_key(pub.skipped_keys, "material:PETG (filament_retraction_length)")); + // Receiver-side miss: the author slot exists but no receiver preset matches. + CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)")); +} + +// A slotted material entry carries the author's per-slot overrides: on load it applies to the +// receiver's matching preset at the author's slot ordinal (first matching author slot -> first +// matching receiver preset, second -> second, ...). Legacy entries without a slot keep applying +// to every matching receiver preset. +TEST_CASE("Published material keys apply to the receiver's matching filament preset by author slot ordinal", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // Three author slots of the SAME material (PETG) with distinct per-slot retraction. + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF" }; + config.opt("filament_type")->values = { "PETG", "PETG", "PETG" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFT99", "GFT99", "GFT99" }; + config.option("filament_retraction_length", true)->values = { 0.7, 0.8, 0.9 }; + return config; + }; + auto make_slotted_entry = [](int slot) { + PublishedMaterialEntry entry; + entry.filament_type = "PETG"; + entry.filament_vendor = "Generic"; + entry.filament_id = "GFT99"; + entry.slot = slot; + entry.keys = { "filament_retraction_length" }; + return entry; + }; + auto add_petg_preset = [](PresetBundle &bundle, const std::string &name) { + Preset &preset = add_inmemory_preset(bundle.filaments, name); + preset.filament_id = "GFT99"; + preset.config.opt_string("filament_type", 0u) = "PETG"; + preset.config.opt_string("filament_vendor", 0u) = "Generic"; + preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; + return &preset; + }; + + // Three receiver presets, one per author slot: each gets its ordinal's value. + { + PresetBundle bundle; + add_petg_preset(bundle, "My PETG 1"); + add_petg_preset(bundle, "My PETG 2"); + add_petg_preset(bundle, "My PETG 3"); + bundle.filament_presets = { "My PETG 1", "My PETG 2", "My PETG 3" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_slotted_entry(0), make_slotted_entry(1), make_slotted_entry(2) }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Each author slot's value lands in the receiver preset at the same ordinal. + CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); + CHECK(bundle.filaments.find_preset("My PETG 2")->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); + CHECK(bundle.filaments.find_preset("My PETG 3")->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(pub.skipped_keys.empty()); + } + + // A single receiver preset: only the first ordinal fits; the later slots are reported + // with a slot-qualified label. + { + PresetBundle bundle; + add_petg_preset(bundle, "My PETG 1"); + bundle.filament_presets = { "My PETG 1" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_slotted_entry(0), make_slotted_entry(1), make_slotted_entry(2) }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); + CHECK(contains_key(pub.skipped_keys, "material:GFT99 slot 1 (filament_retraction_length)")); + CHECK(contains_key(pub.skipped_keys, "material:GFT99 slot 2 (filament_retraction_length)")); + CHECK_FALSE(contains_key(pub.skipped_keys, "material:GFT99 slot 0 (filament_retraction_length)")); + } + + // A legacy entry (no slot) applies to every matching receiver preset, from the first + // author slot. + { + PresetBundle bundle; + add_petg_preset(bundle, "My PETG 1"); + add_petg_preset(bundle, "My PETG 2"); + bundle.filament_presets = { "My PETG 1", "My PETG 2" }; + + PublishedMaterialEntry legacy = make_slotted_entry(0); + legacy.slot = -1; // legacy: no slot field + PublishedConfig pub; + pub.published = true; + pub.material_keys = { legacy }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); + CHECK(bundle.filaments.find_preset("My PETG 2")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); + CHECK(pub.skipped_keys.empty()); + } + + // An out-of-range author slot is silently skipped: nothing applied, nothing reported. + { + PresetBundle bundle; + add_petg_preset(bundle, "My PETG 1"); + bundle.filament_presets = { "My PETG 1" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_slotted_entry(5) }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(pub.skipped_keys.empty()); + } +} + +// The published overlay must validate '#' variant indices: an out-of-range index must be +// reported as skipped and must NOT resize/corrupt the receiver's vector, and a variant suffix +// on a scalar key must be rejected instead of silently no-op'd. +TEST_CASE("Published 3MF rejects out-of-range vector variants and variant-suffixed scalar keys", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + // Vector key, size 2 (matches the receiver's resized vector); distinct values so the + // applied element is observable. + config.opt("wiping_volumes_extruders")->values = { 140., 150. }; + config.opt_float("layer_height") = 0.28; + Preset::normalize(config); + + PresetBundle bundle; + bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values = { 10., 20. }; + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.1; + + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "wiping_volumes_extruders#5", "wiping_volumes_extruders#1", "layer_height#0" }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // In-range variant applied element-wise; the out-of-range one did not resize the vector. + CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 10., 150. }); + CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values.size() == 2); + // Out-of-range variant and variant-suffixed scalar are reported as skipped. + CHECK(contains_key(pub.skipped_keys, "wiping_volumes_extruders#5")); + CHECK(contains_key(pub.skipped_keys, "layer_height#0")); + CHECK_FALSE(contains_key(pub.skipped_keys, "wiping_volumes_extruders#1")); + // The scalar was never applied. + CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.1, 0.000001)); +} + +// A receiver filament preset whose material identity fields are missing (hand-edited preset +// file) must not crash the material pass: the entry simply cannot match and is reported skipped. +TEST_CASE("Published 3MF survives a receiver filament preset missing its material identity", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + Preset::normalize(config); + + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + // Malformed receiver preset: the identity options are missing entirely. + pla.config.erase("filament_type"); + pla.config.erase("filament_vendor"); + pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + bundle.filament_presets = { "My PLA" }; + + PublishedMaterialEntry entry; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.filament_id = "GFL99"; + entry.slot = 0; + entry.keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // No match possible without the identity fields: reported skipped, preset untouched. + CHECK(contains_key(pub.skipped_keys, "material:GFL99 (filament_retraction_length)")); + CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); +} + +// The printer publishable allowlist is the union of the printer tab's "Retraction" and +// "Z-Hop" optgroup option lists; lock the exact contents and order (Tab.cpp). +TEST_CASE("Printer publishable allowlist matches the printer tab's Retraction and Z-Hop optgroups", "[Preset][Bundle][Published]") +{ + auto keys_of = [](const std::vector& opts) { + std::vector keys; + keys.reserve(opts.size()); + for (const PublishablePrinterOption& opt : opts) + keys.emplace_back(opt.key); + return keys; + }; + + const std::vector expected_retraction = { + "retraction_length", "retract_restart_extra", "retraction_speed", "deretraction_speed", + "retraction_minimum_travel", "retract_when_changing_layer", "wipe", "wipe_distance", + "retract_before_wipe", "retract_after_wipe" + }; + const std::vector expected_z_hop = { + "retract_lift_enforce", "z_hop_types", "z_hop", "travel_slope", "retract_lift_above", + "retract_lift_below" + }; + + CHECK(keys_of(publishable_printer_retraction_options()) == expected_retraction); + CHECK(keys_of(publishable_printer_z_hop_options()) == expected_z_hop); + + std::set expected_union(expected_retraction.begin(), expected_retraction.end()); + expected_union.insert(expected_z_hop.begin(), expected_z_hop.end()); + CHECK(publishable_printer_keys() == expected_union); +} + From 96f5a2338711e891c3a7270c76f991f1797109ca Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 12:53:44 +0800 Subject: [PATCH 02/60] Only serialize selected published settings. Minor cleanup --- src/libslic3r/Format/bbs_3mf.cpp | 6 +- src/libslic3r/Format/bbs_3mf.hpp | 1 + src/libslic3r/PublishSettings.cpp | 57 ++++++++++++++++++ src/libslic3r/PublishSettings.hpp | 8 +++ src/slic3r/GUI/ConfigValueFormatter.cpp | 6 +- src/slic3r/GUI/Plater.cpp | 17 ++++-- src/slic3r/GUI/Plater.hpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 32 ++++++++-- src/slic3r/GUI/PublishSettingsDialog.hpp | 2 + tests/libslic3r/test_3mf.cpp | 77 ++++++++++++++++++++++++ 10 files changed, 190 insertions(+), 18 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 3000adb441..a5d20807b7 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -5943,6 +5943,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool m_save_gcode { false }; // whether to save gcode for normal save bool m_skip_model { false }; // skip model when exporting .gcode.3mf bool m_skip_auxiliary { false }; // skip normal axuiliary files + bool m_minimal_published { false }; // published 3MF: omit embedded preset files bool m_use_loaded_id { false }; // whether to use loaded id for identify_id bool m_share_mesh { false }; // whether to share mesh between objects std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE; @@ -6042,6 +6043,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary; m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh; m_from_backup_save = store_params.strategy & SaveStrategy::Backup; + m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished; m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId; @@ -6464,8 +6466,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (cb_cancel) return false; } - // BBS: add project config - if (project_presets.size() > 0) { + // BBS: add project config (omitted for minimal published 3MF) + if (!m_minimal_published && project_presets.size() > 0) { // BBS: add project embedded preset files _add_project_embedded_presets_to_archive(archive, model, project_presets); diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 9c697a14fc..0addf23385 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -145,6 +145,7 @@ enum class SaveStrategy SkipAuxiliary = 1 << 9, UseLoadedId = 1 << 10, ShareMesh = 1 << 11, + MinimalPublished = 1 << 12, SplitModel = 0x1000 | ProductionExt, Encrypted = SecureContentExt | SplitModel, diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 4c0a851696..9ac0c49472 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -2,6 +2,7 @@ #include "PresetBundle.hpp" #include "Preset.hpp" +#include "PrintConfig.hpp" #include @@ -102,4 +103,60 @@ std::vector collect_dirty_settings_keys(const PresetBundle& bundle) return keys; } +DynamicPrintConfig filter_published_config( + const DynamicPrintConfig &full_config, + const std::vector &published_keys, + const std::vector &material_keys) +{ + DynamicPrintConfig filtered; + + std::set base_keys_to_include; + + // 1. Mandatory material identity & slot count keys for 3MF validation/normalization + static const std::vector s_material_identity_keys = { + "filament_colour", + "filament_type", + "filament_vendor", + "filament_ids", + "filament_diameter", + "filament_self_index", + "filament_extruder_variant" + }; + for (const std::string &key : s_material_identity_keys) + base_keys_to_include.insert(key); + + // 2. Published plate / bed geometry keys (wipe tower positioning) + static const std::vector s_plate_geometry_keys = { + "wipe_tower_x", + "wipe_tower_y", + "wipe_tower_rotation_angle" + }; + for (const std::string &key : s_plate_geometry_keys) + base_keys_to_include.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()) + base_keys_to_include.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()) + base_keys_to_include.insert(base_key); + } + } + + // 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()); + } + + return filtered; +} + } // namespace Slic3r diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index ecc939d883..6525fbb66e 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -53,4 +53,12 @@ struct PublishedMaterialEntry { int slot{-1}; std::vector keys; }; + +// 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; +DynamicPrintConfig filter_published_config( + const DynamicPrintConfig &full_config, + const std::vector &published_keys, + const std::vector &material_keys); } diff --git a/src/slic3r/GUI/ConfigValueFormatter.cpp b/src/slic3r/GUI/ConfigValueFormatter.cpp index f3ea539d4f..6f9128841b 100644 --- a/src/slic3r/GUI/ConfigValueFormatter.cpp +++ b/src/slic3r/GUI/ConfigValueFormatter.cpp @@ -73,7 +73,7 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& std::string temp_str = opt_key; if (pos > 0) { boost::erase_head(temp_str, pos + 1); - orig_opt_idx = static_cast(atoi(temp_str.c_str())); + orig_opt_idx = std::atoi(temp_str.c_str()); } opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0; const std::string pure_key = get_pure_opt_key(opt_key); @@ -83,8 +83,8 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& } auto opt_vector = dynamic_cast(option); - if (option->is_scalar() && config.option(pure_key)->is_nil() || - option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)) + if ((option->is_scalar() && option->is_nil()) || + (option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx))) return _L("N/A"); wxString out; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fd307d32d2..571d7350b9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7324,7 +7324,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } auto choise = wxGetApp().app_config->get("no_warn_when_modified_gcodes"); - if (choise.empty() || choise != "true") { + if (!published_config.published && (choise.empty() || choise != "true")) { // BBS: first validate the printer // validate the system profiles std::set modified_gcodes; @@ -16204,16 +16204,21 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items["published_keys"] = j.dump(); model.model_info->metadata_items["published_material_keys"] = jm.dump(); - // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence: + // Minimal published export: filter full_config to only the published keys, material keys, + // identity fields, and plate geometry keys, and omit project-embedded preset dumps. + DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + + // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence and SaveStrategy::MinimalPublished: // without it export_3mf() calls set_project_filename() on success, which would make this // pure export the current project file. Silence keeps the project state untouched, exactly // like export_core_3mf(). - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence; + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) save_strategy = save_strategy | SaveStrategy::FullPathSources; - const int ret = export_3mf(into_path(path), save_strategy); + const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr, &filtered_cfg); // Restore the previous metadata state (both on success and on failure). if (!had_model_info) { @@ -16737,7 +16742,7 @@ void publish(Model &model, SaveStrategy strategy) { } // BBS: backup -int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) +int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn, const DynamicPrintConfig* override_config) { int ret = 0; //if (p->model.objects.empty()) { @@ -16759,7 +16764,7 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // modify model publish(p->model, strategy); - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig cfg = override_config ? *override_config : wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 5a83e5e4e1..d29e86a4fa 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -505,7 +505,7 @@ public: //void export_amf(); //BBS add extra param for exporting 3mf silence // BBS: backup - int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr); + int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr, const DynamicPrintConfig* override_config = nullptr); //BBS void publish_project(); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 36b7372d4a..90ad2e0dc3 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -321,8 +321,8 @@ void PublishSettingsDialog::build_option_model() cat.filament_id = identity.id; cat.filament_slot = slot; if (!icon_name.empty()) { - ScalableBitmap icon_bmp(m_scroll, icon_name, 18); - cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + cat.icon_bmp = ScalableBitmap(m_scroll, icon_name, 18); + cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, cat.icon_bmp.bmp()); } if (section == Section::Material) { // Material header: [master (title)][slim tri-state select-all]. @@ -667,8 +667,8 @@ size_t PublishSettingsDialog::section_group_for(Section kind) } if (!section.icon_name.empty()) { - ScalableBitmap icon_bmp(m_scroll, section.icon_name, 18); - section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, icon_bmp.bmp()); + section.icon_bmp = ScalableBitmap(m_scroll, section.icon_name, 18); + section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, section.icon_bmp.bmp()); } section.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_section(new_index); }); @@ -1062,12 +1062,32 @@ std::vector PublishSettingsDialog::GetPublishedM void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) { - // The remaining bitmap icons are rescaled; the collapse chevrons are - // vector-drawn and repaint themselves. + // Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves. m_search.msw_rescale(); m_menu.msw_rescale(); m_filter_box->SetIcon(m_search.bmp()); m_menu_button->SetBitmap(m_menu.bmp()); + + for (SectionGroup& section : m_sections) { + if (section.icon != nullptr && section.icon_bmp.bmp().IsOk()) { + section.icon_bmp.msw_rescale(); + section.icon->SetBitmap(section.icon_bmp.bmp()); + } + if (section.header_line != nullptr) + section.header_line->Rescale(); + } + + for (Category& cat : m_categories) { + if (cat.icon != nullptr && cat.icon_bmp.bmp().IsOk()) { + cat.icon_bmp.msw_rescale(); + cat.icon->SetBitmap(cat.icon_bmp.bmp()); + } + for (Subcategory& sub : cat.subs) { + if (sub.header != nullptr) + sub.header->Rescale(); + } + } + SetMinSize(FromDIP(wxSize(600, 500))); m_scroll->FitInside(); m_list_sizer->Layout(); diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index ff9cb88e4b..4767b9a29e 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -91,6 +91,7 @@ private: Section section{Section::Print}; size_t group{0}; // index into m_sections std::string icon_name; // bitmap name; empty = no icon + ScalableBitmap icon_bmp; // scalable bitmap for DPI changes wxStaticBitmap* icon{nullptr}; // 18px category icon (null when icon_name empty) wxCheckBox* header{nullptr}; // select-all tri-state // Material opt-in: the master checkbox carries the material title and @@ -117,6 +118,7 @@ private: wxString title; // _L("Printer") / _L("Filament") / _L("Process") Section kind{Section::Print}; // maps 1:1 to the display group std::string icon_name; // "printer" / "filament" / "process" + ScalableBitmap icon_bmp; // scalable bitmap for DPI changes wxStaticBitmap* icon{nullptr}; // 18px, like Category::icon wxCheckBox* header{nullptr}; // tri-state select-all; nullptr for the Filament group ::StaticLine* header_line{nullptr}; // Filament group's clickable title diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 4eccf0e9fa..462324ea13 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -8,6 +8,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/ProjectTask.hpp" +#include "libslic3r/PublishSettings.hpp" #include "test_utils.hpp" @@ -674,3 +675,79 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf } } } + +SCENARIO("Minimal published 3MF serialization filters config and omits embedded presets", "[3mf]") { + GIVEN("a full print configuration and published keys") { + 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(); + + DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); + full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24)); + full_cfg.set_key_value("retraction_length", new ConfigOptionFloats({ 1.2 })); + + const std::vector published_keys = { "layer_height", "retraction_length" }; + const std::vector material_keys = { + { "PLA", "Generic", "GFL99", 0, { "filament_retraction_length" } } + }; + + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + + // Filtered config must contain the published keys and identity keys + REQUIRE(filtered_cfg.option("layer_height") != nullptr); + REQUIRE(filtered_cfg.option("retraction_length") != nullptr); + REQUIRE(filtered_cfg.option("filament_colour") != nullptr); + REQUIRE(filtered_cfg.option("filament_type") != nullptr); + REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); + + // Non-published settings should NOT be in filtered_cfg + REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); + REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); + + model.model_info = std::make_shared(); + model.model_info->metadata_items["published"] = "1"; + model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])"; + + ScopedTemporaryDir backup_dir("orca_min_pub"); + model.set_backup_path(backup_dir.string()); + + WHEN("stored using SaveStrategy::MinimalPublished") { + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + + // Create a fake project preset to verify it gets omitted with MinimalPublished + Preset preset(Preset::TYPE_PRINT, "TestPrintPreset"); + preset.config = full_cfg; + std::vector project_presets = { &preset }; + + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &filtered_cfg; + store_params.project_presets = project_presets; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::MinimalPublished; + REQUIRE(store_bbs_3mf(store_params)); + + Model dst_model; + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector loaded_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, + &loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig); + THEN("the 3MF loads successfully without project embedded presets") { + REQUIRE(loaded); + REQUIRE(loaded_presets.empty()); + REQUIRE(dst_config.option("layer_height") != nullptr); + REQUIRE(dst_config.opt_float("layer_height") == 0.24); + REQUIRE(dst_config.option("sparse_infill_density") == nullptr); + } + release_PlateData_list(dst_plates); + } + } +} + From d124e1ccbc05c35d5f6d3815d46080bc2437846d Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 13:41:38 +0800 Subject: [PATCH 03/60] Update unit test --- src/libslic3r/Format/bbs_3mf.hpp | 4 +++- tests/libslic3r/test_3mf.cpp | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 0addf23385..57a40c2012 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -145,7 +145,9 @@ enum class SaveStrategy SkipAuxiliary = 1 << 9, UseLoadedId = 1 << 10, ShareMesh = 1 << 11, - MinimalPublished = 1 << 12, + // Keep this separate from SplitModel, which uses the 0x1000 bit as part of its + // production-extension value. + MinimalPublished = 1 << 13, SplitModel = 0x1000 | ProductionExt, Encrypted = SecureContentExt | SplitModel, diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 462324ea13..daf46c3834 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -730,6 +730,8 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded REQUIRE(store_bbs_3mf(store_params)); Model dst_model; + ScopedTemporaryDir loaded_backup_dir("orca_min_pub_loaded"); + dst_model.set_backup_path(loaded_backup_dir.string()); DynamicPrintConfig dst_config; ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; PlateDataPtrs dst_plates; @@ -743,11 +745,10 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded REQUIRE(loaded); REQUIRE(loaded_presets.empty()); REQUIRE(dst_config.option("layer_height") != nullptr); - REQUIRE(dst_config.opt_float("layer_height") == 0.24); + REQUIRE_THAT(dst_config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6)); REQUIRE(dst_config.option("sparse_infill_density") == nullptr); } release_PlateData_list(dst_plates); } } } - From 99db5cca38dfe040ada967e3cb10105a642b75aa Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 14:00:20 +0800 Subject: [PATCH 04/60] Renamed to Publish instead of Publish Settings --- src/slic3r/GUI/MainFrame.cpp | 10 +++------- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 11 +++++------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 5594241988..abca800277 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -39,7 +39,6 @@ #include "Plater.hpp" #include "WebViewDialog.hpp" #include "../Utils/Process.hpp" -#include "format.hpp" // BBS #include "PartPlate.hpp" #include "Preferences.hpp" @@ -49,9 +48,6 @@ #include "../Utils/NetworkAgentFactory.hpp" #include "../Utils/PrintHost.hpp" -#include -#include - #include "GUI_App.hpp" #include "UnsavedChangesDialog.hpp" #include "PublishSettingsDialog.hpp" @@ -2822,7 +2818,7 @@ void MainFrame::init_menubar_as_editor() [this](){return m_plater != nullptr && can_save_as(); }, this); #endif - // BBS: publish settings + // BBS: publish fileMenu->AppendSeparator(); auto publish_handler = [this](wxCommandEvent&) { if (!m_plater) return; @@ -2832,11 +2828,11 @@ void MainFrame::init_menubar_as_editor() }; #ifndef __APPLE__ - append_menu_item(fileMenu, wxID_ANY, _L("Publish Settings") + dots, _L("Export a 3MF file with the selected settings embedded"), + append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _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 Settings") + dots, _L("Export a 3MF file with the selected settings embedded"), + append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots, _L("Export a 3MF file with the selected settings embedded"), publish_handler, "", nullptr, [this](){return can_export_model(); }, this); #endif diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 571d7350b9..82dd38fe97 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16240,7 +16240,7 @@ int Plater::export_published_3mf(const std::vector& published_keys, if (ret < 0) { MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), - _L("Publish Settings"), wxOK | wxICON_WARNING).ShowModal(); + _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } return wxID_YES; diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 90ad2e0dc3..548c1402fe 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -98,7 +98,7 @@ struct MaterialIdentity // Menu ids for show_menu(). Dedicated range above the standard ids so the popup cannot // collide with application-level bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). enum { - kPublishSelectAll = wxID_HIGHEST + 1, + kPublishSelectAll = wxID_HIGHEST + 1, kPublishDeselectAll, kPublishSelectVisible, kPublishDeselectVisible, @@ -154,7 +154,7 @@ wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPr PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, - _L("Publish Settings"), + _L("Publish"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER) @@ -233,8 +233,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) EndModal(wxID_OK); return; } - MessageDialog(this, _L("No settings selected. Please select at least one setting to publish."), _L("Publish Settings"), - wxOK | wxICON_WARNING) + MessageDialog(this, _L("No settings selected. Please select at least one setting to publish."), _L("Publish"), wxOK | wxICON_WARNING) .ShowModal(); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); @@ -322,7 +321,7 @@ void PublishSettingsDialog::build_option_model() cat.filament_slot = slot; if (!icon_name.empty()) { cat.icon_bmp = ScalableBitmap(m_scroll, icon_name, 18); - cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, cat.icon_bmp.bmp()); + cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, cat.icon_bmp.bmp()); } if (section == Section::Material) { // Material header: [master (title)][slim tri-state select-all]. @@ -668,7 +667,7 @@ size_t PublishSettingsDialog::section_group_for(Section kind) if (!section.icon_name.empty()) { section.icon_bmp = ScalableBitmap(m_scroll, section.icon_name, 18); - section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, section.icon_bmp.bmp()); + section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, section.icon_bmp.bmp()); } section.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_section(new_index); }); From 76ec2f10c63ec1467c55320d21d2fcc3579074fb Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 16:19:46 +0800 Subject: [PATCH 05/60] Switch to a Tab layout for the Publish dialog --- src/slic3r/GUI/PublishSettingsDialog.cpp | 845 ++++++++++------------- src/slic3r/GUI/PublishSettingsDialog.hpp | 118 ++-- 2 files changed, 427 insertions(+), 536 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 548c1402fe..b67a962650 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -16,85 +16,13 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PublishSettings.hpp" -#include #include #include #include -#include - -// Custom-painted collapse chevron: a vector path (down when expanded, right -// when collapsed) drawn in the dialog's secondary-text grey. Vector drawing -// keeps it crisp at any DPI (the earlier 16px bitmap chevron looked -// blurry/wide). -class CollapseChevron : public wxWindow -{ -public: - explicit CollapseChevron(wxWindow* parent) : wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) - { - SetBackgroundColour(parent->GetBackgroundColour()); - DisableFocusFromKeyboard(); - SetMinSize(FromDIP(wxSize(10, 10))); - Bind(wxEVT_PAINT, &CollapseChevron::on_paint, this); - } - - void SetCollapsed(bool collapsed) - { - if (m_collapsed == collapsed) - return; - m_collapsed = collapsed; - Refresh(); - } - -private: - void on_paint(wxPaintEvent&) - { - wxPaintDC dc(this); - wxGraphicsContext* ctx = wxGraphicsContext::Create(dc); - if (ctx == nullptr) - return; - ctx->SetAntialiasMode(wxANTIALIAS_DEFAULT); - // Same grey as the row value labels, dark-mode aware. - wxPen pen(StateColor::darkModeColorFor(wxColour("#6B6B6B")), FromDIP(1.5), wxPENSTYLE_SOLID); - pen.SetCap(wxCAP_ROUND); - pen.SetJoin(wxJOIN_ROUND); - ctx->SetPen(pen); - - const wxSize sz = GetClientSize(); - const double cx = sz.x / 2.0; - const double cy = sz.y / 2.0; - const double r = std::min(sz.x, sz.y) * 0.32; - - wxGraphicsPath path = ctx->CreatePath(); - if (m_collapsed) { - // Right-pointing chevron ">". - path.MoveToPoint(cx - r, cy - r); - path.AddLineToPoint(cx + r, cy); - path.AddLineToPoint(cx - r, cy + r); - } else { - // Down-pointing chevron "v". - path.MoveToPoint(cx - r, cy - r); - path.AddLineToPoint(cx, cy + r); - path.AddLineToPoint(cx + r, cy - r); - } - ctx->StrokePath(path); - delete ctx; - } - - bool m_collapsed{false}; -}; namespace Slic3r { namespace GUI { namespace { -// Identity of a filament slot: the stable material id when present, else the -// type+vendor pair. Used to emit the PublishedMaterialEntry identity fields. -struct MaterialIdentity -{ - std::string type; - std::string vendor; - std::string id; -}; - // Menu ids for show_menu(). Dedicated range above the standard ids so the popup cannot // collide with application-level bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). enum { @@ -106,9 +34,9 @@ enum { kPublishFilterNonSelected }; -MaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& full) +PublishMaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& full) { - MaterialIdentity identity; + PublishMaterialIdentity identity; if (const auto* types = full.opt("filament_type")) if (slot < types->size()) identity.type = types->get_at(slot); @@ -143,7 +71,7 @@ wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPr if (preset != nullptr && !preset->name.empty()) return from_u8(material_display_name(preset->name)); } - const MaterialIdentity identity = material_identity(slot, full); + const PublishMaterialIdentity identity = material_identity(slot, full); if (!identity.type.empty()) return from_u8(identity.type); return _L("Material"); @@ -163,8 +91,6 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) { SetBackgroundColour(*wxWHITE); - build_option_model(); - // --- filter bar: search box, All/None, menu button --- wxPanel* f_bar = new wxPanel(this, wxID_ANY); f_bar->SetBackgroundColour(GetBackgroundColour()); @@ -212,6 +138,17 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) f_bar->SetSizerAndFit(f_sizer); + constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | + wxTR_FULL_ROW_HIGHLIGHT; + m_outer_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, tab_style); + m_outer_tabs->SetFont(Label::Body_14); + m_outer_tabs->SetBackgroundColour(GetBackgroundColour()); + + m_outer_host = new wxPanel(this, wxID_ANY); + m_outer_host->SetBackgroundColour(GetBackgroundColour()); + m_outer_host_sizer = new wxBoxSizer(wxVERTICAL); + m_outer_host->SetSizer(m_outer_host_sizer); + wxBoxSizer* w_sizer = new wxBoxSizer(wxVERTICAL); wxStaticText* msg = new wxStaticText(this, wxID_ANY, _L("Select which settings to embed in the 3MF file")); @@ -220,7 +157,10 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); - w_sizer->Add(m_scroll, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); + w_sizer->Add(m_outer_tabs, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); + w_sizer->Add(m_outer_host, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); + + build_option_model(); auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); @@ -261,23 +201,18 @@ void PublishSettingsDialog::build_option_model() PresetBundle* bundle = wxGetApp().preset_bundle; DynamicPrintConfig full = bundle->full_config(); - m_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); - m_scroll->SetScrollRate(0, 10); - m_scroll->SetBackgroundColour(GetBackgroundColour()); - m_list_sizer = new wxBoxSizer(wxVERTICAL); - m_scroll->SetSizer(m_list_sizer); - m_scroll->DisableFocusFromKeyboard(); - m_scroll->Bind(wxEVT_RIGHT_DOWN, &PublishSettingsDialog::show_menu, this); - - // "no matching rows" info label, shown by apply_filter(). - m_info = new wxStaticText(m_scroll, wxID_ANY, ""); - m_info->SetFont(Label::Body_13); - m_list_sizer->Add(m_info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10)); - m_info->Hide(); m_info_nonsel = _L("No selected items..."); m_info_allsel = _L("All items selected..."); m_info_empty = _L("No matching items..."); + // Keep the tab order explicit: Section's enum order is Print, Printer, + // Material, while the dialog presents Printer, Filament, Process. + m_sections.reserve(3); + const Section tab_order[] = {Section::Printer, Section::Material, Section::Print}; + for (Section kind : tab_order) + section_group_for(kind); + bind_tab_events(); + // Shared per-option label/value computation; returns false when the option // must be skipped (denylisted / unknown / empty label). value is the pure // stringified value; unit is the translated sidetext (may be empty). @@ -296,165 +231,12 @@ void PublishSettingsDialog::build_option_model() return true; }; - // Find-or-create a main category; builds its header row UI on first use. - // Material sections are additionally matched by identity and slot so two - // identities (or slots) that happen to share a title stay separate. - auto category_index_for = [this, &full](const wxString& title, Section section, const std::string& icon_name, size_t group, - const MaterialIdentity& identity = MaterialIdentity(), size_t slot = 0) -> size_t { - for (size_t i = 0; i < m_categories.size(); ++i) { - if (m_categories[i].title != title || m_categories[i].section != section) - continue; - if (section == Section::Material && - (m_categories[i].filament_id != identity.id || m_categories[i].filament_type != identity.type || - m_categories[i].filament_vendor != identity.vendor || m_categories[i].filament_slot != slot)) - continue; - return i; - } - Category cat; - cat.title = title; - cat.section = section; - cat.group = group; - cat.icon_name = icon_name; - cat.filament_type = identity.type; - cat.filament_vendor = identity.vendor; - cat.filament_id = identity.id; - cat.filament_slot = slot; - if (!icon_name.empty()) { - cat.icon_bmp = ScalableBitmap(m_scroll, icon_name, 18); - cat.icon = new wxStaticBitmap(m_scroll, wxID_ANY, cat.icon_bmp.bmp()); - } - if (section == Section::Material) { - // Material header: [master (title)][slim tri-state select-all]. - // The master is a 2-state opt-in that carries the material title; - // the tri-state is label-less and gates on the master. - cat.master_check = new wxCheckBox(m_scroll, wxID_ANY, title); - cat.master_check->SetFont(Label::Head_14); - cat.master_check->SetToolTip(_L("Export this material")); - cat.header = new wxCheckBox(m_scroll, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); - cat.header->SetFont(Label::Head_14); - cat.header->SetToolTip(_L("Select/deselect all settings in this material")); - } else { - cat.header = new wxCheckBox(m_scroll, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); - cat.header->SetFont(Label::Head_14.Bold()); - } - const size_t new_index = m_categories.size(); - cat.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_category(new_index); }); - // [icon][chevron][(chip)(master)(tri-state) | checkbox]: the chevron - // collapses/expands the category, the checkbox is the select-all. - auto header_sizer = new wxBoxSizer(wxHORIZONTAL); - if (cat.icon != nullptr) - header_sizer->Add(cat.icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); - header_sizer->Add(cat.chevron, 0, wxALIGN_CENTER_VERTICAL); - if (section == Section::Material) { - // Per-slot colour chip, before the master title. - std::string hex; - if (const auto* colours = full.opt("filament_colour")) - if (slot < colours->size()) - hex = colours->get_at(slot); - wxBitmap* chip_bmp = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12)); - // chip_bmp points into get_extruder_color_icon's static BitmapCache - // and must NOT be deleted; the wxStaticBitmap takes its own copy. - header_sizer->Add(new wxStaticBitmap(m_scroll, wxID_ANY, *chip_bmp), 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - header_sizer->Add(cat.master_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - header_sizer->Add(cat.header, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6)); - } else { - header_sizer->Add(cat.header, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(4)); - } - // A wrapper sizer splits the vertical separation (TOP 10) from the - // horizontal indent (LEFT|RIGHT 22), so the top gap collapses with the - // header when it is hidden. - auto wrap = new wxBoxSizer(wxVERTICAL); - wrap->Add(header_sizer, 0, wxTOP, FromDIP(10)); - cat.item = m_list_sizer->Add(wrap, 0, wxLEFT | wxRIGHT, FromDIP(22)); - // Register the new category with its section group: the group's visibility - // and select-all logic iterates section.categories. - m_sections[group].categories.push_back(new_index); - m_categories.push_back(std::move(cat)); - return new_index; - }; - - // Find-or-create a subcategory (optgroup) heading within a category. - auto subcategory_index_for = [this](size_t cat_index, const wxString& title, const wxString& icon) -> size_t { - Category& cat = m_categories[cat_index]; - for (size_t i = 0; i < cat.subs.size(); ++i) - if (cat.subs[i].title == title) - return i; - Subcategory sub; - sub.title = title; - if (!title.IsEmpty()) { - // Same look as the Tab's optgroup headers (incl. its icon), plus a - // collapse chevron. A click on the chevron bitmap does not reach the - // StaticLine, so both are bound to the same toggle (LEFT_UP so the - // StaticLine's label acts as the click target too). - sub.header = new ::StaticLine(m_scroll, false, title, icon); - sub.header->SetFont(Label::Head_14); - sub.header->SetForegroundColour("#363636"); - sub.header->SetCursor(wxCURSOR_HAND); - const size_t new_index = cat.subs.size(); - auto toggle = [this, cat_index, new_index] { toggle_subcategory(cat_index, new_index); }; - sub.header->Bind(wxEVT_LEFT_UP, [toggle](wxMouseEvent&) { toggle(); }); - sub.chevron = create_chevron(m_scroll, wxEVT_LEFT_UP, toggle); - auto header_sizer = new wxBoxSizer(wxHORIZONTAL); - header_sizer->Add(sub.chevron, 0, wxALIGN_CENTER_VERTICAL); - header_sizer->Add(sub.header, 1, wxEXPAND | wxLEFT, FromDIP(4)); - // A wrapper sizer splits the vertical separation (TOP|BOTTOM 6) from - // the horizontal indent (LEFT|RIGHT 38), so the gaps collapse with - // the header when it is hidden. - auto wrap = new wxBoxSizer(wxVERTICAL); - wrap->Add(header_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6)); - sub.item = m_list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38)); - } - cat.subs.push_back(std::move(sub)); - return cat.subs.size() - 1; - }; - - // Creates the row UI (checkbox + white ellipsized value + grey unit) and - // registers the row into the given category/subcategory. - auto add_row_ui = [this](const std::string& key, const wxString& label, const wxString& value, const wxString& unit, size_t cat_index, - size_t sub_index) { - Row row; - row.key = key; - row.label = label; - row.value = value; - row.unit = unit; - row.category = m_categories[cat_index].title; - row.subcategory = m_categories[cat_index].subs[sub_index].title; - row.section = m_categories[cat_index].section; - row.section_title = m_sections[m_categories[cat_index].group].title; - const size_t row_index = m_rows.size(); - m_rows.push_back(std::move(row)); - - Row& r = m_rows[row_index]; - r.check = new wxCheckBox(m_scroll, wxID_ANY, label, wxDefaultPosition, wxDefaultSize); - r.check->SetFont(Label::Body_13); - // Value in the Tab's text color (near-black light / #EFEFF0 dark); the - // unit is the grey secondary text. - r.value_label = new wxStaticText(m_scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); - r.value_label->SetFont(Label::Body_13); - r.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - r.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit); - if (!unit.IsEmpty()) { - r.unit_label = new wxStaticText(m_scroll, wxID_ANY, unit); - r.unit_label->SetFont(Label::Body_13); - r.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); - } - - auto row_sizer = new wxBoxSizer(wxHORIZONTAL); - row_sizer->Add(r.check, 0, wxALIGN_CENTER_VERTICAL); - row_sizer->Add(r.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); - if (r.unit_label != nullptr) - row_sizer->Add(r.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - r.item = m_list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(54)); - - m_categories[cat_index].rows.push_back(row_index); - m_categories[cat_index].subs[sub_index].rows.push_back(row_index); - }; - // --- Phase 1: printer per-extruder retraction settings (displayed first, // mirroring the sidebar's Printer group). The printer tab's // "Extruder"/"Extruder N" pages carry the per-extruder retraction options. { size_t g = section_group_for(Section::Printer); + category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); for (Tab* tab : wxGetApp().tabs_list) { if (tab->m_type != Preset::TYPE_PRINTER) continue; @@ -467,7 +249,7 @@ void PublishSettingsDialog::build_option_model() // when switching material" group is intentionally skipped. if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") continue; - const wxString subcategory = page_title + L" \u00B7 " + _(optgroup->title); + const wxString subcategory = _(optgroup->title); for (const auto& opt : optgroup->opt_map()) { const std::string& opt_id = opt.first; const std::string& pure_key = opt.second.first; @@ -480,7 +262,7 @@ void PublishSettingsDialog::build_option_model() wxString label, value, unit; if (!option_text(opt_id, pure_key, label, value, unit)) continue; - size_t cat_index = category_index_for(_L("Retraction & Z-hop"), Section::Printer, "custom-gcode_extruder", g); + size_t cat_index = category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon); add_row_ui(pure_key, label, value, unit, cat_index, sub_index); } @@ -509,13 +291,12 @@ void PublishSettingsDialog::build_option_model() if (overrides_page != nullptr) { // One section per filament slot: a 4-slot printer (e.g. 1 PLA + - // 3 PETG) shows 4 sections, each disambiguated by its colour chip - // and slot number. The "· Slot N" title suffix keeps the category - // titles unique across slots. + // 3 PETG) shows 4 separate pages, each disambiguated internally by + // its colour chip and slot identity while displaying the bare name. for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { - const MaterialIdentity identity = material_identity(slot, full); - const wxString title = material_title(slot, bundle, full) + L" \u00B7 " + - wxString::Format(_L("Slot %d"), static_cast(slot) + 1); + 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); // A material section must not repeat a key; the same key may // appear in other material sections - that is intended. std::set material_added; @@ -541,9 +322,8 @@ void PublishSettingsDialog::build_option_model() wxString label, value, unit; if (!option_text(value_opt_id, base, label, value, unit)) continue; - size_t cat_index = category_index_for(title, Section::Material, "custom-gcode_filament", g, identity, slot); - size_t sub_index = subcategory_index_for(cat_index, _(optgroup->title), optgroup->icon); - add_row_ui(base, label, value, unit, cat_index, sub_index); + size_t sub_index = subcategory_index_for(category_index, _(optgroup->title), optgroup->icon); + add_row_ui(base, label, value, unit, category_index, sub_index); } } } @@ -558,6 +338,7 @@ void PublishSettingsDialog::build_option_model() if (tab->m_type != Preset::TYPE_PRINT) continue; const auto& icon_map = tab->get_category_icon_map(); + size_t page_index = 0; for (const PageShp& page : tab->m_pages) { wxString category = Tab::translate_category(page->title(), tab->m_type); // Page icon, keyed by the untranslated page title (per-Tab map). @@ -565,6 +346,7 @@ void PublishSettingsDialog::build_option_model() auto icon_it = icon_map.find(page->title()); if (icon_it != icon_map.end()) icon_name = icon_it->second; + const size_t category_index = category_index_for(category, Section::Print, icon_name, g, page_index); for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { for (const auto& opt : optgroup->opt_map()) { @@ -578,11 +360,11 @@ void PublishSettingsDialog::build_option_model() wxString label, value, unit; if (!option_text(opt_id, pure_key, label, value, unit)) continue; - size_t cat_index = category_index_for(category, Section::Print, icon_name, g); - size_t sub_index = subcategory_index_for(cat_index, _(optgroup->title), optgroup->icon); - add_row_ui(opt_id, label, value, unit, cat_index, sub_index); + size_t sub_index = subcategory_index_for(category_index, _(optgroup->title), optgroup->icon); + add_row_ui(opt_id, label, value, unit, category_index, sub_index); } } + ++page_index; } } } @@ -604,28 +386,20 @@ void PublishSettingsDialog::build_option_model() } } - // Wire the section group tri-state headers (chevrons/StaticLine toggles were - // bound at creation in section_group_for). - for (size_t s = 0; s < m_sections.size(); ++s) { - if (m_sections[s].header != nullptr) { - m_sections[s].header->Bind(wxEVT_CHECKBOX, [this, s](wxCommandEvent&) { on_section_toggle(s); }); - update_section_header(m_sections[s]); - } - } - - // Wire the tri-state headers: clicking a header toggles all its children; + // 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); }); - m_categories[c].header->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_category_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, c](wxCommandEvent&) { update_category_header(m_categories[c]); }); + m_rows[r].check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { update_all_headers(); }); update_category_header(m_categories[c]); } - // Material sections start gated (master OFF): their rows and tri-state are + // Material pages start gated (master OFF): their rows and tri-state are // disabled until the author opts the material in. for (size_t c = 0; c < m_categories.size(); ++c) if (m_categories[c].section == Section::Material) @@ -636,8 +410,17 @@ void PublishSettingsDialog::build_option_model() row.matches_filter = true; apply_visibility(); - m_scroll->FitInside(); - m_list_sizer->Layout(); + for (Category& category : m_categories) { + category.scroll->FitInside(); + category.list_sizer->Layout(); + } + for (SectionGroup& section : m_sections) + if (!section.categories.empty()) + section.tabs->SelectItem(0); + if (!m_sections.empty()) { + m_outer_tabs->SelectItem(0); + show_outer_page(0); + } } size_t PublishSettingsDialog::section_group_for(Section kind) @@ -649,7 +432,6 @@ size_t PublishSettingsDialog::section_group_for(Section kind) SectionGroup section; section.kind = kind; const size_t new_index = m_sections.size(); - switch (kind) { case Section::Printer: section.title = _L("Printer"); @@ -665,47 +447,166 @@ size_t PublishSettingsDialog::section_group_for(Section kind) break; } - if (!section.icon_name.empty()) { - section.icon_bmp = ScalableBitmap(m_scroll, section.icon_name, 18); - section.icon = new wxStaticBitmap(m_scroll, wxID_ANY, section.icon_bmp.bmp()); - } + constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | + wxTR_FULL_ROW_HIGHLIGHT; + section.page = new wxPanel(m_outer_host, wxID_ANY); + section.page->SetBackgroundColour(GetBackgroundColour()); + auto* page_sizer = new wxBoxSizer(wxVERTICAL); + section.tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, tab_style); + section.tabs->SetFont(Label::Body_14); + section.tabs->SetBackgroundColour(GetBackgroundColour()); + page_sizer->Add(section.tabs, 0, wxEXPAND); + section.page_host = new wxPanel(section.page, wxID_ANY); + section.page_host->SetBackgroundColour(GetBackgroundColour()); + section.page_host_sizer = new wxBoxSizer(wxVERTICAL); + section.page_host->SetSizer(section.page_host_sizer); + page_sizer->Add(section.page_host, 1, wxEXPAND | wxTOP, FromDIP(4)); + section.page->SetSizer(page_sizer); - section.chevron = create_chevron(m_scroll, wxEVT_LEFT_DOWN, [this, new_index] { toggle_section(new_index); }); - - auto header_sizer = new wxBoxSizer(wxHORIZONTAL); - if (section.icon != nullptr) - header_sizer->Add(section.icon, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(2)); - header_sizer->Add(section.chevron, 0, wxALIGN_CENTER_VERTICAL); - - if (kind == Section::Material) { - // Filament group: clickable StaticLine title, no tri-state (each - // material below opts in individually). - section.header_line = new ::StaticLine(m_scroll, false, section.title); - section.header_line->SetFont(Label::Head_14.Bold()); - section.header_line->SetForegroundColour("#363636"); - section.header_line->SetCursor(wxCURSOR_HAND); - section.header_line->SetToolTip(_L("Enable each material below to export its settings")); - auto toggle = [this, new_index] { toggle_section(new_index); }; - section.header_line->Bind(wxEVT_LEFT_UP, [toggle](wxMouseEvent&) { toggle(); }); - header_sizer->Add(section.header_line, 1, wxEXPAND | wxLEFT, FromDIP(4)); - } else { - // Printer/Process: tri-state select-all carries the title. - section.header = new wxCheckBox(m_scroll, wxID_ANY, section.title, wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); - section.header->SetFont(Label::Head_14.Bold()); - header_sizer->Add(section.header, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(4)); - } - - // A wrapper sizer splits the larger vertical separation (TOP 14) from the - // shallow horizontal indent (LEFT|RIGHT 6), so the top gap collapses with - // the header when it is hidden. wxEXPAND lets the Filament StaticLine's - // separator span the width like the subcategory headers. - auto wrap = new wxBoxSizer(wxVERTICAL); - wrap->Add(header_sizer, 0, wxEXPAND | wxTOP, FromDIP(14)); - section.item = m_list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(6)); + m_outer_tabs->AppendItem(section.title); + m_outer_host_sizer->Add(section.page, 1, wxEXPAND); + section.page->Hide(); m_sections.push_back(std::move(section)); return new_index; } +size_t PublishSettingsDialog::category_index_for(const wxString& title, + Section section, + const std::string& icon_name, + size_t group, + size_t source_index, + const PublishMaterialIdentity& identity) +{ + for (size_t i : m_sections[group].categories) { + Category& existing = m_categories[i]; + if (existing.title == title && existing.section == section && existing.source_index == source_index && + existing.filament_id == identity.id && existing.filament_type == identity.type && existing.filament_vendor == identity.vendor) + return i; + } + + Category category; + category.title = title; + category.section = section; + category.group = group; + category.source_index = source_index; + category.icon_name = icon_name; + category.filament_type = identity.type; + category.filament_vendor = identity.vendor; + category.filament_id = identity.id; + category.filament_slot = source_index; + category.page = new wxPanel(m_sections[group].page_host, wxID_ANY); + category.page->SetBackgroundColour(GetBackgroundColour()); + auto* page_sizer = new wxBoxSizer(wxVERTICAL); + + if (section == Section::Material) { + auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); + std::string hex; + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + if (const auto* colours = full.opt("filament_colour")) + if (source_index < colours->size()) + hex = colours->get_at(source_index); + if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) + header_sizer->Add(new wxStaticBitmap(category.page, wxID_ANY, *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)); + page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); + } + + category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); + category.scroll->SetScrollRate(0, 10); + category.scroll->SetBackgroundColour(GetBackgroundColour()); + category.list_sizer = new wxBoxSizer(wxVERTICAL); + category.scroll->SetSizer(category.list_sizer); + category.scroll->DisableFocusFromKeyboard(); + category.scroll->Bind(wxEVT_RIGHT_DOWN, &PublishSettingsDialog::show_menu, this); + category.info = new wxStaticText(category.scroll, wxID_ANY, m_info_empty); + category.info->SetFont(Label::Body_13); + category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10)); + category.info->Hide(); + page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4)); + category.page->SetSizer(page_sizer); + category.page->Hide(); + + const size_t category_index = m_categories.size(); + m_categories.push_back(std::move(category)); + m_sections[group].categories.push_back(category_index); + m_sections[group].tabs->AppendItem(title); + m_sections[group].page_host_sizer->Add(m_categories[category_index].page, 1, wxEXPAND); + if (m_sections[group].selected_inner < 0) + m_sections[group].selected_inner = 0; + return category_index; +} + +size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon) +{ + Category& category = m_categories[category_index]; + for (size_t i = 0; i < category.subs.size(); ++i) + if (category.subs[i].title == title) + return i; + + Subcategory sub; + sub.title = title; + if (!title.IsEmpty()) { + sub.header = new ::StaticLine(category.scroll, false, title, icon); + sub.header->SetFont(Label::Head_14); + sub.header->SetForegroundColour("#363636"); + auto* wrap = new wxBoxSizer(wxVERTICAL); + wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6)); + sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22)); + } + category.subs.push_back(std::move(sub)); + return category.subs.size() - 1; +} + +void PublishSettingsDialog::add_row_ui(const std::string& key, + const wxString& label, + const wxString& value, + const wxString& unit, + size_t category_index, + size_t subcategory_index) +{ + Category& category = m_categories[category_index]; + Row row; + row.key = key; + row.label = label; + row.value = value; + row.unit = unit; + row.category = category.title; + row.subcategory = category.subs[subcategory_index].title; + row.section = category.section; + row.section_title = m_sections[category.group].title; + row.outer_index = category.group; + row.inner_index = category_index; + row.subcategory_index = subcategory_index; + const size_t row_index = m_rows.size(); + m_rows.push_back(std::move(row)); + Row& current = m_rows[row_index]; + current.check = new wxCheckBox(category.scroll, wxID_ANY, label); + current.check->SetFont(Label::Body_13); + 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 (!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) { Category& cat = m_categories[category_index]; @@ -723,7 +624,7 @@ void PublishSettingsDialog::on_category_toggle(size_t category_index) bool value = !all_checked; for (size_t r : cat.rows) m_rows[r].check->SetValue(value); - update_category_header(cat); + update_all_headers(); } void PublishSettingsDialog::on_master_toggle(size_t category_index) @@ -733,60 +634,19 @@ void PublishSettingsDialog::on_master_toggle(size_t category_index) for (size_t r : cat.rows) m_rows[r].check->Enable(cat.master); cat.header->Enable(cat.master); - update_category_header(cat); + update_all_headers(); } -void PublishSettingsDialog::on_section_toggle(size_t section_index) +void PublishSettingsDialog::update_all_headers() { - SectionGroup& section = m_sections[section_index]; - if (section.header == nullptr) - return; // defensive: the Filament group has no select-all - // All-or-none over every enabled row in the group's categories. - bool all_checked = true; - for (size_t c : section.categories) { - for (size_t r : m_categories[c].rows) - if (m_rows[r].check->IsEnabled() && !m_rows[r].check->GetValue()) { - all_checked = false; - break; - } - if (!all_checked) - break; - } - const bool value = !all_checked; - for (size_t c : section.categories) - for (size_t r : m_categories[c].rows) - if (m_rows[r].check->IsEnabled()) - m_rows[r].check->SetValue(value); - for (size_t c : section.categories) - update_category_header(m_categories[c]); - update_section_header(section); -} - -void PublishSettingsDialog::update_section_header(SectionGroup& section) -{ - if (section.header == nullptr) - return; // Filament group has no tri-state. - int checked = 0; - int total = 0; - for (size_t c : section.categories) { - for (size_t r : m_categories[c].rows) { - if (!m_rows[r].check->IsEnabled()) - continue; // gated material rows don't count (defensive) - ++total; - if (m_rows[r].check->GetValue()) - ++checked; - } - } - if (total == 0 || checked == 0) - section.header->Set3StateValue(wxCHK_UNCHECKED); - else if (checked == total) - section.header->Set3StateValue(wxCHK_CHECKED); - else - section.header->Set3StateValue(wxCHK_UNDETERMINED); + 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) { @@ -813,6 +673,71 @@ void PublishSettingsDialog::set_row_bold(Row& row, bool bold) row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13); } +void PublishSettingsDialog::save_scroll_position(Category& category) +{ + if (category.scroll != nullptr) + category.scroll->GetViewStart(&category.scroll_pos.x, &category.scroll_pos.y); +} + +void PublishSettingsDialog::show_outer_page(size_t section_index) +{ + if (section_index >= m_sections.size()) + return; + if (m_selected_outer >= 0 && m_selected_outer < static_cast(m_sections.size())) { + SectionGroup& old_section = m_sections[m_selected_outer]; + if (old_section.selected_inner >= 0 && old_section.selected_inner < static_cast(old_section.categories.size())) + save_scroll_position(m_categories[old_section.categories[old_section.selected_inner]]); + m_sections[m_selected_outer].page->Hide(); + } + m_selected_outer = static_cast(section_index); + SectionGroup& section = m_sections[section_index]; + section.page->Show(); + if (section.selected_inner >= 0) + show_inner_page(section_index, section.selected_inner); + m_outer_host_sizer->Layout(); +} + +void PublishSettingsDialog::show_inner_page(size_t section_index, int inner_index) +{ + if (section_index >= m_sections.size()) + return; + SectionGroup& section = m_sections[section_index]; + if (inner_index < 0 || inner_index >= static_cast(section.categories.size())) + return; + if (section.selected_inner >= 0 && section.selected_inner < static_cast(section.categories.size())) { + save_scroll_position(m_categories[section.categories[section.selected_inner]]); + m_categories[section.categories[section.selected_inner]].page->Hide(); + } + section.selected_inner = inner_index; + Category& category = m_categories[section.categories[inner_index]]; + category.page->Show(); + category.scroll->FitInside(); + category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y); + section.page_host_sizer->Layout(); +} + +void PublishSettingsDialog::on_outer_tab_changed(wxCommandEvent& event) +{ + const int selection = event.GetInt(); + if (selection >= 0 && selection < static_cast(m_sections.size())) + show_outer_page(static_cast(selection)); +} + +void PublishSettingsDialog::on_inner_tab_changed(size_t section_index, wxCommandEvent& event) +{ + const int selection = event.GetInt(); + if (section_index < m_sections.size() && selection >= 0 && selection < static_cast(m_sections[section_index].categories.size())) + show_inner_page(section_index, selection); +} + +void PublishSettingsDialog::bind_tab_events() +{ + m_outer_tabs->Bind(wxEVT_TAB_SEL_CHANGED, &PublishSettingsDialog::on_outer_tab_changed, this); + for (size_t section_index = 0; section_index < m_sections.size(); ++section_index) + m_sections[section_index].tabs->Bind(wxEVT_TAB_SEL_CHANGED, + [this, section_index](wxCommandEvent& event) { on_inner_tab_changed(section_index, event); }); +} + void PublishSettingsDialog::apply_filter(const wxString& filter_text) { Freeze(); @@ -823,11 +748,10 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text) const bool pseudo = (filter == "::sel" || filter == "::nonsel"); m_fb_sizer->Show(!pseudo); - // Update the per-row match flags only; actual visibility is computed by - // apply_visibility() (which also respects the collapse state). + // Update row matches first; page and optgroup visibility is applied below. if (pseudo) { if (m_filter_ctrl->GetValue().Lower() != filter) { - m_filter_ctrl->SetValue(filter); + m_filter_ctrl->ChangeValue(filter); m_filter_ctrl->SetSelection(0, -1); } const bool want_checked = (filter == "::sel"); @@ -843,20 +767,43 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text) } } - // The info label reflects the filter result only; a collapsed section - // hiding its matches is a user choice, not "no match". - m_info->Show(); - for (const Row& row : m_rows) { - if (row.matches_filter) { - m_info->Hide(); - break; + size_t first_outer = 0; + int first_inner = -1; + bool active_has_match = false; + for (size_t s = 0; s < m_sections.size(); ++s) { + for (size_t inner = 0; inner < m_sections[s].categories.size(); ++inner) { + Category& category = m_categories[m_sections[s].categories[inner]]; + bool has_match = false; + for (size_t r : category.rows) + has_match = has_match || m_rows[r].matches_filter; + category.info->Show(!has_match); + if (!has_match) + category.info->SetLabel(pseudo ? (filter == "::sel" ? m_info_nonsel : m_info_allsel) : m_info_empty); + if (has_match && first_inner < 0) { + first_outer = s; + first_inner = static_cast(inner); + } + if (static_cast(s) == m_selected_outer && static_cast(inner) == m_sections[s].selected_inner) + active_has_match = has_match; } } - if (m_info->IsShown()) - m_info->SetLabel(pseudo ? (filter == "::sel" ? m_info_nonsel : m_info_allsel) : m_info_empty); + if (!active_has_match && first_inner >= 0 && + (m_selected_outer != static_cast(first_outer) || m_sections[first_outer].selected_inner != first_inner)) { + if (m_selected_outer != static_cast(first_outer)) { + m_outer_tabs->SelectItem(static_cast(first_outer)); + show_outer_page(first_outer); + } + m_sections[first_outer].tabs->SelectItem(first_inner); + show_inner_page(first_outer, first_inner); + } apply_visibility(); - m_scroll->FitInside(); + for (Category& category : m_categories) { + save_scroll_position(category); + category.scroll->FitInside(); + category.list_sizer->Layout(); + category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y); + } Layout(); Thaw(); } @@ -864,95 +811,26 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text) void PublishSettingsDialog::apply_visibility() { Freeze(); - for (SectionGroup& section : m_sections) { - // The section header stays visible whenever any row in the group matches - // (a section header never depends on its own collapsed state). - bool section_any = false; - for (size_t c : section.categories) { - for (size_t r : m_categories[c].rows) - if (m_rows[r].matches_filter) { - section_any = true; - break; - } - if (section_any) - break; - } - section.item->Show(section_any); - section.chevron->SetCollapsed(section.collapsed); - - for (size_t c : section.categories) { - Category& cat = m_categories[c]; - - // The category header stays visible whenever it has any match and - // the section is not collapsed, so it can always be re-expanded. - bool cat_any = false; - for (size_t r : cat.rows) - if (m_rows[r].matches_filter) { - cat_any = true; - break; - } - cat.item->Show(cat_any && !section.collapsed); - cat.chevron->SetCollapsed(cat.collapsed); - - for (Subcategory& sub : cat.subs) { - if (sub.header != nullptr) { - // The subcategory header visibility depends on its rows' matches - // and on its ancestors, but NOT on its own collapsed state. - bool sub_any = false; - for (size_t r : sub.rows) - if (m_rows[r].matches_filter) { - sub_any = true; - break; - } - sub.item->Show(sub_any && !section.collapsed && !cat.collapsed); - sub.chevron->SetCollapsed(sub.collapsed); - } - // Rows are hidden by the filter and by any collapsed ancestor. - for (size_t r : sub.rows) - m_rows[r].item->Show(m_rows[r].matches_filter && !section.collapsed && !cat.collapsed && !sub.collapsed); - } + for (Category& category : m_categories) { + bool category_any = false; + for (size_t r : category.rows) + category_any = category_any || m_rows[r].matches_filter; + category.info->Show(!category_any); + for (Subcategory& sub : category.subs) { + bool sub_any = false; + for (size_t r : sub.rows) + sub_any = sub_any || m_rows[r].matches_filter; + if (sub.header != nullptr) + sub.item->Show(sub_any); + for (size_t r : sub.rows) + m_rows[r].item->Show(m_rows[r].matches_filter); } + category.list_sizer->Layout(); + category.scroll->FitInside(); } Thaw(); } -void PublishSettingsDialog::toggle_section(size_t section_index) -{ - m_sections[section_index].collapsed = !m_sections[section_index].collapsed; - apply_visibility(); - m_scroll->FitInside(); - m_list_sizer->Layout(); -} - -void PublishSettingsDialog::toggle_category(size_t category_index) -{ - m_categories[category_index].collapsed = !m_categories[category_index].collapsed; - apply_visibility(); - m_scroll->FitInside(); - m_list_sizer->Layout(); -} - -void PublishSettingsDialog::toggle_subcategory(size_t category_index, size_t subcategory_index) -{ - m_categories[category_index].subs[subcategory_index].collapsed = !m_categories[category_index].subs[subcategory_index].collapsed; - apply_visibility(); - m_scroll->FitInside(); - m_list_sizer->Layout(); -} - -CollapseChevron* PublishSettingsDialog::create_chevron(wxWindow* parent, - const wxEventTypeTag& event_type, - std::function toggle) -{ - CollapseChevron* chevron = new CollapseChevron(parent); - chevron->SetCursor(wxCURSOR_HAND); - // The tag type (not wxEventType) keeps Bind's EventTag template deduced as - // wxEventTypeTag; wxEvent& is used so the helper works with - // any mouse event tag, and the toggle itself does not inspect the event. - chevron->Bind(event_type, [toggle](wxEvent&) { toggle(); }); - return chevron; -} - void PublishSettingsDialog::select_all(bool value) { // "All" does not auto-enable gated material sections; "None" leaves a gated @@ -960,8 +838,18 @@ void PublishSettingsDialog::select_all(bool value) for (Row& row : m_rows) if (row.check->IsEnabled()) row.check->SetValue(value); - for (Category& cat : m_categories) - update_category_header(cat); + update_all_headers(); +} + +bool PublishSettingsDialog::row_is_visible(const Row& row) const +{ + if (m_selected_outer < 0 || m_selected_outer >= static_cast(m_sections.size()) || + row.outer_index != static_cast(m_selected_outer) || m_sections[m_selected_outer].selected_inner < 0 || + m_sections[m_selected_outer].selected_inner >= static_cast(m_sections[m_selected_outer].categories.size()) || + row.inner_index != m_sections[m_selected_outer].categories[m_sections[m_selected_outer].selected_inner] || !row.matches_filter || + !row.check->IsEnabled()) + return false; + return row.item->IsShown(); } void PublishSettingsDialog::select_visible(bool value) @@ -973,24 +861,30 @@ void PublishSettingsDialog::select_visible(bool value) // Toggle the rows that are visible under the *current* filter. for (Row& row : m_rows) - if (row.check->IsShown() && row.check->IsEnabled()) + if (row_is_visible(row)) row.check->SetValue(value); if (clear_pseudo) { // Note: SetValue() may fire wxEVT_TEXT on some platforms, which // re-enters apply_filter() - that is fine, the rows above were already // toggled and the trailing call below is idempotent. - m_filter_ctrl->SetValue(""); + m_filter_ctrl->ChangeValue(""); apply_filter(""); // resync visibility, headers and the All/None bar } - for (Category& cat : m_categories) - update_category_header(cat); + update_all_headers(); } void PublishSettingsDialog::show_menu(wxMouseEvent& evt) { bool filtering = !m_filter_ctrl->GetValue().IsEmpty(); - bool list_empty = m_info->IsShown(); + bool list_empty = true; + if (m_selected_outer >= 0) { + for (const Row& row : m_rows) + if (row_is_visible(row)) { + list_empty = false; + break; + } + } wxMenu m; m.Append(kPublishSelectAll, _L("Select All"))->Enable(!filtering); @@ -1066,30 +960,25 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) m_menu.msw_rescale(); m_filter_box->SetIcon(m_search.bmp()); m_menu_button->SetBitmap(m_menu.bmp()); - - for (SectionGroup& section : m_sections) { - if (section.icon != nullptr && section.icon_bmp.bmp().IsOk()) { - section.icon_bmp.msw_rescale(); - section.icon->SetBitmap(section.icon_bmp.bmp()); - } - if (section.header_line != nullptr) - section.header_line->Rescale(); - } + m_outer_tabs->Rescale(); for (Category& cat : m_categories) { if (cat.icon != nullptr && cat.icon_bmp.bmp().IsOk()) { cat.icon_bmp.msw_rescale(); cat.icon->SetBitmap(cat.icon_bmp.bmp()); } - for (Subcategory& sub : cat.subs) { - if (sub.header != nullptr) - sub.header->Rescale(); - } + if (cat.header != nullptr) + cat.header->Refresh(); + if (cat.master_check != nullptr) + cat.master_check->Refresh(); + cat.scroll->FitInside(); + cat.list_sizer->Layout(); } + for (SectionGroup& section : m_sections) + section.tabs->Rescale(); + SetMinSize(FromDIP(wxSize(600, 500))); - m_scroll->FitInside(); - m_list_sizer->Layout(); Refresh(); } diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 4767b9a29e..0bae39daa4 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -3,6 +3,7 @@ #include "GUI_Utils.hpp" #include "wxExtensions.hpp" +#include "Widgets/TabCtrl.hpp" #include "libslic3r/PublishSettings.hpp" @@ -13,21 +14,24 @@ #include #include -// Forward declarations (all are global classes, see Widgets/TextInput.hpp, -// Widgets/StaticLine.hpp and the CollapseChevron definition in the .cpp). +// Forward declarations (all are global classes, see Widgets/TextInput.hpp and +// Widgets/StaticLine.hpp). class TextInput; class StaticLine; -class CollapseChevron; namespace Slic3r { namespace GUI { +struct PublishMaterialIdentity +{ + std::string type; + std::string vendor; + std::string id; +}; + // Dialog that lets a model author select which settings get embedded in a 3MF. -// Settings are grouped the way the tabs show them: the process (print) pages, -// the printer's per-extruder retraction settings, and one section per material -// used in the project (filament overrides). Each main category has a select-all -// tri-state header, subcategory (optgroup) headings, one row per setting -// (checkbox + grey value label), and both header levels are collapsible (chevron -// toggle). A search filter and an All/None / select-visible menu are provided. +// Settings are grouped into the same nested custom tab layout used by the +// Process settings: Printer, Filament, and Process outer tabs, with category or +// material tabs inside each section. Optgroups are ordinary grouped headers. // Modified (dirty) settings are pre-checked and shown bold. On OK, the print // rows become the "published_keys" list and the material rows become the // per-material "published_material_keys". @@ -63,44 +67,49 @@ private: wxString label; wxString value; wxString unit; - wxString section_title; // top-level group title, for filter matching + wxString section_title; // outer tab title, for filter matching Section section{Section::Print}; + size_t outer_index{0}; + size_t inner_index{0}; + size_t subcategory_index{0}; bool dirty{false}; // matches a dirty base key: pre-checked + bold bool matches_filter{false}; // survives the active filter (computed by apply_filter) wxCheckBox* check{nullptr}; wxStaticText* value_label{nullptr}; wxStaticText* unit_label{nullptr}; - wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in m_list_sizer + wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer }; - // A subcategory (optgroup) heading. Rows store indices into m_rows. + // An optgroup heading. Rows store indices into m_rows. struct Subcategory { wxString title; ::StaticLine* header{nullptr}; // null when the title is empty - bool collapsed{false}; - CollapseChevron* chevron{nullptr}; - wxSizerItem* item{nullptr}; // sizer item of the header h-sizer in m_list_sizer + wxSizerItem* item{nullptr}; std::vector rows; }; - // A main category with its select-all tri-state header. + // An inner TabCtrl page with its select-all/material controls and content. struct Category { wxString title; Section section{Section::Print}; - size_t group{0}; // index into m_sections - std::string icon_name; // bitmap name; empty = no icon + size_t group{0}; // index into m_sections / outer page + size_t source_index{0}; // stable source page or material slot index + std::string source_title; + std::string icon_name; + wxPanel* page{nullptr}; + wxScrolledWindow* scroll{nullptr}; + wxBoxSizer* list_sizer{nullptr}; + wxStaticText* info{nullptr}; + wxPoint scroll_pos{0, 0}; ScalableBitmap icon_bmp; // scalable bitmap for DPI changes - wxStaticBitmap* icon{nullptr}; // 18px category icon (null when icon_name empty) - wxCheckBox* header{nullptr}; // select-all tri-state + wxStaticBitmap* icon{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}; - bool collapsed{false}; - CollapseChevron* chevron{nullptr}; - wxSizerItem* item{nullptr}; // sizer item of the header h-sizer in m_list_sizer // Material identity, only for Section::Material categories. std::string filament_type; std::string filament_vendor; @@ -111,20 +120,17 @@ private: std::vector rows; // flattened rows, for the tri-state math }; - // A top-level section group mirroring the editor sidebar. Categories are - // nested inside. + // One outer TabCtrl page. Category entries are its inner tabs. struct SectionGroup { - wxString title; // _L("Printer") / _L("Filament") / _L("Process") - Section kind{Section::Print}; // maps 1:1 to the display group - std::string icon_name; // "printer" / "filament" / "process" - ScalableBitmap icon_bmp; // scalable bitmap for DPI changes - wxStaticBitmap* icon{nullptr}; // 18px, like Category::icon - wxCheckBox* header{nullptr}; // tri-state select-all; nullptr for the Filament group - ::StaticLine* header_line{nullptr}; // Filament group's clickable title - CollapseChevron* chevron{nullptr}; - wxSizerItem* item{nullptr}; - bool collapsed{false}; + wxString title; // _L("Printer") / _L("Filament") / _L("Process") + Section kind{Section::Print}; // maps 1:1 to the display group + std::string icon_name; // "printer" / "filament" / "process" + wxPanel* page{nullptr}; + TabCtrl* tabs{nullptr}; + wxPanel* page_host{nullptr}; + wxBoxSizer* page_host_sizer{nullptr}; + int selected_inner{-1}; std::vector categories; // indices into m_categories }; @@ -139,33 +145,31 @@ private: // Material opt-in toggled: enables/disables the material's rows + tri-state // and resyncs the header. void on_master_toggle(size_t category_index); - // Collapse/expand a category or subcategory; resyncs visibility + chevrons. - void toggle_category(size_t category_index); - void toggle_subcategory(size_t category_index, size_t subcategory_index); - // Find-or-create the top-level section group for a Section kind (builds its - // header row on first use). + // Return/create the fixed outer page for a Section kind. size_t section_group_for(Section kind); - // Top-level section group: select-all tri-state toggled / collapse/expand / - // header resync. - void on_section_toggle(size_t section_index); - void toggle_section(size_t section_index); - void update_section_header(SectionGroup& section); - // Single pass over categories/subs/rows: shows an item iff it is not hidden - // by the filter and (for subs/rows) by a collapsed ancestor. Flips the - // header chevrons. Only reads matches_filter; never re-runs filter matching. + 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); + 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(); - // Creates a collapse chevron with a hand cursor; clicking it (with the given - // mouse event, LEFT_DOWN for categories / LEFT_UP for subcategories, - // matching the header's own binding) invokes the toggle. - CollapseChevron* create_chevron(wxWindow* parent, const wxEventTypeTag& event_type, std::function toggle); + void bind_tab_events(); - wxScrolledWindow* m_scroll{nullptr}; - wxBoxSizer* m_list_sizer{nullptr}; // vertical sizer of the scrolled window - wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer + TabCtrl* m_outer_tabs{nullptr}; + wxPanel* m_outer_host{nullptr}; + wxBoxSizer* m_outer_host_sizer{nullptr}; + int m_selected_outer{-1}; + wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer TextInput* m_filter_box{nullptr}; wxTextCtrl* m_filter_ctrl{nullptr}; wxStaticBitmap* m_menu_button{nullptr}; - wxStaticText* m_info{nullptr}; wxString m_info_nonsel; wxString m_info_allsel; wxString m_info_empty; @@ -175,8 +179,6 @@ private: std::vector m_rows; std::vector m_categories; - // Fixed display order enforced by phase order in build_option_model(): - // Printer, then Filament, then Process. std::vector m_sections; }; From 92123cdc2c044c9a161def34812a80d13ce9dc57 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 14 Aug 2026 16:50:33 +0800 Subject: [PATCH 06/60] Add filament color next to tab button in filament setting --- src/slic3r/GUI/PublishSettingsDialog.cpp | 46 ++++++++++++++++++++++-- src/slic3r/GUI/PublishSettingsDialog.hpp | 1 + src/slic3r/GUI/Widgets/Button.cpp | 21 +++++++---- src/slic3r/GUI/Widgets/Button.hpp | 2 ++ src/slic3r/GUI/Widgets/TabCtrl.cpp | 16 +++++++++ src/slic3r/GUI/Widgets/TabCtrl.hpp | 2 ++ 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index b67a962650..c3b927d09b 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -505,8 +505,10 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title, if (const auto* colours = full.opt("filament_colour")) if (source_index < colours->size()) hex = colours->get_at(source_index); - if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) - header_sizer->Add(new wxStaticBitmap(category.page, wxID_ANY, *chip), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { + 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")); @@ -535,7 +537,19 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title, const size_t category_index = m_categories.size(); m_categories.push_back(std::move(category)); m_sections[group].categories.push_back(category_index); - m_sections[group].tabs->AppendItem(title); + if (section == Section::Material) { + std::string hex; + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + if (const auto* colours = full.opt("filament_colour")) + if (source_index < colours->size()) + hex = colours->get_at(source_index); + if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) + m_sections[group].tabs->AppendItem(title, *chip); + else + m_sections[group].tabs->AppendItem(title); + } else { + m_sections[group].tabs->AppendItem(title); + } m_sections[group].page_host_sizer->Add(m_categories[category_index].page, 1, wxEXPAND); if (m_sections[group].selected_inner < 0) m_sections[group].selected_inner = 0; @@ -971,6 +985,15 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) cat.header->Refresh(); if (cat.master_check != nullptr) cat.master_check->Refresh(); + if (cat.filament_color_chip != nullptr) { + std::string hex; + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + if (const auto* colours = full.opt("filament_colour")) + if (cat.filament_slot < colours->size()) + hex = colours->get_at(cat.filament_slot); + if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) + cat.filament_color_chip->SetBitmap(*chip); + } cat.scroll->FitInside(); cat.list_sizer->Layout(); } @@ -978,6 +1001,23 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) for (SectionGroup& section : m_sections) section.tabs->Rescale(); + 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]; + if (category.section != Section::Material) + continue; + std::string hex; + if (const auto* colours = full.opt("filament_colour")) + if (category.filament_slot < colours->size()) + hex = colours->get_at(category.filament_slot); + if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { + const SectionGroup& section = m_sections[category.group]; + const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); + if (iter != section.categories.end()) + m_sections[category.group].tabs->SetItemBitmap(static_cast(iter - section.categories.begin()), *chip); + } + } + SetMinSize(FromDIP(wxSize(600, 500))); Refresh(); } diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 0bae39daa4..949c08a81b 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -105,6 +105,7 @@ private: wxPoint scroll_pos{0, 0}; 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. diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index e236c84e67..0efba60f3d 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -80,6 +80,7 @@ bool Button::SetFont(const wxFont& font) void Button::SetIcon(const wxString& icon) { + custom_icon = wxNullBitmap; auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); if (!icon.IsEmpty()) { //BBS set button icon default size to 20 @@ -95,6 +96,13 @@ void Button::SetIcon(const wxString& icon) } } +void Button::SetBitmap(const wxBitmap& bitmap) +{ + custom_icon = bitmap; + messureSize(); + Refresh(); +} + void Button::SetInactiveIcon(const wxString &icon) { if (!icon.IsEmpty()) { @@ -311,7 +319,8 @@ void Button::render(wxDC& dc) } } auto szContent = textSize; - if (icon.bmp().IsOk()) { + const bool has_custom_icon = custom_icon.IsOk(); + if (has_custom_icon || icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon if (vertical) @@ -319,7 +328,7 @@ void Button::render(wxDC& dc) else szContent.x += spacing; } - szIcon = icon.GetBmpSize(); + szIcon = has_custom_icon ? custom_icon.GetSize() : icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; if (szIcon.x > szContent.x) szContent.x = szIcon.x; @@ -342,12 +351,12 @@ void Button::render(wxDC& dc) } // start draw wxPoint pt = rcContent.GetLeftTop(); - if (icon.bmp().IsOk()) { + if (has_custom_icon || icon.bmp().IsOk()) { if (vertical) pt.x += (rcContent.width - szIcon.x) / 2; else pt.y += (rcContent.height - szIcon.y) / 2; - dc.DrawBitmap(icon.bmp(), pt); + dc.DrawBitmap(has_custom_icon ? custom_icon : icon.bmp(), pt); //BBS norrow size between text and icon if (vertical) { pt.y += szIcon.y + spacing; @@ -380,7 +389,7 @@ void Button::messureSize() wxClientDC dc(this); dc.GetTextExtent(GetLabel(), &textSize.width, &textSize.height, &textSize.x, &textSize.y); wxSize szContent = textSize.GetSize(); - if (this->active_icon.bmp().IsOk()) { + if (custom_icon.IsOk() || this->active_icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon if (vertical) @@ -388,7 +397,7 @@ void Button::messureSize() else szContent.x += 5; } - wxSize szIcon = this->active_icon.GetBmpSize(); + wxSize szIcon = custom_icon.IsOk() ? custom_icon.GetSize() : this->active_icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; if (szIcon.x > szContent.x) szContent.x = szIcon.x; diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 94b245a75b..bb9a013baa 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -35,6 +35,7 @@ class Button : public StaticBox wxSize paddingSize; ScalableBitmap active_icon; ScalableBitmap inactive_icon; + wxBitmap custom_icon; StateColor text_color; @@ -61,6 +62,7 @@ public: bool SetFont(const wxFont& font) override; void SetIcon(const wxString& icon); + void SetBitmap(const wxBitmap& bitmap); void SetInactiveIcon(const wxString& icon); diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 40e291d5ad..6aeffd2dce 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -84,6 +84,7 @@ void TabCtrl::Rescale() { for (auto & b : btns) b->Rescale(); + relayout(); } bool TabCtrl::SetFont(wxFont const& font) @@ -117,6 +118,13 @@ int TabCtrl::AppendItem(const wxString &item, return btns.size() - 1; } +int TabCtrl::AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData) +{ + const int index = AppendItem(item, -1, -1, clientData); + SetItemBitmap(index, bitmap); + return index; +} + bool TabCtrl::DeleteItem(int item) { if (item < 0 || item >= btns.size()) { @@ -170,6 +178,14 @@ void TabCtrl::SetItemText(unsigned int item, wxString const &value) btns[item]->SetLabel(value); } +void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap) +{ + if (item >= btns.size()) + return; + btns[item]->SetBitmap(bitmap); + relayout(); +} + bool TabCtrl::GetItemBold(unsigned int item) const { if (item >= btns.size()) return false; diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index a25f332fb3..e79dd1dee6 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -29,6 +29,7 @@ public: public: 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); bool DeleteItem(int item); @@ -46,6 +47,7 @@ public: wxString GetItemText(unsigned int item) const; void SetItemText(unsigned int item, wxString const &value); + void SetItemBitmap(unsigned int item, const wxBitmap& bitmap); bool GetItemBold(unsigned int item) const; void SetItemBold(unsigned int item, bool bold); From 2b18744cc237c2b9d9e30190d58ecc597c84432d Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 17 Aug 2026 17:50:54 +0800 Subject: [PATCH 07/60] Allow model to be Published without any settings modified --- src/slic3r/GUI/PublishSettingsDialog.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index c3b927d09b..63333ea451 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -165,16 +165,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - // At least one checked, enabled (publishable) key is required. A gated - // material row is disabled even if its value is pre-checked, so it must - // not count. - for (const Row& row : m_rows) - if (row.check->GetValue() && row.check->IsEnabled()) { - EndModal(wxID_OK); - return; - } - MessageDialog(this, _L("No settings selected. Please select at least one setting to publish."), _L("Publish"), wxOK | wxICON_WARNING) - .ShowModal(); + // Publish is always allowed: no settings selected means a publish with + // no settings override. + EndModal(wxID_OK); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); @@ -1012,7 +1005,7 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) hex = colours->get_at(category.filament_slot); if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { const SectionGroup& section = m_sections[category.group]; - const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); + const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); if (iter != section.categories.end()) m_sections[category.group].tabs->SetItemBitmap(static_cast(iter - section.categories.begin()), *chip); } From 6c429059e019d558bf9502aadf886df6e9539fc7 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 18 Aug 2026 13:49:10 +0800 Subject: [PATCH 08/60] 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. --- src/libslic3r/PresetBundle.cpp | 219 ++++++++++ src/libslic3r/PresetBundle.hpp | 3 + src/libslic3r/PublishSettings.cpp | 81 +++- src/libslic3r/PublishSettings.hpp | 22 + src/slic3r/GUI/KBShortcutsDialog.cpp | 1 + src/slic3r/GUI/MainFrame.cpp | 25 +- src/slic3r/GUI/MainFrame.hpp | 2 + src/slic3r/GUI/Plater.cpp | 73 +++- src/slic3r/GUI/Plater.hpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 213 ++++----- src/slic3r/GUI/PublishSettingsDialog.hpp | 33 +- tests/libslic3r/test_3mf.cpp | 95 +++++ .../libslic3r/test_preset_bundle_loading.cpp | 403 ++++++++++++++++++ 13 files changed, 1037 insertions(+), 135 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index febfa5d4dc..8a047fea00 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4783,6 +4783,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (is_published) { std::vector skipped_keys; std::set 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(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(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 &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(static_cast(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(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(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("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("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("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("filament_colour")) { + if (slot < proj_colour->values.size()) + proj_colour->values[slot] = entry.color; + } + if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour")) { + if (slot < proj_multi_colour->values.size()) + proj_multi_colour->values[slot] = entry.color; + } + } + + 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 diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index c8af2e4b71..d568de30e3 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -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 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 material_replacements; }; // Bundle of Print + Filament + Printer presets. diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 9ac0c49472..0d1bd07989 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -3,11 +3,31 @@ #include "PresetBundle.hpp" #include "Preset.hpp" #include "PrintConfig.hpp" +#include "MaterialType.hpp" #include +#include +#include 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& 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 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 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> full_slot_map; // 1. Mandatory material identity & slot count keys for 3MF validation/normalization static const std::vector 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 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 &keep_slots) { + auto *vec = dynamic_cast(&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(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(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; diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index 6525fbb66e..aadf0d3234 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -52,8 +52,30 @@ struct PublishedMaterialEntry { // entries apply to every matching receiver preset. int slot{-1}; std::vector 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 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; diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index ed513c425f..df69fa5434 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -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 diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index abca800277..3cb246529f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -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 diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index a614783c31..040b9447e8 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -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); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 82dd38fe97..28ba184d1c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5449,7 +5449,7 @@ struct Plater::priv BoundingBox scaled_bed_shape_bb() const; // BBS: backup & restore - std::vector load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi = false); + std::vector load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi = false, bool* published_out = nullptr); std::vector 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 Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) +std::vector Plater::priv::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) { std::vector empty_result; bool dlg_cont = true; @@ -7257,6 +7257,22 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const auto &k : *entry_keys_it) if (k.is_string()) entry.keys.emplace_back(k.get()); + // Filament-publishing-v2 fields; absent in legacy files. + if (m.contains("full") && m["full"].is_boolean()) + entry.full = m["full"].get(); + 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()); + if (m.contains("publish_type") && m["publish_type"].is_boolean()) + entry.publish_type = m["publish_type"].get(); + if (m.contains("type") && m["type"].is_string()) + entry.publish_type_value = m["type"].get(); + if (m.contains("publish_color") && m["publish_color"].is_boolean()) + entry.publish_color = m["publish_color"].get(); + if (m.contains("color") && m["color"].is_string()) + entry.color = m["color"].get(); published_config.material_keys.emplace_back(std::move(entry)); } } catch (...) { @@ -7266,6 +7282,18 @@ std::vector Plater::priv::load_files(const std::vector& 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 Plater::priv::load_files(const std::vector& 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 Plater::priv::load_files(const std::vector& 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("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 res = load_files(input_paths, strategy); + bool loaded_published = false; + std::vector 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 Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi) { +std::vector Plater::load_files(const std::vector& 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& 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 diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d29e86a4fa..d6ec283409 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -383,7 +383,7 @@ public: bool preview_zip_archive(const boost::filesystem::path& archive_path); // BBS: restore - std::vector load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false); + std::vector load_files(const std::vector& 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 load_files(const std::vector& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false); // to be called on drag and drop diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 63333ea451..5baff8282c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -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("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("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 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(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 PublishSettingsDialog::GetPublishedM { std::vector 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(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 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& denylist = publish_structural_keys(); + std::vector 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]; diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 949c08a81b..70b1dc8a2d 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -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 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 subs; - std::vector rows; // flattened rows, for the tri-state math + std::vector 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 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(); diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index daf46c3834..d9d6833a5a 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -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("filament_diameter")->values = { 1.75, 1.75 }; + full_cfg.opt("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("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("filament_flow_ratio")->values[1] == 0.98); + } + THEN("the non-published slot is masked to its default") { + REQUIRE(filtered_cfg.opt("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(); + 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 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() == true); + REQUIRE(entries[0]["full_keys"].is_array()); + REQUIRE(entries[0]["full_keys"].size() == 2); + REQUIRE(entries[0]["publish_type"].get() == true); + REQUIRE(entries[0]["type"] == "PLA"); + REQUIRE(entries[0]["publish_color"].get() == false); + } + release_PlateData_list(dst_plates); + } + } +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index fc61083896..f1d2c79f95 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -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("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PETG" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFT99" }; + config.option("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("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("filament_retraction_length")->values == std::vector{ 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("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("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("filament_retraction_length")->values == std::vector{ 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("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("filament_retraction_length")->values == std::vector{ 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("filament_retraction_length")->values == std::vector{ 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("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PETG" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFT99" }; + config.option("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("filament_retraction_length", true)->values = { 0.5 }; + pla.config.opt("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("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#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("filament_retraction_length", true)->values = { 0.5 }; + pla.config.opt("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("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#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("filament_retraction_length", true)->values = { 0.5 }; + pla.config.opt("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("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("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("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("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("filament_colour")->values == std::vector{ "#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("filament_colour")->values == std::vector{ "#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("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + config.option("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("filament_colour", true)->values = { "#123456" }; + pla.config.opt("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("filament_colour")->values == std::vector{ "#ABCDEF" }); + CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 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("filament_colour", true)->values = { "#123456" }; + pla.config.opt("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("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("filament_colour")->values == std::vector{ "#ABCDEF" }); + CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 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. From b82d4f3af8ab729a72912b8ddd78f52718fbcec4 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 18 Aug 2026 15:11:00 +0800 Subject: [PATCH 09/60] Fix issues with filament import when receiver has fewer filament slots than the author's 3MF format --- src/libslic3r/PresetBundle.cpp | 150 ++++++++++++++++-- .../libslic3r/test_preset_bundle_loading.cpp | 78 ++++++++- 2 files changed, 217 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index c58e6c30e7..fd3d4d24b6 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5047,15 +5047,30 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (has_new_semantics) { // Defensive cap: never exceed the file's own filament count. target_slots = std::min(target_slots, num_filaments); + // Slots that carry published content (full/type/colour) must reference a stored + // preset that no other slot shares: the overlay mutates stored presets in place + // (colour and keys), so a shared preset would leak one slot's published values + // into every slot that references it. + std::set published_slots; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if ((entry.full || entry.publish_type || entry.publish_color) && entry.slot >= 0) + published_slots.insert(entry.slot); + std::set used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); + // Mirror first_visible_idx()'s start index so suppressed default presets are + // never picked as a slot material. + const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; 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(new_slot_idx) && entry.publish_type && !entry.publish_type_value.empty()) { + if (published_slots.count(static_cast(new_slot_idx)) != 0) { + // Proactively assign a distinct matching candidate preset if this slot + // carries a published type... + for (const PublishedMaterialEntry &entry : published_config->material_keys) { + if (entry.slot != static_cast(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty()) + continue; for (size_t i = 0; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) continue; if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { initial_preset = candidate.name; @@ -5064,11 +5079,114 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } break; } + // ...otherwise any visible preset not already used by another slot. + if (initial_preset.empty()) { + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + initial_preset = candidate.name; + break; + } + } } if (initial_preset.empty()) - initial_preset = this->filaments.first_visible().name; + // Unpublished filler slot, or every visible preset is already used: repeat + // the receiver's last preset, mirroring the "Add one filament" behaviour + // (PresetBundle::set_num_filaments). + initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name + : this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); + used_preset_names.insert(initial_preset); } + // Slots that were grown before this block (e.g. by update_multi_material_filament_presets + // matching the extruder count) may still alias another slot; re-point them at a + // distinct preset. Slot 0, the receiver's own material, is never re-assigned. + for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { + if (published_slots.count(static_cast(slot)) == 0) + continue; + bool shared = false; + for (size_t other = 0; other < this->filament_presets.size(); ++other) + if (other != slot && this->filament_presets[other] == this->filament_presets[slot]) { + shared = true; + break; + } + if (!shared) + continue; + std::string replacement; + for (const PublishedMaterialEntry &entry : published_config->material_keys) { + if (entry.slot != static_cast(slot) || !entry.publish_type || entry.publish_type_value.empty()) + continue; + for (size_t i = 0; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { + replacement = candidate.name; + break; + } + } + break; + } + if (replacement.empty()) { + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + continue; + replacement = candidate.name; + break; + } + } + if (replacement.empty()) + continue; // every visible preset is used: aliasing is unavoidable + used_preset_names.erase(this->filament_presets[slot]); + this->filament_presets[slot] = replacement; + used_preset_names.insert(replacement); + } + // Mirror set_num_filaments' project_config vector handling ("Add one filament"): + // resize the per-slot colour/type/map vectors to the grown slot count and seed the + // new entries so the slots render with colours instead of blank chips. Only the + // new entries are seeded; the receiver's existing values are left untouched. + ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour"); + ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour"); + ConfigOptionStrings *proj_colour_type = this->project_config.opt("filament_colour_type"); + ConfigOptionInts *proj_map = this->project_config.opt("filament_map"); + ConfigOptionInts *proj_nozzle_map = this->project_config.opt("filament_nozzle_map"); + ConfigOptionInts *proj_volume_map = this->project_config.opt("filament_volume_map"); + const size_t old_colour_count = (proj_colour != nullptr) ? proj_colour->values.size() : 0; + if (proj_colour) proj_colour->resize(target_slots); + if (proj_multi_colour) proj_multi_colour->values.resize(target_slots); + if (proj_colour_type) proj_colour_type->values.resize(target_slots); + if (proj_map) proj_map->values.resize(target_slots, 1); + if (proj_nozzle_map) proj_nozzle_map->values.resize(target_slots, 0); + if (proj_volume_map) proj_volume_map->values.resize(target_slots, static_cast(NozzleVolumeType::nvtStandard)); + this->ams_multi_color_filment.resize(target_slots); + for (size_t slot = old_colour_count; slot < target_slots; ++slot) { + std::string seed; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if (entry.slot == static_cast(slot) && entry.publish_color && !entry.color.empty()) { + seed = entry.color; + break; + } + if (seed.empty()) { + if (const Preset *preset = this->filaments.find_preset(this->filament_presets[slot], false)) { + const ConfigOptionStrings *colours = preset->config.opt("filament_colour"); + if (colours != nullptr && !colours->values.empty()) + seed = colours->values.front(); + } + if (seed.empty()) + seed = "#F2754E"; // filament_colour default + } + if (proj_colour && slot < proj_colour->values.size()) + proj_colour->values[slot] = seed; + if (proj_multi_colour && slot < proj_multi_colour->values.size()) + proj_multi_colour->values[slot] = seed; + if (proj_colour_type && slot < proj_colour_type->values.size()) + proj_colour_type->values[slot] = "1"; // default colour type + } + // Rebuild the flush volumes for the grown slot count (set_num_filaments does the + // same; without it the matrix would stay at the receiver's old size). + this->update_multi_material_filament_presets(); auto apply_slot_keys = [&](Preset &preset, const std::vector &slot_keys, int author_slot, const std::string &material_label) { @@ -5124,17 +5242,31 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool apply_slot = false; } else { // Type mismatch: replace the slot with the first visible same-type - // filament from the receiver's library. - std::string replacement; + // filament from the receiver's library, preferring one that no other + // slot references (a shared stored preset would leak this slot's + // published values into that slot). + std::string replacement, first_same_type; 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 (first_same_type.empty()) + first_same_type = candidate.name; + bool used_elsewhere = false; + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (s != slot && this->filament_presets[s] == candidate.name) { + used_elsewhere = true; + break; + } + if (!used_elsewhere) { + replacement = candidate.name; + break; + } } + if (replacement.empty()) + replacement = first_same_type; if (!replacement.empty()) { const std::string old_name = recv->name; this->filament_presets[slot] = replacement; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index f1d2c79f95..8716af417d 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1069,8 +1069,10 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); } - // Slot 3 published: the receiver grows to 4 so the published slot exists; the filler is - // the receiver's own first visible material. + // Slot 3 published: the receiver grows to 4 so the published slot exists. The unpublished + // filler slots repeat the receiver's last preset ("Add one filament" behaviour); the + // published slot gets a visible preset not used by another slot (with a single-preset + // library it falls back to the receiver's last preset, aliasing being unavoidable). { PresetBundle bundle; add_pla_preset(bundle); @@ -1085,7 +1087,18 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 4); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "My PLA"); CHECK(bundle.filament_presets[3] == filler); + // The project-level per-slot vectors were grown and seeded like "Add one filament": + // fillers take their preset's colour, the published slot its published colour. + CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); + CHECK(bundle.project_config.opt("filament_colour")->values[3] == "#ABCDEF"); + CHECK(bundle.project_config.opt("filament_multi_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour_type")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); + CHECK(bundle.project_config.opt("flush_volumes_matrix")->values.size() == 16); } // Slots 0 and 2 published: the receiver grows to 3, never to the file's 4. @@ -1102,9 +1115,70 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); } } +// The published overlay mutates stored filament presets in place per slot, so a slot carrying +// published content must never share its stored preset with another slot: its colour/keys +// would leak into the sibling slot - and, with "repeat the last preset" growth, into the +// receiver's own first slot. Regression for the slot-aliasing hazard. +TEST_CASE("Published 3MF gives grown published slots a distinct preset so values never leak", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + return config; + }; + + // The receiver has one slot of its own material plus one more preset in the library; the + // author publishes only slot 4 (Red). With naive repeat-last growth the new slot would + // reference the receiver's own preset and the published red would recolor it; the grown + // slot must point at a distinct preset. + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + mine.config.opt("filament_colour", true)->values = { "#123456" }; + Preset &other = add_inmemory_preset(bundle.filaments, "Other PLA"); + other.config.opt_string("filament_type", 0u) = "PLA"; + other.config.opt("filament_colour", true)->values = { "#654321" }; + bundle.filament_presets = { "My PLA" }; + + PublishedMaterialEntry entry; + entry.slot = 3; + entry.publish_color = true; + entry.color = "#ABCDEF"; + 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.size() == 4); + // Unpublished filler slots repeat the receiver's last preset ("Add one filament"). + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "My PLA"); + // The published slot references the unused library preset, not the receiver's own... + CHECK(bundle.filament_presets[3] == "Other PLA"); + // ...so the published colour landed there and never recoloured the receiver's material. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); + CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // The project-level colours are sized and seeded for every grown slot. + CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); + CHECK(bundle.project_config.opt("filament_colour")->values[3] == "#ABCDEF"); + CHECK(bundle.project_config.opt("filament_multi_colour")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_colour_type")->values.size() == 4); + CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); +} + // 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 From 36a8811cc081cae7f9b7ba267c1eafb696b0fefc Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 18 Aug 2026 16:29:27 +0800 Subject: [PATCH 10/60] Change label to "Publish 3MF..." --- localization/i18n/OrcaSlicer.pot | 179 ++++++++++---- localization/i18n/ca/OrcaSlicer_ca.po | 197 ++++++++++++---- localization/i18n/cs/OrcaSlicer_cs.po | 201 ++++++++++++---- localization/i18n/de/OrcaSlicer_de.po | 197 ++++++++++++---- localization/i18n/en/OrcaSlicer_en.po | 179 ++++++++++---- localization/i18n/es/OrcaSlicer_es.po | 197 ++++++++++++---- localization/i18n/eu/OrcaSlicer_eu.po | 197 ++++++++++++---- localization/i18n/fr/OrcaSlicer_fr.po | 197 ++++++++++++---- localization/i18n/hu/OrcaSlicer_hu.po | 199 ++++++++++++---- localization/i18n/it/OrcaSlicer_it.po | 197 ++++++++++++---- localization/i18n/ja/OrcaSlicer_ja.po | 199 ++++++++++++---- localization/i18n/ko/OrcaSlicer_ko.po | 203 ++++++++++++---- localization/i18n/lt/OrcaSlicer_lt.po | 201 ++++++++++++---- localization/i18n/nl/OrcaSlicer_nl.po | 201 ++++++++++++---- localization/i18n/pl/OrcaSlicer_pl.po | 203 ++++++++++++---- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 203 ++++++++++++---- localization/i18n/ru/OrcaSlicer_ru.po | 247 +++++++++++++++----- localization/i18n/sv/OrcaSlicer_sv.po | 207 ++++++++++++---- localization/i18n/th/OrcaSlicer_th.po | 197 ++++++++++++---- localization/i18n/tr/OrcaSlicer_tr.po | 205 ++++++++++++---- localization/i18n/uk/OrcaSlicer_uk.po | 199 ++++++++++++---- localization/i18n/vi/OrcaSlicer_vi.po | 203 ++++++++++++---- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 201 ++++++++++++---- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 197 ++++++++++++---- src/slic3r/GUI/PublishSettingsDialog.cpp | 2 +- 25 files changed, 3733 insertions(+), 1075 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 88e49a85fc..c55cdb7336 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -4452,6 +4452,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, possible-c-format, possible-boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4533,6 +4547,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4545,6 +4565,9 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "" +msgid "N/A" +msgstr "" + msgid "Printing" msgstr "" @@ -4784,6 +4807,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -4987,9 +5016,6 @@ msgstr "" msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "" -msgid "N/A" -msgstr "" - msgid "System agents" msgstr "" @@ -5615,7 +5641,7 @@ msgstr "" msgid "Size:" msgstr "" -#, possible-c-format, possible-boost-format +#, possible-boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5790,6 +5816,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -5916,6 +5945,12 @@ msgstr "" msgid "Save current project as" msgstr "" +msgid "Publish" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "" @@ -7634,6 +7669,12 @@ msgstr "" msgid "Customized Preset" msgstr "" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "" @@ -7780,19 +7821,19 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: same file.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, possible-boost-format +#, possible-c-format, possible-boost-format msgid "✔ Replaced %s.\n" msgstr "" @@ -7994,6 +8035,14 @@ msgstr "" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" @@ -8797,6 +8846,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -9024,9 +9081,6 @@ msgstr "" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "" -msgid "Publish" -msgstr "" - msgid "Publish was canceled" msgstr "" @@ -9042,6 +9096,21 @@ msgstr "" msgid "Jump to webpage" msgstr "" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, possible-c-format, possible-boost-format msgid "Save %s as" msgstr "" @@ -9052,9 +9121,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9732,20 +9813,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9931,6 +9998,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10057,6 +10127,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, possible-boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10179,9 +10255,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11445,6 +11518,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11740,9 +11816,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -12279,9 +12352,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12347,6 +12417,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -13359,6 +13435,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13839,6 +13921,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14800,6 +14888,12 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" @@ -14893,6 +14987,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15278,6 +15375,12 @@ msgstr "" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -18253,9 +18356,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -19087,9 +19187,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 79a9d82df4..4eb07b2635 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4828,6 +4828,20 @@ msgstr "La temperatura actual de la cambra és superior a la temperatura segura msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la cambra (%d℃) és superior a la temperatura objectiu de la cambra (%d℃). El valor mínim és el llindar a partir del qual comença la impressió mentre la cambra continua escalfant-se cap a l'objectiu, de manera que no l'hauria de superar. Es limitarà al valor objectiu." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4948,6 +4962,12 @@ msgstr "" "Sí - Activa el generador de parets Arachne\n" "No - Desactiva el generador de parets Arachne i estableix el mode [Desplaçament] de la pell difusa" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Ample de la Vora d'Adherència" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El mode espiral només funciona quan els bucles de paret són 1, el suport està desactivat, la detecció d'acumulació per sondeig està desactivada, les capes de la coberta superior són 0, la densitat de farciment dispers és 0 i el tipus de timelapse és tradicional." @@ -4963,6 +4983,9 @@ msgstr "" "Sí: canviar aquesta configuració i activar el mode d'espiral automàticament\n" "No - Renunciar a utilitzar el mode espiral aquesta vegada" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Imprimint" @@ -5202,6 +5225,12 @@ msgstr "No s'ha pogut generar el gcode cali" msgid "Calibration error" msgstr "Error de calibratge" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Xarxa no disponible" @@ -5416,9 +5445,6 @@ msgstr "Patró no vàlid. Utilitzeu N, N#K, o una llista separada per comes amb msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Format no vàlid. Format vectorial esperat: \"%1%\"" -msgid "N/A" -msgstr "N/D" - # AI Translated msgid "System agents" msgstr "Agents del sistema" @@ -6067,7 +6093,7 @@ msgstr "Volum:" msgid "Size:" msgstr "Mida:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "S'han trobat conflictes de rutes gcode a la capa %d, Z = %.2lfmm. Si us plau, separeu els objectes conflictius més lluny ( %s <-> %s )." @@ -6248,6 +6274,9 @@ msgstr "Multidispositiu" msgid "Project" msgstr "Projecte" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Sí" @@ -6377,6 +6406,12 @@ msgstr "Desa el projecte com a" msgid "Save current project as" msgstr "Desar el projecte actual com" +msgid "Publish" +msgstr "Publicar" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF STL/STEP/SVG/OBJ/AMF" @@ -8195,6 +8230,12 @@ msgstr "Confirmeu que els Codis-G d'aquests perfils són segurs per evitar danys msgid "Customized Preset" msgstr "Perfil personalitzat" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Els noms dels components dins del fitxer STEP no tenen format UTF8!" @@ -8361,19 +8402,19 @@ msgstr "No s'ha seleccionat el directori per a la substitució" msgid "Replaced with 3D files from directory:\n" msgstr "Substituït amb fitxers 3D del directori:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Omès %s: mateix fitxer.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Omès %s: el fitxer no existeix.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Omès %s: la substitució ha fallat.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Substituït %s.\n" @@ -8583,6 +8624,14 @@ msgstr "Desa el fitxer Laminat com a:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "El fitxer %s s'ha enviat a l'emmagatzematge de la impressora i es pot visualitzar a la impressora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipus de broquet no està establert. Establiu el broquet i torneu-ho a provar." @@ -9506,6 +9555,14 @@ msgstr "Mostrar els perfils no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra els perfils incompatibles o no compatibles a les llistes desplegables d'impressora i de filament. Aquests perfils no es poden seleccionar." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Funcions experimentals" @@ -9748,9 +9805,6 @@ msgstr "Anar a la pàgina web de publicació de models" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: La preparació pot trigar uns quants minuts. Si us plau, sigui pacient." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "La publicació ha estat cancel·lada" @@ -9766,6 +9820,21 @@ msgstr "Carregant dades" msgid "Jump to webpage" msgstr "Anar a la pàgina web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Desar %s com a" @@ -9776,9 +9845,21 @@ msgstr "Perfil d'usuari" msgid "Preset Inside Project" msgstr "Perfil intern del Projecte" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Desvincula del pare" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "El nom no està disponible." @@ -10521,22 +10602,6 @@ msgstr "Estàs segur que vols activar aquesta opció?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Els patrons de farciment estan dissenyats normalment per gestionar la rotació automàticament per garantir una impressió correcta i aconseguir els efectes desitjats (p. ex., Gyroid, Cúbic). Rotar el patró de farciment dispers actual pot portar a un suport insuficient. Procediu amb precaució i comproveu minuciosament qualsevol problema d'impressió potencial. Esteu segur que voleu activar aquesta opció?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'alçada de la capa és massa petita.\n" -"Es posarà a min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." - -msgid "Adjust to the set range automatically?\n" -msgstr "Voleu ajustar el rang automàticament?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Característica experimental: Retreure i tallar el filament a major distància durant els canvis de filaments per minimitzar el flux. Tot i que pot reduir notablement el flux, també pot elevar el risc d'esclops de broquets o altres complicacions d'impressió." @@ -10735,6 +10800,9 @@ msgstr "Trobades paraules clau reservades" msgid "Setting Overrides" msgstr "Anul·lacions de configuració" +msgid "Retraction when switching material" +msgstr "Retracció en canviar de material" + msgid "Basic information" msgstr "Informació bàsica" @@ -10867,6 +10935,12 @@ msgstr "Perfils de processos compatibles" msgid "Printable space" msgstr "Espai imprimible" +msgid "Printer Agent" +msgstr "Agent de la impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10997,9 +11071,6 @@ msgstr "Límits d'alçada de capa" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retracció en canviar de material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12380,6 +12451,9 @@ msgstr " està massa a prop de la zona d'exclusió, i es provocaran col·lisions msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " és massa a prop de l'àrea de detecció d'acumulació i es causaran col·lisions.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les temperatures de broquet seleccionades són incompatibles. La temperatura de broquet de cada filament ha d'estar dins del rang de temperatura de broquet recomanat dels altres filaments. Altrament, es pot produir una obturació del broquet o danys a la impressora." @@ -12714,9 +12788,6 @@ msgstr "Utilitzar 3MF en lloc de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activeu-ho si la impressora accepta un fitxer 3MF com a treball d'impressió. Quan està activat, Orca Slicer envia el fitxer laminat com a .gcode.3mf, en lloc d'un fitxer .gcode simple." -msgid "Printer Agent" -msgstr "Agent de la impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora." @@ -13402,9 +13473,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocitat dels ponts interns. Si el valor s'expressa com un percentatge, es calcularà en funció de la velocitat del pont (bridge_speed). El valor per defecte és del 150%." -msgid "Brim width" -msgstr "Ample de la Vora d'Adherència" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distància del model a la línia de la Vora d'Adherència més exterior" @@ -13488,6 +13556,12 @@ msgstr "" "La geometria es simplificarà abans de detectar angles pronunciats. Aquest paràmetre indica la longitud mínima de la desviació per a la simplificació.\n" "0 per desactivar" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "màquina compatible ascendent" @@ -14679,6 +14753,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleració del farciment superficial superior. L'ús d'un valor inferior pot millorar la qualitat de la superfície superior" @@ -15232,6 +15312,12 @@ msgstr "Amb quin tipus de Codi-G és compatible la impressora." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Impressora modificada de pellets" @@ -16321,6 +16407,12 @@ msgstr "Retracció llarga al canviar d'extrusor" msgid "Retraction distance when extruder change" msgstr "Distància de retracció al canviar d'extrusor" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Alçada Z-hop" @@ -16419,6 +16511,9 @@ msgstr "Longitud addicional en reiniciar" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quan la retracció es compensa després d'un desplaçament, l'extrusor introduirà una quantitat addicional de filament. Aquest ajustament rarament es necessita." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quan la retracció es compensa després d'un canvi d'eina, l'extrusor introduirà una quantitat addicional de filament." @@ -16835,6 +16930,12 @@ msgstr "Canvi d'eina a la Torre de Purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Força el capçal a desplaçar-se a la Torre de Purga abans d'emetre l'ordre de canvi d'eina (Tx). Només és rellevant per a impressores multiextrusor (multicapçal) que utilitzen una Torre de Purga de tipus 2. Per defecte, Orca omet aquest desplaçament en màquines multicapçal perquè el firmware gestiona el canvi de capçal, cosa que pot fer que l'ordre Tx s'emeti sobre la peça impresa. Activeu aquesta opció si voleu que el canvi d'eina s'emeti sempre sobre la Torre de Purga." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Sense capes poc denses( beta )" @@ -20121,9 +20222,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Pujada al amfitrió( host ) d'impressió" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccioneu la implementació de l'agent de xarxa per a la comunicació amb la impressora. Els agents disponibles es registren a l'inici." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleccioneu una impressora Flashforge" @@ -21066,9 +21164,6 @@ msgstr "Alguna cosa inesperada ha passat en intentar iniciar sessió, torneu-ho msgid "User canceled." msgstr "Usuari cancel·lat." -msgid "Head diameter" -msgstr "Diàmetre del cap" - msgid "Max angle" msgstr "Angle màxim" @@ -21887,6 +21982,22 @@ msgstr "" "Evitar la deformació( warping )\n" "Sabíeu que quan imprimiu materials propensos a deformar-se, com ara l'ABS, augmentar adequadament la temperatura del llit pot reduir la probabilitat de deformació?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'alçada de la capa és massa petita.\n" +#~ "Es posarà a min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'alçada de la capa supera el límit a Configuració de la Impressora -> Extrusora -> Límits d'alçada de la capa, això pot causar problemes de qualitat d'impressió." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Voleu ajustar el rang automàticament?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diàmetre del cap" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d'impressió dins d'una sola capa" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index e21fd3086c..95259e0895 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -4786,6 +4786,20 @@ msgstr "Aktuální teplota komory je vyšší než bezpečná teplota materiálu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimální teplota komory (%d℃) je vyšší než cílová teplota komory (%d℃). Minimální hodnota je práh, při kterém tisk začíná, zatímco se komora dále ohřívá k cílové teplotě, takže by ji neměla překročit. Bude omezena na cílovou hodnotu." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Upravit" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4906,6 +4920,12 @@ msgstr "" "Ano – povolit Arachne Wall Generator\n" "Ne – zakázat Arachne Wall Generator a nastavit režim [Displacement] pro Fuzzy Skin" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Šířka límce" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spirálový režim funguje pouze tehdy, když je počet smyček stěny 1, podpěry jsou vypnuté, detekce usazenin sondováním je vypnutá, počet horních plných vrstev je 0, hustota řídké výplně je 0 a typ časosběru je tradiční." @@ -4921,6 +4941,10 @@ msgstr "" "Ano – změnit tato nastavení a automaticky povolit spirálový režim\n" "Ne – tentokrát nepoužít spirálový režim" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Tisk" @@ -5160,6 +5184,12 @@ msgstr "Nepodařilo se vygenerovat kalibrační G-code." msgid "Calibration error" msgstr "Chyba kalibrace" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Síť není dostupná" @@ -5372,10 +5402,6 @@ msgstr "Neplatný vzor. Použijte N, N#K nebo seznam oddělený čárkami s voli msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Neplatný formát. Očekávaný vektorový formát: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Systémoví agenti" @@ -6029,7 +6055,7 @@ msgstr "Objem:" msgid "Size:" msgstr "Velikost:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Byly nalezeny konflikty drah G-kódu ve vrstvě %d, Z = %.2lf mm. Oddělte prosím konfliktní objekty více od sebe (%s <-> %s)." @@ -6210,6 +6236,9 @@ msgstr "Více zařízení" msgid "Project" msgstr "Projekt" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Ano" @@ -6338,6 +6367,12 @@ msgstr "Uložit projekt jako" msgid "Save current project as" msgstr "Uložit aktuální projekt jako" +msgid "Publish" +msgstr "Publikovat" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8157,6 +8192,12 @@ msgstr "Potvrďte prosím, že je G-code v těchto předvolbách bezpečný, aby msgid "Customized Preset" msgstr "Přizpůsobená předvolba" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Názvy komponent v souboru STEP nejsou ve formátu UTF-8!" @@ -8320,19 +8361,19 @@ msgstr "Nebyla vybrána složka pro nahrazení" msgid "Replaced with 3D files from directory:\n" msgstr "Nahrazeno 3D soubory ze složky:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Přeskočeno %s: stejný soubor.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Přeskočeno %s: soubor neexistuje.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Přeskočeno %s: nahrazení se nezdařilo.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Nahrazeno %s.\n" @@ -8543,6 +8584,14 @@ msgstr "Uložit rozřezaný soubor jako:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Soubor %s byl odeslán do úložiště tiskárny a lze jej zobrazit na tiskárně." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Typ trysky není nastaven. Nastavte prosím trysku a zkuste to znovu." @@ -9457,6 +9506,14 @@ msgstr "Zobrazit nepodporované předvolby" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zobrazovat nekompatibilní/nepodporované předvolby v rozevíracích seznamech tiskáren a filamentů. Tyto předvolby nelze vybrat." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Experimentální funkce" @@ -9696,9 +9753,6 @@ msgstr "Přejít na webovou stránku publikace modelu" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Poznámka: Příprava může trvat několik minut. Buďte prosím trpěliví." -msgid "Publish" -msgstr "Publikovat" - msgid "Publish was canceled" msgstr "Publikování bylo zrušeno" @@ -9714,6 +9768,21 @@ msgstr "Nahrávání dat" msgid "Jump to webpage" msgstr "Přejít na webovou stránku" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Uložit %s jako" @@ -9724,10 +9793,22 @@ msgstr "Uživatelská předvolba" msgid "Preset Inside Project" msgstr "Předvolba v projektu" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Oddělit od nadřazeného" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Název není k dispozici." @@ -10469,22 +10550,6 @@ msgstr "Opravdu chcete tuto možnost povolit?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vzory výplně jsou obvykle navrženy tak, aby automaticky pracovaly s rotací a zajistily správný tisk i zamýšlený efekt (např. Gyroid, Cubic). Otočení aktuální řídké výplně může vést k nedostatečné opoře. Postupujte opatrně a pečlivě zkontrolujte možné problémy při tisku. Opravdu chcete tuto možnost povolit?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Výška vrstvy je příliš malá.\n" -"Bude nastavena na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automaticky upravit do nastaveného rozsahu?\n" - -msgid "Adjust" -msgstr "Upravit" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentální funkce: Stažení a odstřižení filamentu na větší vzdálenost během výměny filamentu pro minimalizaci purge. Ačkoliv to může výrazně snížit purge, může to také zvýšit riziko ucpání trysky nebo jiných komplikací při tisku." @@ -10684,6 +10749,9 @@ msgstr "Byla nalezena rezervovaná klíčová slova" msgid "Setting Overrides" msgstr "Přepisování nastavení" +msgid "Retraction when switching material" +msgstr "Retrakce při změně materiálu" + msgid "Basic information" msgstr "Základní informace" @@ -10816,6 +10884,13 @@ msgstr "Kompatibilní procesní profily" msgid "Printable space" msgstr "Tisknutelný prostor" +# AI Translated +msgid "Printer Agent" +msgstr "Agent tiskárny" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10943,9 +11018,6 @@ msgstr "Omezení výšky vrstvy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakce při změně materiálu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12363,6 +12435,9 @@ msgstr " je příliš blízko oblasti vyloučení a může způsobit kolize.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " je příliš blízko oblasti detekce shlukování a dojde ke kolizi.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Vybrané teploty trysky nejsou kompatibilní. Teplota trysky každého filamentu musí spadat do doporučeného rozsahu teplot ostatních filamentů. Jinak může dojít k ucpání trysky nebo poškození tiskárny." @@ -12696,10 +12771,6 @@ msgstr "Použít 3MF místo G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Zapněte, pokud tiskárna přijímá jako tiskovou úlohu soubor 3MF. Je-li zapnuto, odešle Orca Slicer slicovaný soubor jako .gcode.3mf místo prostého souboru .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent tiskárny" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou." @@ -13387,9 +13458,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Rychlost vnitřních mostů. Pokud je hodnota zadána v procentech, vypočítá se podle bridge_speed. Výchozí hodnota je 150 %." -msgid "Brim width" -msgstr "Šířka límce" - msgid "This is the distance from the model to the outermost brim line." msgstr "Vzdálenost od modelu k nejvzdálenější brim linii." @@ -13470,6 +13538,12 @@ msgstr "" "Geometrie bude decimována před detekcí ostrých úhlů. Tento parametr určuje minimální délku odchylky pro decimaci.\n" "0 pro deaktivaci." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "stroj zpětně kompatibilní" @@ -14646,6 +14720,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Akcelerace výplně horní plochy. Použití nižší hodnoty může zlepšit kvalitu horní plochy." @@ -15198,6 +15278,12 @@ msgstr "Jaký typ G-code je s tiskárnou kompatibilní." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Tiskárna na pelety" @@ -16265,6 +16351,12 @@ msgstr "Dlouhá retrakce při změně extruderu" msgid "Retraction distance when extruder change" msgstr "Délka retrakce při změně extruderu" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Výška Z-hopu" @@ -16362,6 +16454,9 @@ msgstr "Dodatečná délka při restartu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Při kompenzaci retrakce po pohybu přesunu extruder posune toto přídavné množství filamentu. Toto nastavení je potřeba jen zřídka." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Při kompenzaci retrakce po výměně nástroje extruder posune toto přídavné množství filamentu." @@ -16780,6 +16875,12 @@ msgstr "Výměna nástroje na věži na očištění trysky" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Vynutí přejezd tiskové hlavy k věži na očištění trysky před vydáním příkazu k výměně nástroje (Tx). Týká se pouze tiskáren s více extrudery (více tiskovými hlavami), které používají věž na očištění trysky typu 2. Ve výchozím nastavení Orca na strojích s více tiskovými hlavami tento přejezd vynechává, protože výměnu hlavy řeší firmware, což může vést k vydání příkazu Tx nad tištěným dílem. Zapněte tuto volbu, chcete-li, aby byla výměna nástroje vždy vydána nad věží na očištění trysky." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Žádné řídké vrstvy (beta)" @@ -20043,9 +20144,6 @@ msgstr "Fyzická tiskárna" msgid "Print Host upload" msgstr "Nahrání na tiskový server" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Vyberte implementaci síťového agenta pro komunikaci s tiskárnou. Dostupní agenti jsou registrováni při spuštění." - # AI Translated msgid "Select a Flashforge printer" msgstr "Vyberte tiskárnu Flashforge" @@ -21002,9 +21100,6 @@ msgstr "Při pokusu o přihlášení došlo k neočekávané chybě, zkuste to p msgid "User canceled." msgstr "Zrušeno uživatelem." -msgid "Head diameter" -msgstr "Průměr hlavy" - msgid "Max angle" msgstr "Maximální úhel" @@ -21873,6 +21968,22 @@ msgstr "" "Zamezte kroucení\n" "Víte, že při tisku materiálů náchylných ke kroucení, jako je ABS, může vhodné zvýšení teploty vyhřívané desky snížit pravděpodobnost kroucení?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Výška vrstvy je příliš malá.\n" +#~ "Bude nastavena na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Výška vrstvy přesahuje limit v Nastavení tiskárny -> Extruder -> Omezení výšky vrstvy, což může způsobit problémy s kvalitou tisku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automaticky upravit do nastaveného rozsahu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Průměr hlavy" + #~ msgid "Print order within a single layer." #~ msgstr "Pořadí tisku v rámci jedné vrstvy." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 50384598e3..0a86439cab 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -4692,6 +4692,20 @@ msgstr "Die aktuelle Kammer-Temperatur ist höher als die sichere Temperatur des msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Die minimale Druckraumtemperatur (%d℃) ist höher als die Ziel-Druckraumtemperatur (%d℃). Der Minimalwert ist der Schwellenwert, bei dem der Druck beginnt, während der Druckraum weiter auf die Zieltemperatur heizt; er sollte diese daher nicht überschreiten. Er wird auf die Zieltemperatur begrenzt." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Anpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4812,6 +4826,12 @@ msgstr "" "Ja - Arachne Wall Generator aktivieren\n" "Nein - Arachne Wall Generator deaktivieren und den Modus [Verschiebung] des Fuzzy Skin setzen" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Randbreite" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Der Spiralmodus funktioniert nur, wenn die Wandschleifen 1 sind, die Stütze deaktiviert ist, die Klumpenerkennung durch Abtasten deaktiviert ist, die oberen Schichtlagen 0 sind, die Dichte der spärlichen Füllung 0 ist und der Zeitraffertyp traditionell ist." @@ -4827,6 +4847,9 @@ msgstr "" "Ja - Diese Einstellungen ändern und den Spiralmodus automatisch aktivieren\n" "Nein - Spiralmodus nicht aktivieren" +msgid "N/A" +msgstr "Nicht verfügbar" + msgid "Printing" msgstr "Drucken" @@ -5066,6 +5089,12 @@ msgstr "Fehler beim Generieren des Kalibrierungs-G-Codes" msgid "Calibration error" msgstr "Kalibrierungsfehler" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Netzwerk nicht verfügbar" @@ -5278,9 +5307,6 @@ msgstr "Ungültiges Muster. Verwenden Sie N, N#K oder eine durch Kommas getrennt msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Ungültiges Format. Erwartetes Vektorformat: \"%1%\"" -msgid "N/A" -msgstr "Nicht verfügbar" - # AI Translated msgid "System agents" msgstr "Systemagenten" @@ -5923,7 +5949,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Größe:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikte von G-Code-Pfaden wurden bei Layer %d, Z = %.2lf mm gefunden.Bitte trennen Sie die konfliktbehafteten Objekte weiter voneinander (%s <-> %s)." @@ -6103,6 +6129,9 @@ msgstr "Multi-Gerät" msgid "Project" msgstr "Projekt" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Ja" @@ -6230,6 +6259,12 @@ msgstr "Projekt speichern als" msgid "Save current project as" msgstr "Aktuelles Projekt speichern als" +msgid "Publish" +msgstr "Veröffentlichen" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8027,6 +8062,12 @@ msgstr "Bitte bestätigen Sie, dass die G-Codes innerhalb dieser Profile sicher msgid "Customized Preset" msgstr "Benutzerdefinierte Profile" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Der Name der Komponenten in der Step-Datei ist nicht im UTF8-Format!" @@ -8191,19 +8232,19 @@ msgstr "Verzeichnis um daraus zu ersetzen wurde nicht ausgewählt" msgid "Replaced with 3D files from directory:\n" msgstr "Ersetzt durch 3D-Dateien aus Verzeichnis:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Übersprungen %s: gleiche Datei.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Übersprungen %s: Datei existiert nicht.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Übersprungen %s: Ersetzen fehlgeschlagen.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Ersetzt %s.\n" @@ -8415,6 +8456,14 @@ msgstr "Geslicte Datei speichern unter:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Die Datei %s wurde an den Speicher des Druckers gesendet und kann auf dem Drucker angezeigt werden." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Die Düsenart ist nicht eingestellt. Bitte stellen Sie die Düse ein und versuchen Sie es erneut." @@ -9296,6 +9345,14 @@ msgstr "Nicht unterstützte Profile anzeigen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Zeigt inkompatible/nicht unterstützte Profile in den Dropdown-Listen für Drucker und Filament an. Diese Profile können nicht ausgewählt werden." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Experimentelle Funktionen" @@ -9530,9 +9587,6 @@ msgstr "Zur Modellveröffentlichungs-Webseite springen" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Hinweis: Die Vorbereitung kann einige Minuten dauern. Bitte haben Sie Geduld." -msgid "Publish" -msgstr "Veröffentlichen" - msgid "Publish was canceled" msgstr "Veröffentlichung wurde abgebrochen" @@ -9548,6 +9602,21 @@ msgstr "Daten werden hochgeladen" msgid "Jump to webpage" msgstr "Zu einer Website springen" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s speichern als" @@ -9558,9 +9627,21 @@ msgstr "Benutzerprofil" msgid "Preset Inside Project" msgstr "Projektbasiertes Profil" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Vom übergeordneten Element trennen" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Der Name ist nicht verfügbar." @@ -10296,22 +10377,6 @@ msgstr "Sind Sie sicher, dass Sie diese Option aktivieren möchten?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Infill-Muster sind in der Regel so konzipiert, dass sie eine automatische Drehung ermöglichen, um einen ordnungsgemäßen Druck zu gewährleisten und die beabsichtigten Effekte zu erzielen (z. B. Gyroid, Cubic). Das Drehen des aktuellen spärlichen Infill-Musters kann zu unzureichender Unterstützung führen. Bitte gehen Sie vorsichtig vor und überprüfen Sie gründlich auf mögliche Druckprobleme. Sind Sie sicher, dass Sie diese Option aktivieren möchten?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Die Schichthöhe ist zu klein.\n" -"Sie wird auf min_layer_height gesetzt\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch an den eingestellten Bereich anpassen?\n" - -msgid "Adjust" -msgstr "Anpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentelle Funktion: Filament beim Filamentwechsel weiter zurückziehen und abschneiden, um den Flush zu minimieren. Obwohl dies den Flush deutlich reduzieren kann, kann es auch das Risiko von Düsenverstopfungen oder anderen Druckkomplikationen erhöhen." @@ -10505,6 +10570,9 @@ msgstr "Reservierte Schlüsselwörter gefunden" msgid "Setting Overrides" msgstr "Überschreiben der Einstellungen" +msgid "Retraction when switching material" +msgstr "Rückzug bei Materialwechsel" + msgid "Basic information" msgstr "Grundlegende Informationen" @@ -10634,6 +10702,12 @@ msgstr "Kompatible Prozessprofile" msgid "Printable space" msgstr "Druckbarer Raum" +msgid "Printer Agent" +msgstr "Drucker-Agent" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10759,9 +10833,6 @@ msgstr "Höhenbegrenzungen für Schichten" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rückzug bei Materialwechsel" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12103,6 +12174,9 @@ msgstr " ist zu nahe am Sperrbereich und es werden Kollisionen verursacht.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ist zu nahe am Klumpenerkennungsbereich und es werden Kollisionen verursacht.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Die ausgewählten Düsentemperaturen sind nicht kompatibel. Die Düsentemperatur jedes Filaments muss innerhalb des empfohlenen Düsentemperaturbereichs der anderen Filamente liegen. Andernfalls kann es zu Düsenverstopfungen oder Druckerschäden kommen." @@ -12418,9 +12492,6 @@ msgstr "Benutze 3MF statt G-Code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivieren Sie diese Option, wenn der Drucker eine 3MF-Datei als Druckauftrag akzeptiert. Wenn aktiviert, sendet Orca Slicer die geslicete Datei als .gcode.3mf, anstatt als einfache .gcode-Datei." -msgid "Printer Agent" -msgstr "Drucker-Agent" - msgid "Select the network agent implementation for printer communication." msgstr "Wählen Sie die Netzwerk-Agent-Implementierung für die Druckerkommunikation aus." @@ -13091,9 +13162,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Geschwindigkeit der internen Brücken. Wenn der Wert als Prozentsatz angegeben wird, wird er auf der Grundlage der Brückengeschwindigkeit berechnet. Der Standardwert beträgt 150 %." -msgid "Brim width" -msgstr "Randbreite" - msgid "This is the distance from the model to the outermost brim line." msgstr "Abstand vom Modell zur äußersten Randlinie" @@ -13174,6 +13242,12 @@ msgstr "" "Die Geometrie wird vor der Erkennung scharfer Winkel reduziert. Dieser Parameter ist ein Indikator für die minimale Länge der Abweichung für die Reduzierung.\n" "0 zum Deaktivieren." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "Aufwärtskompatible Maschine" @@ -14341,6 +14415,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Dies ist die Beschleunigung der Füllung von der obersten Schicht. Die Verwendung eines niedrigeren Werts kann die Qualität der Oberfläche verbessern." @@ -14874,6 +14954,12 @@ msgstr "Mit welcher Art von G-Code ist der Drucker kompatibel." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Pellet-Modifizierter Drucker" @@ -15920,6 +16006,12 @@ msgstr "Langer Rückzug beim Extruderwechsel" msgid "Retraction distance when extruder change" msgstr "Rückzugslänge beim Extruderwechsel" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z-Hub-Höhe" @@ -16014,6 +16106,9 @@ msgstr "Zusätzliche Länge beim Neustart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Wenn die Rückzugskompensation nach dem Reisemove durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben. Diese Einstellung wird nur selten benötigt." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Wenn die Rückzugskompensation nach dem Wechsel des Werkzeugs durchgeführt wird, wird der Extruder diese zusätzliche Menge an Filament schieben." @@ -16431,6 +16526,12 @@ msgstr "Werkzeugwechsel auf dem Reinigungsturm" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Erzwinge, dass der Werkzeugkopf zum Reinigungsturm fährt, bevor der Werkzeugwechselbefehl (Tx) ausgegeben wird. Nur relevant für Mehrfach-Extruder (Mehrfach-Werkzeugkopf) Drucker, die einen Typ-2-Reinigungsturm verwenden. Standardmäßig überspringt Orca die Fahrt auf Mehrfach-Werkzeugkopf-Maschinen, da die Firmware den Kopfwechsel übernimmt, was dazu führen kann, dass der Tx-Befehl über dem gedruckten Teil ausgegeben wird. Aktivieren Sie diese Option, wenn Sie möchten, dass der Werkzeugwechsel immer über dem Reinigungsturm ausgegeben wird." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Keine dünnen Schichten (Beta)" @@ -19650,9 +19751,6 @@ msgstr "Drucker" msgid "Print Host upload" msgstr "Hochladen zum Druck-Host" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wählen Sie die Implementierung des Netzwerkagenten für die Druckerkommunikation. Verfügbare Agenten werden beim Start registriert." - msgid "Select a Flashforge printer" msgstr "Wählen Sie einen Flashforge-Drucker aus" @@ -20500,9 +20598,6 @@ msgstr "Es ist etwas Unerwartetes passiert, als Sie versucht haben, sich anzumel msgid "User canceled." msgstr "Benutzer abgebrochen." -msgid "Head diameter" -msgstr "Kopfdurchmesser" - msgid "Max angle" msgstr "Maximaler Winkel" @@ -21286,6 +21381,22 @@ msgstr "" "Verwerfungen vermeiden\n" "Wussten Sie, dass beim Drucken von Materialien, die zu Verwerfungen neigen, wie z.B. ABS, durch eine entsprechende Erhöhung der Heizbetttemperatur die Wahrscheinlichkeit von Verwerfungen verringert werden kann?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Die Schichthöhe ist zu klein.\n" +#~ "Sie wird auf min_layer_height gesetzt\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Die Schichthöhe überschreitet das Limit in Druckereinstellungen -> Extruder -> Schichthöhenlimits. Dies kann zu Problemen mit der Druckqualität führen." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch an den eingestellten Bereich anpassen?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopfdurchmesser" + #~ msgid "Print order within a single layer." #~ msgstr "Druckreihenfolge innerhalb einer einzelnen Schicht" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 232820f681..03b83309de 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -4448,6 +4448,20 @@ msgstr "" msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4529,6 +4543,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" @@ -4541,6 +4561,9 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "" +msgid "N/A" +msgstr "" + msgid "Printing" msgstr "" @@ -4780,6 +4803,12 @@ msgstr "" msgid "Calibration error" msgstr "" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "" @@ -4983,9 +5012,6 @@ msgstr "" msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "" -msgid "N/A" -msgstr "" - msgid "System agents" msgstr "" @@ -5611,7 +5637,7 @@ msgstr "" msgid "Size:" msgstr "" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "" @@ -5786,6 +5812,9 @@ msgstr "" msgid "Project" msgstr "" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "" @@ -5912,6 +5941,12 @@ msgstr "" msgid "Save current project as" msgstr "" +msgid "Publish" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "" @@ -7630,6 +7665,12 @@ msgstr "" msgid "Customized Preset" msgstr "" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "" @@ -7776,19 +7817,19 @@ msgstr "" msgid "Replaced with 3D files from directory:\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "" @@ -7990,6 +8031,14 @@ msgstr "" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" @@ -8793,6 +8842,14 @@ msgstr "" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "" @@ -9020,9 +9077,6 @@ msgstr "" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "" -msgid "Publish" -msgstr "" - msgid "Publish was canceled" msgstr "" @@ -9038,6 +9092,21 @@ msgstr "" msgid "Jump to webpage" msgstr "" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "" @@ -9048,9 +9117,21 @@ msgstr "" msgid "Preset Inside Project" msgstr "" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "" @@ -9728,20 +9809,6 @@ msgstr "" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "" - -msgid "Adjust to the set range automatically?\n" -msgstr "" - -msgid "Adjust" -msgstr "" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "" @@ -9927,6 +9994,9 @@ msgstr "" msgid "Setting Overrides" msgstr "" +msgid "Retraction when switching material" +msgstr "" + msgid "Basic information" msgstr "" @@ -10053,6 +10123,12 @@ msgstr "" msgid "Printable space" msgstr "" +msgid "Printer Agent" +msgstr "" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10175,9 +10251,6 @@ msgstr "" msgid "Z-Hop" msgstr "" -msgid "Retraction when switching material" -msgstr "" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11441,6 +11514,9 @@ msgstr "" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "" @@ -11736,9 +11812,6 @@ msgstr "" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "" -msgid "Printer Agent" -msgstr "" - msgid "Select the network agent implementation for printer communication." msgstr "" @@ -12275,9 +12348,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "" -msgid "Brim width" -msgstr "" - msgid "This is the distance from the model to the outermost brim line." msgstr "" @@ -12343,6 +12413,12 @@ msgid "" "0 to deactivate." msgstr "" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "" @@ -13355,6 +13431,12 @@ msgstr "" msgid "Gyroid" msgstr "" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "" @@ -13835,6 +13917,12 @@ msgstr "" msgid "Klipper" msgstr "" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "" @@ -14796,6 +14884,12 @@ msgstr "" msgid "Retraction distance when extruder change" msgstr "" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "" @@ -14889,6 +14983,9 @@ msgstr "" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "" @@ -15274,6 +15371,12 @@ msgstr "" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "" @@ -18249,9 +18352,6 @@ msgstr "" msgid "Print Host upload" msgstr "" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "" - msgid "Select a Flashforge printer" msgstr "" @@ -19083,9 +19183,6 @@ msgstr "" msgid "User canceled." msgstr "" -msgid "Head diameter" -msgstr "" - msgid "Max angle" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 1913c4512a..74617b88ec 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -4564,6 +4564,20 @@ msgstr "La temperatura actual de la recámara es superior a la temperatura de se msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura mínima de la recámara (%d℃) es superior a la temperatura objetivo de la recámara (%d℃). El valor mínimo es el umbral en el que comienza la impresión mientras la recámara continúa calentándose hacia el objetivo, por lo que no debería superarlo. Se ajustará al valor objetivo." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4684,6 +4698,12 @@ msgstr "" "Sí: habilitar el generador de muros Arachne\n" "No: deshabilitar el generador de paredes Arachne y establecer el modo [Desplazamiento] de la piel rugosa" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Ancho del borde de adherencia" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "El modo espiral solo funciona cuando los bucles de perímetro son 1, el soporte está desactivado, la detección de agrupamientos mediante sondeo está desactivada, las capas superiores de la carcasa son 0, la densidad de relleno es 0 y el tipo de lapso de tiempo es tradicional." @@ -4699,6 +4719,9 @@ msgstr "" "Sí - Cambiar estos ajustes y activar el modo espiral automáticamente\n" "No - Dejar de usar el modo espiral esta vez" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Imprimiendo" @@ -4938,6 +4961,12 @@ msgstr "Fallo al generar el G-Code de calibración" msgid "Calibration error" msgstr "Error de calibración" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "Red no disponible" @@ -5144,9 +5173,6 @@ msgstr "Patrón inválido. Use N, N#K, o una lista separada por comas con #K opc msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato inválido. Formato de vector esperado: \"%1%\"" -msgid "N/A" -msgstr "N/A" - msgid "System agents" msgstr "Agentes del sistema" @@ -5779,7 +5805,7 @@ msgstr "Volumen:" msgid "Size:" msgstr "Tamaño:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Se han encontrado conflictos de rutas G-Code en la capa %d, Z = %.2lfmm. Por favor, separe más los objetos en conflicto (%s <-> %s)." @@ -5960,6 +5986,9 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Proyecto" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Sí" @@ -6087,6 +6116,12 @@ msgstr "Guardar proyecto como" msgid "Save current project as" msgstr "Guardar el proyecto actual como" +msgid "Publish" +msgstr "Publicar" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7841,6 +7876,12 @@ msgstr "¡Por favor, confirme que el G-Code dentro de los perfiles son seguros p msgid "Customized Preset" msgstr "Perfil Personalizado" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "¡El nombre de los componentes dentro del archivo de pasos no tiene formato UTF8!" @@ -7997,19 +8038,19 @@ msgstr "No se seleccionó el directorio para el reemplazo" msgid "Replaced with 3D files from directory:\n" msgstr "Reemplazado con archivos 3D desde el directorio:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Omitido %s: mismo archivo.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Omitido %s: el archivo no existe.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Omitido %s: fallo al reemplazar.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Reemplazado %s.\n" @@ -8218,6 +8259,14 @@ msgstr "Guardar el archivo laminado como:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "El archivo %s ha sido mandado al almacenamiento de la impresora y puede ser visualizado en la impresora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipo de boquilla no está establecido. Configure la boquilla e inténtelo de nuevo." @@ -9074,6 +9123,14 @@ msgstr "Mostrar ajustes preestablecidos no compatibles" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostrar los ajustes preestablecidos incompatibles o no compatibles en los menús desplegables de impresoras y filamentos. Estos ajustes preestablecidos no se pueden seleccionar." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Funciones experimentales" @@ -9305,9 +9362,6 @@ msgstr "Ir a la página web de publicación de modelos" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: La preparación puede llevar varios minutos. Por favor, sea paciente." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "La publicación fue cancelada" @@ -9323,6 +9377,21 @@ msgstr "Cargando datos" msgid "Jump to webpage" msgstr "Ir a la página web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Guardar %s como" @@ -9333,9 +9402,21 @@ msgstr "Perfil de usuario" msgid "Preset Inside Project" msgstr "Perfil interno del proyecto" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Separar del elemento padre" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "El nombre no está disponible." @@ -10031,22 +10112,6 @@ msgstr "¿Está seguro de que desea activar esta opción?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Los patrones de relleno suelen diseñarse para gestionar la rotación automáticamente y asegurar una impresión adecuada y lograr sus efectos previstos (p. ej., Giroide, Cúbico). Rotar el patrón de relleno actual puede provocar soporte insuficiente. Proceda con precaución y compruebe detenidamente posibles problemas de impresión. ¿Está seguro de que desea activar esta opción?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La altura de la capa es demasiado pequeña.\n" -"Se establecerá en min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." - -msgid "Adjust to the set range automatically?\n" -msgstr "¿Desea ajustar el rango automáticamente?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Función experimental: retraer y cortar el filamento a una mayor distancia durante los cambios de filamento para minimizar el purgado. Aunque puede reducir notablemente el purgado, también puede aumentar el riesgo de atascos de boquilla u otras complicaciones de impresión.Característica experimental: Retraer y cortar el filamento a mayor distancia durante los cambios de filamento para minimizar el descarte. Aunque puede reducir notablemente el descarte, también puede elevar el riesgo de atascos de boquillas u otros problemas en la impresión." @@ -10238,6 +10303,9 @@ msgstr "Palabras clave utilizadas y encontradas" msgid "Setting Overrides" msgstr "Sobreescribir Ajustes de impresora" +msgid "Retraction when switching material" +msgstr "Retracción al cambiar de material" + msgid "Basic information" msgstr "Información básica" @@ -10364,6 +10432,12 @@ msgstr "Perfiles de proceso compatibles" msgid "Printable space" msgstr "Espacio imprimible" +msgid "Printer Agent" +msgstr "Agente de impresora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10489,9 +10563,6 @@ msgstr "Límites de altura de la capa" msgid "Z-Hop" msgstr "Salto en Z" -msgid "Retraction when switching material" -msgstr "Retracción al cambiar de material" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11809,6 +11880,9 @@ msgstr " está demasiado cerca de una zona de exclusión, lo que provocará coli msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está demasiado cerca del área de detección de aglomeraciones, y se producirán colisiones.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Las temperaturas de boquilla seleccionadas son incompatibles. La temperatura de boquilla de cada filamento debe estar dentro del rango de temperaturas recomendado para los demás filamentos. De lo contrario, podrían producirse atascos en la boquilla o daños en la impresora." @@ -12116,9 +12190,6 @@ msgstr "Utiliza 3MF en lugar de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activa esta opción si la impresora admite un archivo 3MF como trabajo de impresión. Cuando está activada, Orca Slicer envía el archivo cortado como un archivo .gcode.3mf, en lugar de como un archivo .gcode convencional." -msgid "Printer Agent" -msgstr "Agente de impresora" - msgid "Select the network agent implementation for printer communication." msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora." @@ -12794,9 +12865,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidad de los puntes internos. Si se expresa como un porcentaje, será Calculado en base a la velocidad de puente. El valor por defecto es 150%." -msgid "Brim width" -msgstr "Ancho del borde de adherencia" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distancia del modelo a la línea más externa del borde de adherencia." @@ -12876,6 +12944,12 @@ msgstr "" "La geometría se verá diezmada antes de detectar angulos agudos. Este parámetro indica la longitud mínima de desviación para el diezmado\n" "0 para desactivar." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "máquina compatible ascendente" @@ -14011,6 +14085,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Aceleración del relleno de la superficie superior. El uso de un valor más bajo puede mejorar la calidad de la superficie superior." @@ -14544,6 +14624,12 @@ msgstr "Con qué tipo de G-Code es compatible la impresora." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Impresora Modificada para Pellets" @@ -15583,6 +15669,12 @@ msgstr "Retracción larga al cambiar de extrusor" msgid "Retraction distance when extruder change" msgstr "Distancia de retracción al cambiar de extrusor" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Altura de Salto en Z" @@ -15676,6 +15768,9 @@ msgstr "Longitud extra de reinicio" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Cuando la retracción se compensa después de un desplazamiento, el extrusor expulsará esta cantidad adicional de filamento. Esta función no suele ser necesaria." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Cuando se compensa la retracción después de cambiar de cabezal, el extrusor expulsará esta cantidad adicional de filamento." @@ -16082,6 +16177,12 @@ msgstr "Cambio de herramienta en la torre de purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Obliga al cabezal a desplazarse hasta la torre de purga antes de emitir el comando de cambio de herramienta (Tx). Solo es relevante para impresoras con múltiples extrusores (múltiples cabezales) que utilicen una torre de limpieza de tipo 2. Por defecto, Orca omite el desplazamiento en máquinas con múltiples cabezales porque el firmware se encarga del cambio de cabezal, lo que puede provocar que el comando Tx se emita por encima de la pieza impresa. Habilita esta opción si deseas que el cambio de herramienta se emita siempre por encima de la torre de purga." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Sin capas de baja densidad (beta)" @@ -19281,9 +19382,6 @@ msgstr "Impresora física" msgid "Print Host upload" msgstr "Mandar al servidor de impresión" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Seleccione la implementación del agente de red para la comunicación con la impresora. Los agentes disponibles se registran al iniciar el sistema." - msgid "Select a Flashforge printer" msgstr "Selecciona una impresora Flashforge" @@ -20125,9 +20223,6 @@ msgstr "Ha ocurrido algo inesperado al intentar iniciar sesión, inténtelo de n msgid "User canceled." msgstr "Cancelado por el usuario." -msgid "Head diameter" -msgstr "Diámetro de la cabeza" - msgid "Max angle" msgstr "Ángulo máximo" @@ -20861,6 +20956,22 @@ msgstr "" "Evita la deformación\n" "¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La altura de la capa es demasiado pequeña.\n" +#~ "Se establecerá en min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La altura de la capa excede el límite en Ajustes de la Impresora -> Extrusor -> Limite de Altura de Capa, esto puede causar problemas de calidad de impresión." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "¿Desea ajustar el rango automáticamente?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diámetro de la cabeza" + #~ msgid "Print order within a single layer." #~ msgstr "Orden de impresión dentro de cada capa." diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index fa10cc387f..cceccaa64c 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana \n" "Language-Team: \n" @@ -4606,6 +4606,20 @@ msgstr "Uneko ganberako tenperatura materialaren tenperatura segurua baino handi msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Ganberako gutxieneko tenperatura (%d ℃) helburuko ganbera-tenperatura (%d ℃) baino altuagoa da. Gutxieneko balioa inprimaketa hasten den atalasea da, ganberak helbururantz berotzen jarraitzen duen bitartean; beraz, ez luke helburua gainditu behar. Helburuko baliora mugatuko da." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Doitu" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4725,6 +4739,12 @@ msgstr "" "Bai - Gaitu Arachne horma-sorgailua\n" "Ez - Desgaitu Arachne horma-sorgailua eta ezarri gainazal zimurraren [Desplazamendua] modua" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Itsaspen ertzaren zabalera" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Espiral moduak baldintza hauetan bakarrik funtzionatzen du: horma-begiztak 1 izatea, euskarriak desgaituta egotea, haztatze bidezko material-metaketa detektatzea desgaituta egotea, goiko estalki-geruzak 0 izatea, dentsitate baxuko betegarriaren dentsitatea 0 izatea eta timelapse mota tradizionala izatea." @@ -4740,6 +4760,9 @@ msgstr "" "Bai - Aldatu ezarpen hauek eta gaitu espiral/loreontzi modua automatikoki\n" "Ez - Utzi bertan behera espiral modua gaitzea" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Inprimatzen" @@ -4979,6 +5002,12 @@ msgstr "Hutsegitea gertatu da kalibrazioko G-Code-a sortzean" msgid "Calibration error" msgstr "Kalibrazio akatsa" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Sarea ez dago erabilgarri" @@ -5192,9 +5221,6 @@ msgstr "Patroi baliogabea. Erabili N, N#K edo komaz bereizitako zerrenda bat, sa msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formatuak ez du balio. Espero den formatu bektoriala: \"%1%\"" -msgid "N/A" -msgstr "N/A" - msgid "System agents" msgstr "Sistema-agenteak" @@ -5828,7 +5854,7 @@ msgstr "Bolumena:" msgid "Size:" msgstr "Tamaina:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-code ibilbideen gatazkak aurkitu dira %d geruzan, Z = %.2lf mm. Urrundu gehiago gatazkan dauden objektuak (%s <-> %s)." @@ -6005,6 +6031,9 @@ msgstr "Gailu anitz" msgid "Project" msgstr "Proiektua" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Bai" @@ -6132,6 +6161,12 @@ msgstr "Gorde proiektua honela" msgid "Save current project as" msgstr "Gorde uneko proiektua honela" +msgid "Publish" +msgstr "Argitaratu" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Inportatu 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7909,6 +7944,12 @@ msgstr "Berretsi aurrezarpen hauetako G-codea segurua dela, makinari kalterik ez msgid "Customized Preset" msgstr "Aurrezarpen pertsonalizatua" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP fitxategiko osagai-izena(k) ez dago/daude UTF-8 formatuan!" @@ -8064,19 +8105,19 @@ msgstr "Ez da ordezkatzeko direktoriorik hautatu" msgid "Replaced with 3D files from directory:\n" msgstr "Direktorio honetako 3D fitxategiekin ordeztuta:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s saltatu da: fitxategi bera.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s saltatu da: fitxategia ez da existitzen.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s saltatu da: ezin izan da ordeztu.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s ordezkatu da.\n" @@ -8285,6 +8326,14 @@ msgstr "Gorde xerratutako fitxategia honela:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s fitxategia inprimagailuaren biltegiratze-eremura bidali da eta inprimagailuan ikus daiteke." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Pita mota ez dago ezarrita. Ezarri pita eta saiatu berriro." @@ -9142,6 +9191,14 @@ msgstr "Erakutsi onartzen ez diren aurrezarpenak" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Erakutsi bateraezinak edo onartu gabeak diren aurrezarpenak inprimagailuaren eta filamentuaren goitibeherako zerrendetan. Aurrezarpen hauek ezin dira hautatu." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Ezaugarri esperimentalak" @@ -9374,9 +9431,6 @@ msgstr "Joan modeloa argitaratzeko web-orrira" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Oharra: prestaketak minutu batzuk iraun ditzake. Izan pazientzia." -msgid "Publish" -msgstr "Argitaratu" - msgid "Publish was canceled" msgstr "Argitaratzea bertan behera utzi da" @@ -9392,6 +9446,21 @@ msgstr "Datuak igotzen" msgid "Jump to webpage" msgstr "Joan web-orrira" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Gorde %s honela" @@ -9402,9 +9471,21 @@ msgstr "Erabiltzailearen aurrezarpena" msgid "Preset Inside Project" msgstr "Proiektu barruko aurrezarpena" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Bereizi gurasotik" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Izena ez dago erabilgarri." @@ -10124,22 +10205,6 @@ msgstr "Ziur aukera hau gaitu nahi duzula?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Betegarri-patroiak normalean biraketa automatikoki kudeatzeko diseinatuta daude, behar bezala inprimatzeko eta nahi den efektua lortzeko (adibidez, Giroidea edo Kubikoa). Uneko dentsitate baxuko betegarri-patroia biratzeak euskarri eskasa eragin dezake. Kontuz jarraitu eta egiaztatu arretaz inprimatze-arazorik sor daitekeen. Ziur zaude aukera hau gaitu nahi duzula?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Geruza-altuera txikiegia da.\n" -"min_layer_height baliora ezarriko da\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." - -msgid "Adjust to the set range automatically?\n" -msgstr "Doitu automatikoki ezarritako barrutira?\n" - -msgid "Adjust" -msgstr "Doitu" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funtzio esperimentala: filamentu aldaketetan distantzia handiagoan atzera egitea eta moztea, purgatzea minimizatzeko. Purgatzea nabarmen murriztu dezakeen arren, pitaren buxadurak edo bestelako inprimatze-arazoak izateko arriskua ere handitu dezake." @@ -10333,6 +10398,9 @@ msgstr "Erreserbatutako gako-hitzak aurkitu dira" msgid "Setting Overrides" msgstr "Ezarpenen gainidazketak" +msgid "Retraction when switching material" +msgstr "Atzera-egitea materiala aldatzean" + msgid "Basic information" msgstr "Oinarrizko informazioa" @@ -10459,6 +10527,12 @@ msgstr "Prozesu-profil bateragarriak" msgid "Printable space" msgstr "Inprimatzeko espazioa" +msgid "Printer Agent" +msgstr "Inprimagailu-agentea" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10584,9 +10658,6 @@ msgstr "Geruza-altueraren mugak" msgid "Z-Hop" msgstr "Z jauzia" -msgid "Retraction when switching material" -msgstr "Atzera-egitea materiala aldatzean" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11912,6 +11983,9 @@ msgstr " bazterketa-eremu batetik gertuegi dago, eta talkak eragingo ditu.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " material-metaketa detektatzeko eremutik gertuegi dago, eta talkak eragingo ditu.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Hautatutako pita-tenperaturak ez dira bateragarriak. Filamentu bakoitzaren pita-tenperaturak gainerako filamentuen gomendatutako pita-tenperatura tartean egon behar du. Bestela, pita buxatu edo inprimagailua kaltetu daiteke." @@ -12228,9 +12302,6 @@ msgstr "Erabili 3MF G-codearen ordez" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Gaitu aukera hau inprimagailuak 3MF fitxategi bat inprimatze-lan gisa onartzen badu. Gaituta dagoenean, OrcaSlicerrek xerratutako fitxategia .gcode.3mf gisa bidaltzen du, .gcode fitxategi arrunt baten ordez." -msgid "Printer Agent" -msgstr "Inprimagailu-agentea" - msgid "Select the network agent implementation for printer communication." msgstr "Hautatu inprimagailuarekin komunikatzeko sare-agentearen inplementazioa." @@ -12905,9 +12976,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Barru-zubien abiadura. Balioa ehuneko gisa adierazten bada, Zubien abiadura-ren arabera kalkulatuko da. Lehenetsitako balioa % 150ekoa da." -msgid "Brim width" -msgstr "Itsaspen ertzaren zabalera" - msgid "This is the distance from the model to the outermost brim line." msgstr "Hau da modelotik itsaspen ertzaren kanporen lerrora dagoen distantzia." @@ -12987,6 +13055,12 @@ msgstr "" "Geometria sinplifikatu egingo da angelu zorrotzak detektatu aurretik. Parametro honek sinplifikaziorako desbideratzearen gutxieneko luzera adierazten du.\n" "0, desaktibatzeko." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "gorantz bateragarria den makina" @@ -14137,6 +14211,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidea" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Hau da goiko gainazaleko betegarriaren azelerazioa. Balio txikiago batek goiko gainazalaren kalitatea hobetu dezake." @@ -14676,6 +14756,12 @@ msgstr "Inprimagailua zer G-code motarekin den bateragarria." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Pelletekin moldatutako inprimagailua" @@ -15719,6 +15805,12 @@ msgstr "Atzera-egite luzea estrusorea aldatzean" msgid "Retraction distance when extruder change" msgstr "Atzera-egite distantzia estrusorea aldatzean" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z jauziaren altuera" @@ -15812,6 +15904,9 @@ msgstr "Berrabiaraztean luzera gehigarria" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Mugimenduaren ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du. Ezarpen hau gutxitan behar da." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Tresna aldatu ondoren atzera-egitea konpentsatzen denean, estrusoreak filamentu kantitate gehigarri hau bultzatuko du." @@ -16220,6 +16315,12 @@ msgstr "Tresna-aldaketa purgatze-dorrean" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Behartu inprimatze-burua purgatze-dorrera joatera tresna aldatzeko agindua (Tx) eman aurretik. 2. motako purgatze-dorrea erabiltzen duten estrusore anitzeko (inprimatze-buru anitzeko) inprimagailuetarako bakarrik da garrantzitsua. Lehenespenez, Orcak ez du joan-etorria egiten inprimatze-buru anitzeko makinetan, firmwareak buruaren aldaketa kudeatzen duelako; horren ondorioz, Tx agindua inprimatutako piezaren gainean eman daiteke. Gaitu aukera hau tresna-aldaketa beti purgatze-dorrearen gainean egin dadin." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Geruza bakandurik ez (beta)" @@ -19429,9 +19530,6 @@ msgstr "Inprimagailu fisikoa" msgid "Print Host upload" msgstr "Inprimatze-ostalariaren karga" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Hautatu sare-agentearen inplementazioa inprimagailuarekin komunikatzeko. Erabilgarri dauden agenteak abioan erregistratzen dira." - msgid "Select a Flashforge printer" msgstr "Hautatu Flashforge inprimagailu bat" @@ -20278,9 +20376,6 @@ msgstr "Ustekabeko zerbait gertatu da saioa hasten saiatzean; saiatu berriro." msgid "User canceled." msgstr "Erabiltzaileak bertan behera utzi du." -msgid "Head diameter" -msgstr "Buruaren diametroa" - msgid "Max angle" msgstr "Gehieneko angelua" @@ -21016,6 +21111,22 @@ msgstr "" "Saihestu okertzea\n" "Ba al zenekien ABS bezalako okertzeko joera duten materialak inprimatzean ohe beroaren tenperatura egoki igotzeak okertzeko probabilitatea murriztu dezakeela?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Geruza-altuera txikiegia da.\n" +#~ "min_layer_height baliora ezarriko da\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Geruza-altuerak Inprimagailuaren ezarpenak -> Estrusorea -> Geruza-altueraren mugak ataleko muga gainditzen du; horrek inprimatze-kalitateko arazoak sor ditzake." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Doitu automatikoki ezarritako barrutira?\n" + +#~ msgid "Head diameter" +#~ msgstr "Buruaren diametroa" + #~ msgid "Print order within a single layer." #~ msgstr "Geruza bakarreko inprimatze-ordena." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 1257994f16..5aeed754f0 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -4643,6 +4643,20 @@ msgstr "La température actuelle du caisson est supérieure à la température d msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La température minimale du caisson (%d℃) est supérieure à la température cible du caisson (%d℃). La valeur minimale est le seuil à partir duquel l’impression démarre tandis que le caisson continue de chauffer vers la cible ; elle ne doit donc pas la dépasser. Elle sera limitée à la cible." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Ajuster" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4762,6 +4776,12 @@ msgstr "" "Oui - Activer le générateur de parois Arachne\n" "Non - Désactiver le générateur de parois Arachne et définir le mode [Déplacement] de la surface irrégulière" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Largeur de la bordure" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Le mode spirale ne fonctionne que lorsque le nombre de parois est 1, le support est désactivé, la détection d'agglomération par sondage est désactivée, les couches supérieures sont à 0, la densité de remplissage clairsemé est à 0 et le type de timelapse est traditionnel." @@ -4777,6 +4797,9 @@ msgstr "" "Oui - Modifiez ces paramètres et activez automatiquement le mode spirale/vase\n" "Non - Annuler l'activation du mode spirale" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Impression" @@ -5016,6 +5039,12 @@ msgstr "Échec de la génération du G-code de calibration" msgid "Calibration error" msgstr "Erreur de la calibration" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Réseau indisponible" @@ -5228,9 +5257,6 @@ msgstr "Motif invalide. Utilisez N, N#K, ou une liste séparée par des virgules msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Format invalide. Format vectoriel attendu : \"%1%\"" -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Agents système" @@ -5871,7 +5897,7 @@ msgstr "Volume :" msgid "Size:" msgstr "Taille :" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Des conflits de chemins G-code ont été trouvés au niveau de la couche %d, z = %.2lfmm. Veuillez séparer davantage les objets en conflit (%s <-> %s)." @@ -6052,6 +6078,9 @@ msgstr "Multi-appareils" msgid "Project" msgstr "Projet" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Oui" @@ -6179,6 +6208,12 @@ msgstr "Enregistrer le projet sous" msgid "Save current project as" msgstr "Enregistrer le projet actuel sous" +msgid "Publish" +msgstr "Publier" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7964,6 +7999,12 @@ msgstr "Veuillez vous assurer que les G-codes de ces préréglages sont sûrs af msgid "Customized Preset" msgstr "Préréglage personnalisé" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Le nom des composants dans le fichier STEP n'est pas au format UTF-8 !" @@ -8120,19 +8161,19 @@ msgstr "Le répertoire pour le remplacement n'a pas été sélectionné" msgid "Replaced with 3D files from directory:\n" msgstr "Remplacé par des fichiers 3D depuis le répertoire :\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Ignoré %s : même fichier.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Ignoré %s : le fichier n'existe pas.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Ignoré %s : échec du remplacement.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Remplacé %s.\n" @@ -8341,6 +8382,14 @@ msgstr "Enregistrer le fichier découpé sous :" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Le fichier %s a été envoyé vers l'espace de stockage de l'imprimante et peut être visualisé sur l'imprimante." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Le type de buse n'est pas défini. Veuillez définir la buse et réessayer." @@ -9211,6 +9260,14 @@ msgstr "Afficher les préréglages non pris en charge" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Affiche les préréglages incompatibles ou non pris en charge dans les listes déroulantes d’imprimantes et de filaments. Ces préréglages ne peuvent pas être sélectionnés." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Fonctionnalités expérimentales" @@ -9444,9 +9501,6 @@ msgstr "Accéder à la page internet de publication des modèles" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Remarque : La préparation peut prendre plusieurs minutes. Veuillez patienter." -msgid "Publish" -msgstr "Publier" - msgid "Publish was canceled" msgstr "La publication a été annulée" @@ -9462,6 +9516,21 @@ msgstr "Téléversement des données" msgid "Jump to webpage" msgstr "Ouvrir la page internet" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Enregistrer %s sous" @@ -9472,9 +9541,21 @@ msgstr "Préréglage utilisateur" msgid "Preset Inside Project" msgstr "Préréglage intégré au projet" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Détacher du parent" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Le nom n'est pas disponible." @@ -10211,22 +10292,6 @@ msgstr "Voulez-vous vraiment activer cette option ?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Les motifs de remplissage sont généralement conçus pour gérer la rotation automatiquement afin d'assurer une impression correcte et d'atteindre les effets souhaités (ex. : Gyroïde, Cubique). La rotation du motif de remplissage clairsemé actuel peut entraîner un support insuffisant. Veuillez procéder avec précaution et vérifier soigneusement tout problème d'impression potentiel. Voulez-vous vraiment activer cette option ?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"La hauteur de couche est trop faible.\n" -"Elle sera définie à min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." - -msgid "Adjust to the set range automatically?\n" -msgstr "S’ajuster automatiquement à la plage définie ?\n" - -msgid "Adjust" -msgstr "Ajuster" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Fonction expérimentale : Rétracter et couper le filament à une plus grande distance lors des changements de filament afin de minimiser le rinçage. Bien que cela puisse réduire considérablement le rinçage, cela peut également augmenter le risque de bouchage des buses ou d’autres complications d’impression." @@ -10422,6 +10487,9 @@ msgstr "Mots clés réservés trouvés" msgid "Setting Overrides" msgstr "Forçage des réglages" +msgid "Retraction when switching material" +msgstr "Rétraction lors du changement de matériau" + msgid "Basic information" msgstr "Informations de base" @@ -10548,6 +10616,12 @@ msgstr "Profils de traitement compatibles" msgid "Printable space" msgstr "Espace imprimable" +msgid "Printer Agent" +msgstr "Agent d'imprimante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10673,9 +10747,6 @@ msgstr "Limites de hauteur de couche" msgid "Z-Hop" msgstr "Saut en Z" -msgid "Retraction when switching material" -msgstr "Rétraction lors du changement de matériau" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12010,6 +12081,9 @@ msgstr " est trop proche d'une zone d'exclusion. Cela va entraîner des collisio msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " est trop proche de la zone de détection d'agglomération, et des collisions seront causées.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Les températures de buse sélectionnées sont incompatibles. La température de buse de chaque filament doit se situer dans la plage de température de buse recommandée des autres filaments. Sinon, un bouchage de la buse ou des dommages à l’imprimante peuvent survenir." @@ -12323,9 +12397,6 @@ msgstr "Utiliser le 3MF au lieu du G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Activez ceci si l’imprimante accepte un fichier 3MF comme tâche d’impression. Lorsque cette option est activée, Orca Slicer envoie le fichier découpé au format .gcode.3mf au lieu d’un simple fichier .gcode." -msgid "Printer Agent" -msgstr "Agent d'imprimante" - msgid "Select the network agent implementation for printer communication." msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante." @@ -13000,9 +13071,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vitesse des ponts internes. Si la valeur est exprimée en pourcentage, elle sera calculée sur la base de la vitesse du pont. La valeur par défaut est 150%." -msgid "Brim width" -msgstr "Largeur de la bordure" - msgid "This is the distance from the model to the outermost brim line." msgstr "Distance du modèle à la ligne de bord la plus externe" @@ -13082,6 +13150,12 @@ msgstr "" "La géométrie sera décimée avant de détecter les angles vifs. Ce paramètre indique la longueur minimale de l’écart pour la décimation.\n" "0 pour désactiver" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "machine à compatibilité ascendante" @@ -14236,6 +14310,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroïde" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Il s'agit de l'accélération de la surface supérieure du remplissage. Utiliser une valeur plus petite pourrait améliorer la qualité de la surface supérieure" @@ -14774,6 +14854,12 @@ msgstr "Avec quel type de G-code l'imprimante est-elle compatible." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Imprimante à pellets" @@ -15821,6 +15907,12 @@ msgstr "Rétraction longue lors du changement d'extrudeur" msgid "Retraction distance when extruder change" msgstr "Distance de rétraction lors du changement d'extrudeur" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Hauteur du saut en Z" @@ -15914,6 +16006,9 @@ msgstr "Longueur supplémentaire" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Lorsque la rétraction est compensée après le mouvement de déplacement, l’extrudeuse poussera cette quantité supplémentaire de filament. Ce paramètre est rarement nécessaire." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Lorsque la rétraction est compensée après le changement d’outil, l’extrudeur poussera cette quantité supplémentaire de filament." @@ -16326,6 +16421,12 @@ msgstr "Changement d’outil sur la tour d’essuyage" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Force la tête d’outil à se déplacer vers la tour d’essuyage avant d’émettre la commande de changement d’outil (Tx). Pertinent uniquement pour les imprimantes multi-extrudeurs (à têtes d’outil multiples) utilisant une tour d’essuyage de type 2. Par défaut, Orca omet ce déplacement sur les machines à têtes d’outil multiples car le firmware gère le changement de tête, ce qui peut entraîner l’émission de la commande Tx au-dessus de la pièce imprimée. Activez cette option si vous préférez que le changement d’outil soit toujours émis au-dessus de la tour d’essuyage." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Pas de couches éparses (beta)" @@ -19542,9 +19643,6 @@ msgstr "Imprimante Physique" msgid "Print Host upload" msgstr "Envoi vers l’imprimante hôte" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Sélectionner l'implémentation de l'agent réseau pour la communication avec l'imprimante. Les agents disponibles sont enregistrés au démarrage." - msgid "Select a Flashforge printer" msgstr "Sélectionner une imprimante Flashforge" @@ -20392,9 +20490,6 @@ msgstr "Un événement inattendu s’est produit lors de la connexion, veuillez msgid "User canceled." msgstr "L’utilisateur a annulé." -msgid "Head diameter" -msgstr "Diamètre de la tête" - msgid "Max angle" msgstr "Angle maximal" @@ -21176,6 +21271,22 @@ msgstr "" "Éviter la déformation\n" "Saviez-vous que lors de l’impression de matériaux susceptibles de se déformer, tels que l’ABS, une augmentation appropriée de la température du plateau chauffant peut réduire la probabilité de déformation?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "La hauteur de couche est trop faible.\n" +#~ "Elle sera définie à min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "La hauteur de la couche dépasse la limite fixée dans Paramètres de l’imprimante -> Extrudeur -> Limites de la hauteur de la couche, ce qui peut entraîner des problèmes de qualité d’impression." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "S’ajuster automatiquement à la plage définie ?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diamètre de la tête" + #~ msgid "Print order within a single layer." #~ msgstr "Ordre d’impression au sein d’une même couche" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 98cd987512..6c9c5669d8 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4739,6 +4739,20 @@ msgstr "A kamra aktuális hőmérséklete magasabb az anyag biztonságos hőmér msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A minimális kamrahőmérséklet (%d℃) magasabb a cél kamrahőmérsékletnél (%d℃). A minimális érték az a küszöb, amelynél a nyomtatás elindul, miközben a kamra tovább melegszik a célérték felé, ezért nem haladhatja meg azt. Az érték a célértékre lesz korlátozva." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Módosítás" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4858,6 +4872,12 @@ msgstr "" "Igen - Engedélyezd az Arachne falgenerátort\n" "Nem - Tiltsd le az Arachne falgenerátort, majd állítsd a barázdált felületet [Eltolás] módra" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Perem szélessége" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "A spirál mód csak akkor működik, ha a falhurkok száma 1, a támasz és a szondázásos csomósodásészlelés ki van kapcsolva, a felső héjrétegek száma 0, a kitöltés sűrűsége 0, a Timelapse típusa pedig hagyományos." @@ -4873,6 +4893,10 @@ msgstr "" "Igen - Módosítsd ezeket a beállításokat, és automatikusan kapcsold be a spirál módot\n" "Nem - Most ne kapcsold be a spirál módot" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Nyomtatás" @@ -5112,6 +5136,12 @@ msgstr "Nem sikerült létrehozni a kalibrációs G-kódot" msgid "Calibration error" msgstr "Kalibrációs hiba" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "A hálózat nem érhető el" @@ -5324,10 +5354,6 @@ msgstr "Érvénytelen minta. Használj N, N#K formátumot, vagy vesszővel elvá msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Érvénytelen formátum. Elvárt vektor formátum: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Rendszerügynökök" @@ -5971,7 +5997,7 @@ msgstr "Térfogat:" msgid "Size:" msgstr "Méret:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "G-kód útvonalütközés található a(z) %d. rétegen, Z = %.2lfmm. Helyezd távolabb egymástól az ütköző objektumokat (%s <-> %s)." @@ -6153,6 +6179,9 @@ msgstr "Több eszköz" msgid "Project" msgstr "Projekt" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Igen" @@ -6281,6 +6310,12 @@ msgstr "Projekt mentése másként" msgid "Save current project as" msgstr "Jelenlegi projekt mentése másként" +msgid "Publish" +msgstr "Közzététel" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8079,6 +8114,12 @@ msgstr "Kérlek, győződj meg arról, hogy a beállításokban található G-k msgid "Customized Preset" msgstr "Egyedi beállítás" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "A STEP fájlon belüli komponens neve nem UTF-8 formátumban van!" @@ -8244,19 +8285,19 @@ msgstr "A cseréhez nem lett mappa kiválasztva" msgid "Replaced with 3D files from directory:\n" msgstr "Cserélve a mappából származó 3D fájlokra:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s kihagyva: azonos fájl.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s kihagyva: a fájl nem létezik.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s kihagyva: a csere sikertelen.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔%s lecserélve.\n" @@ -8467,6 +8508,14 @@ msgstr "Szeletelt fájl mentése mint:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "A(z) %s fájlt elküldtük a nyomtató tárhelyére. A fájl a nyomtatón tekinthető meg." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "A fúvókatípus nincs beállítva. Állítsd be a fúvókát, majd próbáld újra." @@ -9362,6 +9411,14 @@ msgstr "Nem támogatott beállítások megjelenítése" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Megjeleníti a nem kompatibilis vagy nem támogatott beállításokat a nyomtató- és filamentlegördülő listákban. Ezek a beállítások nem választhatók ki." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Kísérleti funkciók" @@ -9604,9 +9661,6 @@ msgstr "Ugrás a modell közzététele weboldalra" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Megjegyzés: Az előkészítés több percig is eltarthat. Kérlek, várj." -msgid "Publish" -msgstr "Közzététel" - msgid "Publish was canceled" msgstr "A közzététel törlésre került" @@ -9622,6 +9676,21 @@ msgstr "Adatok feltöltése" msgid "Jump to webpage" msgstr "Ugrás a weboldalra" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s mentése" @@ -9632,9 +9701,21 @@ msgstr "Felhasználói beállítás" msgid "Preset Inside Project" msgstr "Projekt a beállításon belül" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Leválasztás a szülőről" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "A név nem elérhető." @@ -10376,22 +10457,6 @@ msgstr "Biztos, hogy engedélyezed ezt az opciót?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "A kitöltési minták általában maguk kezelik a forgatást a megfelelő nyomtatás és a kívánt hatás elérése érdekében (pl. Gyroid, Cubic). A jelenlegi kitöltési minta elforgatása elégtelen alátámasztáshoz vezethet. Kérlek, járj el körültekintően, és alaposan ellenőrizd a lehetséges nyomtatási problémákat. Biztos, hogy engedélyezed ezt a beállítást?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A rétegmagasság túl kicsi.\n" -"A rendszer a min_layer_height értékre állítja.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." - -msgid "Adjust to the set range automatically?\n" -msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" - -msgid "Adjust" -msgstr "Módosítás" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Kísérleti funkció: Filamentcsere közben nagyobb távolságon történő visszahúzás és elvágás az öblítés minimalizálása érdekében. Bár ez jelentősen csökkentheti az öblítés mértékét, növelheti a fúvóka eltömődésének vagy más nyomtatási problémák kockázatát." @@ -10587,6 +10652,9 @@ msgstr "Foglalt kulcsszavakat találtunk" msgid "Setting Overrides" msgstr "Beállítások felülbírálása" +msgid "Retraction when switching material" +msgstr "Visszahúzás anyagváltáskor" + msgid "Basic information" msgstr "Alapinformációk" @@ -10720,6 +10788,12 @@ msgstr "Kompatibilis folyamatprofilok" msgid "Printable space" msgstr "Nyomtatási terület" +msgid "Printer Agent" +msgstr "Nyomtatóügynök" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10845,9 +10919,6 @@ msgstr "Rétegmagasság limitek" msgid "Z-Hop" msgstr "Z-emelés" -msgid "Retraction when switching material" -msgstr "Visszahúzás anyagváltáskor" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12200,6 +12271,9 @@ msgstr " túl közel van a tiltott területhez, a nyomtatás során előfordulha msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " túl közel van a csomósodásészlelési területhez, és ez ütközést fog okozni.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "A kiválasztott fúvóka hőmérsékletek nem kompatibilisek. Mindegyik filament fúvóka hőmérsékletének a többi filament ajánlott fúvóka hőmérsékleti tartományába kell esnie. Ellenkező esetben a fúvóka eltömődhet vagy a nyomtató megsérülhet." @@ -12530,9 +12604,6 @@ msgstr "3MF használata G-kód helyett" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Kapcsold be, ha a nyomtató 3MF fájlt fogad el nyomtatási feladatként. Bekapcsolva az Orca Slicer a szeletelt fájlt .gcode.3mf formátumban küldi el egyszerű .gcode fájl helyett." -msgid "Printer Agent" -msgstr "Nyomtatóügynök" - msgid "Select the network agent implementation for printer communication." msgstr "Válaszd ki a nyomtató kommunikációjához használt hálózati ügynök implementációját." @@ -13220,9 +13291,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "A belső hidak sebessége. Ha az érték százalékban van megadva, a bridge_speed alapján lesz kiszámítva. Az alapértelmezett érték 150%." -msgid "Brim width" -msgstr "Perem szélessége" - msgid "This is the distance from the model to the outermost brim line." msgstr "A modell és a legkülső peremvonal közötti távolság" @@ -13302,6 +13370,12 @@ msgstr "" "Az éles szögek észlelése előtt a geometria egyszerűsítve lesz. Ez a paraméter a leegyszerűsítésnél figyelembe vett eltérés minimális hosszát adja meg.\n" "0 értékkel kikapcsolható." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "felfelé kompatibilis gép" @@ -14475,6 +14549,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "A felső felületi kitöltés gyorsulása. Alacsonyabb érték használata javíthatja a felső felület minőségét" @@ -15017,6 +15097,12 @@ msgstr "Milyen G-kóddal kompatibilis a nyomtató." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Granulátumos módosított nyomtató" @@ -16079,6 +16165,12 @@ msgstr "Hosszú visszahúzás extruderváltáskor" msgid "Retraction distance when extruder change" msgstr "Visszahúzási távolság extruderváltáskor" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z-emelés magassága" @@ -16172,6 +16264,9 @@ msgstr "Extra hossz újraindításkor" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Amikor a visszahúzás kompenzálásra kerül utazási mozgás után, az extruder ezt a további szálmennyiséget nyomja előre. Erre a beállításra ritkán van szükség." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Amikor a visszahúzás kompenzálásra kerül szerszámváltás után, az extruder ezt a további szálmennyiséget nyomja előre." @@ -16588,6 +16683,12 @@ msgstr "Szerszámcsere a törlőtoronyban" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "A szerszámcsere parancs (Tx) kiadása előtt a törlőtoronyhoz mozgatja a szerszámfejet. Csak a 2-es típusú törlőtornyot használó többextruderes (több szerszámfejes) nyomtatóknál van jelentősége. Az Orca alapértelmezés szerint kihagyja ezt a mozgást a több szerszámfejes gépeknél, mert a fejcserét a firmware kezeli. Emiatt azonban előfordulhat, hogy a Tx parancsot a nyomtatott tárgy felett adja ki. Kapcsold be ezt a beállítást, ha azt szeretnéd, hogy a szerszámcsere mindig a törlőtorony felett történjen." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Nincsenek ritka rétegek (béta)" @@ -19847,9 +19948,6 @@ msgstr "Fizikai nyomtató" msgid "Print Host upload" msgstr "Feltöltés a nyomtatóra" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Válaszd ki a nyomtatóval való kommunikációhoz használt hálózati ügynököt. Az elérhető ügynököket indításkor regisztrálja a rendszer." - # AI Translated msgid "Select a Flashforge printer" msgstr "Válassz egy Flashforge nyomtatót" @@ -20791,9 +20889,6 @@ msgstr "Bejelentkezés közben váratlan hiba történt, próbáld újra." msgid "User canceled." msgstr "Felhasználó által megszakítva." -msgid "Head diameter" -msgstr "Fej átmérő" - msgid "Max angle" msgstr "Maximális szög" @@ -21607,6 +21702,22 @@ msgstr "" "Kunkorodás elkerülése\n" "Tudtad, hogy a kunkorodásra hajlamos anyagok (például ABS) nyomtatásakor az asztal hőmérsékletének növelése csökkentheti a kunkorodás valószínűségét?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A rétegmagasság túl kicsi.\n" +#~ "A rendszer a min_layer_height értékre állítja.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A rétegmagasság meghaladja a Nyomtatóbeállítások -> Extruder -> Rétegmagasság limitek menüpontban megadott értéket, ez minőségbeli problémákat okozhat a nyomtatás során." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Szeretnéd az értéket automatikusan a beállított tartományhoz igazítani?\n" + +#~ msgid "Head diameter" +#~ msgstr "Fej átmérő" + #~ msgid "Print order within a single layer." #~ msgstr "Nyomtatási sorrend egyetlen rétegen belül." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 3c43178102..8533b8b39f 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4741,6 +4741,20 @@ msgstr "L'attuale temperatura della camera è superiore alla temperatura di sicu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "La temperatura minima della camera (%d℃) è superiore alla temperatura target della camera (%d℃). Il valore minimo è la soglia alla quale inizia la stampa mentre la camera continua a riscaldarsi verso il target, quindi non dovrebbe superarlo. Verrà limitato al valore target." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Regola" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4860,6 +4874,12 @@ msgstr "" "Sì - Abilita generatore di pareti Arachne\n" "No - Disabilita generatore di pareti Arachne e imposta la modalità [Spostamento] della Superficie ruvida" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Larghezza tesa" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "La modalità spirale funziona solo quando i perimetri sono 1, il supporto è disabilitato, il rilevamento degli ammassi tramite sondaggio è disabilitato, gli strati superiori della shell sono 0, la densità del riempimento sparso è 0 e il tipo di timelapse è tradizionale." @@ -4875,6 +4895,9 @@ msgstr "" "Sì - Modifica queste impostazioni ed abilita la modalità spirale automaticamente\n" "No - Annulla l'attivazione della modalità a spirale" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Stampa" @@ -5114,6 +5137,12 @@ msgstr "Impossibile generare G-code di calibrazione" msgid "Calibration error" msgstr "Errore di calibrazione" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Rete non disponibile" @@ -5326,9 +5355,6 @@ msgstr "Schema non valido. Utilizzare N, N#K o un elenco separato da virgole con msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato non valido. Formato vettoriale previsto: \"%1%\"" -msgid "N/A" -msgstr "N/D" - # AI Translated msgid "System agents" msgstr "Agenti di sistema" @@ -5973,7 +5999,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Dimensione:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Sono stati trovati conflitti di percorsi nel G-code sullo strato %d, Z = %.2lfmm. Si prega di separare gli oggetti in conflitto (%s <-> %s)." @@ -6154,6 +6180,9 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Progetto" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Sì" @@ -6283,6 +6312,12 @@ msgstr "Salva progetto con nome" msgid "Save current project as" msgstr "Salva progetto corrente con nome" +msgid "Publish" +msgstr "Pubblica" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importa 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8083,6 +8118,12 @@ msgstr "Si prega di confermare che i G-code all'interno di questi profili sono s msgid "Customized Preset" msgstr "Profilo personalizzato" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Il nome dei componenti all'interno del file STEP non è in formato UTF8!" @@ -8244,19 +8285,19 @@ msgstr "La directory per la sostituzione non è stata selezionata" msgid "Replaced with 3D files from directory:\n" msgstr "Sostituito con file 3D dalla directory:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Saltato %s: stesso file.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Saltato %s: il file non esiste.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Saltato %s: sostituzione fallita.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Sostituito %s.\n" @@ -8466,6 +8507,14 @@ msgstr "Salva file elaborato con nome:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Il file %s è stato inviato alla memoria della stampante e può essere visualizzato da lì." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Il tipo di ugello non è impostato. Impostare l'ugello e riprovare." @@ -9381,6 +9430,14 @@ msgstr "Mostra i profili non supportati" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Mostra i profili incompatibili/non supportati negli elenchi a discesa di stampante e filamento. Questi profili non possono essere selezionati." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Funzionalità sperimentali" @@ -9622,9 +9679,6 @@ msgstr "Vai alla pagina web di pubblicazione del modello" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: la preparazione può richiedere alcuni minuti. Si prega di avere pazienza." -msgid "Publish" -msgstr "Pubblica" - msgid "Publish was canceled" msgstr "La pubblicazione è stata annullata" @@ -9640,6 +9694,21 @@ msgstr "Caricamento dati" msgid "Jump to webpage" msgstr "Vai alla pagina web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Salva %s con nome" @@ -9650,9 +9719,21 @@ msgstr "Profilo utente" msgid "Preset Inside Project" msgstr "Profilo interno al progetto" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Scollega dal genitore" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Nome non disponibile." @@ -10392,22 +10473,6 @@ msgstr "Sei sicuro di voler abilitare questa opzione?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "I pattern di riempimento sono generalmente progettati per gestire automaticamente la rotazione per garantire una stampa corretta e ottenere gli effetti desiderati (ad es. Gyroid, Cubico). La rotazione del pattern di riempimento sparso corrente potrebbe portare a un supporto insufficiente. Procedere con cautela e verificare accuratamente eventuali problemi di stampa. Sei sicuro di voler abilitare questa opzione?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"L'altezza dello strato è troppo piccola.\n" -"Sarà impostato su min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." - -msgid "Adjust to the set range automatically?\n" -msgstr "Regolare automaticamente l'intervallo impostato?\n" - -msgid "Adjust" -msgstr "Regola" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funzionalità sperimentale: ritrazione e taglio del filamento a una distanza maggiore durante i cambi di filamento per ridurre al minimo lo spurgo. Sebbene possa ridurre notevolmente lo spurgo, può anche aumentare il rischio di intasamento degli ugelli o di altre complicazioni di stampa." @@ -10603,6 +10668,9 @@ msgstr "Parole chiave riservate trovate" msgid "Setting Overrides" msgstr "Sovrascrivi impostazioni" +msgid "Retraction when switching material" +msgstr "Retrazione quando si cambia materiale" + msgid "Basic information" msgstr "Informazioni di base" @@ -10734,6 +10802,12 @@ msgstr "Profili di processo compatibili" msgid "Printable space" msgstr "Spazio di stampa" +msgid "Printer Agent" +msgstr "Agente stampante" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10859,9 +10933,6 @@ msgstr "Limiti altezza strati" msgid "Z-Hop" msgstr "Sollevamento Z" -msgid "Retraction when switching material" -msgstr "Retrazione quando si cambia materiale" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12221,6 +12292,9 @@ msgstr " è troppo vicino all'area di esclusione e si verificheranno collisioni. msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " è troppo vicino all'area di rilevamento ammassi e verranno causate collisioni.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Le temperature degli ugelli selezionate sono incompatibili. La temperatura dell'ugello per ciascun filamento deve rientrare nell'intervallo di temperatura consigliato per gli altri filamenti. In caso contrario, potrebbero verificarsi ostruzioni degli ugelli o danni alla stampante." @@ -12550,9 +12624,6 @@ msgstr "Usa 3MF invece di G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Abilita questa opzione se la stampante accetta un file 3MF come processo di stampa. Quando è abilitata, Orca Slicer invia il file elaborato come .gcode.3mf, invece di un semplice file .gcode." -msgid "Printer Agent" -msgstr "Agente stampante" - msgid "Select the network agent implementation for printer communication." msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante." @@ -13239,9 +13310,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocità dei ponti interni. Se il valore è espresso in percentuale, verrà calcolato in base a bridge_speed. Il valore predefinito è 150%." -msgid "Brim width" -msgstr "Larghezza tesa" - msgid "This is the distance from the model to the outermost brim line." msgstr "Questa è la distanza tra il modello e la linea più esterna della tesa." @@ -13321,6 +13389,12 @@ msgstr "" "La geometria verrà decimata prima di rilevare gli spigoli vivi. Questo parametro indica la lunghezza minima dello scostamento per la decimazione.\n" "0 per disattivare." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "macchina compatibile con versioni successive" @@ -14495,6 +14569,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Accelerazione del riempimento della superficie superiore. L'utilizzo di un valore inferiore può migliorare la qualità della superficie superiore." @@ -15039,6 +15119,12 @@ msgstr "Con quale tipo di G-code la stampante è compatibile." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Stampante modificata per granuli" @@ -16098,6 +16184,12 @@ msgstr "Retrazione lunga al cambio estrusore" msgid "Retraction distance when extruder change" msgstr "Distanza di retrazione al cambio estrusore" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Altezza sollevamento Z" @@ -16195,6 +16287,9 @@ msgstr "Lunghezza aggiuntiva in ripresa" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando la retrazione è compensata dopo uno spostamento, l'estrusore espelle questa quantità aggiuntiva di filamento. Questa impostazione è raramente necessaria." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando la retrazione è compensata dopo un cambio di testina, l'estrusore espelle questa quantità aggiuntiva di filamento." @@ -16612,6 +16707,12 @@ msgstr "Cambio utensile sulla torre di spurgo" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Forza la testa di stampa a spostarsi sulla torre di spurgo prima di emettere il comando di cambio utensile (Tx). Rilevante solo per le stampanti multi-estrusore (multi-testa) che utilizzano una torre di spurgo di Tipo 2. Per impostazione predefinita Orca salta lo spostamento sulle macchine multi-testa perché il firmware gestisce il cambio della testa, il che può far sì che il comando Tx venga emesso sopra la parte stampata. Abilita questa opzione se desideri che il cambio utensile venga sempre emesso sopra la torre di spurgo." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Nessuno strato sparso (beta)" @@ -19865,9 +19966,6 @@ msgstr "Stampante fisica" msgid "Print Host upload" msgstr "Caricamento host di stampa" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selezionare l'implementazione dell'agente di rete per la comunicazione con la stampante. Gli agenti disponibili vengono registrati all'avvio." - # AI Translated msgid "Select a Flashforge printer" msgstr "Seleziona una stampante Flashforge" @@ -20810,9 +20908,6 @@ msgstr "Si è verificato un problema imprevisto durante il tentativo di accesso. msgid "User canceled." msgstr "Utente rimosso." -msgid "Head diameter" -msgstr "Diametro testa" - msgid "Max angle" msgstr "Angolo massimo" @@ -21631,6 +21726,22 @@ msgstr "" "Evita le deformazioni\n" "Sapevi che quando si stampano materiali soggetti a deformazioni come l'ABS, aumentare in modo appropriato la temperatura del piano riscaldato può ridurre la probabilità di deformazione?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "L'altezza dello strato è troppo piccola.\n" +#~ "Sarà impostato su min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "L'altezza dello strato supera il limite in Impostazioni stampante -> Estrusore -> Limiti Altezza Strato. Ciò potrebbe causare problemi di qualità di stampa." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Regolare automaticamente l'intervallo impostato?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diametro testa" + #~ msgid "Print order within a single layer." #~ msgstr "Ordine di stampa all'interno di un singolo strato." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 0d9f044060..2c28d2b166 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4750,6 +4750,20 @@ msgstr "現在のチャンバー温度が材料の安全温度を超えていま msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低庫内温度 (%d℃) が目標庫内温度 (%d℃) を上回っています。最低値は、チャンバーが目標に向けて加熱を続けながら印刷を開始するしきい値であるため、目標値を超えてはいけません。値は目標値に制限されます。" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "調整" + # AI Translated msgid "" "Layer height too small\n" @@ -4873,6 +4887,12 @@ msgstr "" "はい - Arachneウォールジェネレーターを有効にする\n" "いいえ - Arachneウォールジェネレーターを無効にし、ファジースキンを[変位]モードに設定する" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "ブリム幅" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "スパイラルモードは壁ループが1、サポートが無効、プロービングによるクランピング検出が無効、上部シェルレイヤーが0、スパースインフィル密度が0、タイムラプスタイプがトラディショナルの場合のみ機能します。" @@ -4888,6 +4908,10 @@ msgstr "" "はい - 変更して、スパイラルモードを有効にします\n" "いいえ - 変更せず、スパイラルモードを有効しません" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "造形中" @@ -5127,6 +5151,12 @@ msgstr "キャリブレーションG-codeの生成に失敗しました" msgid "Calibration error" msgstr "キャリブレーションエラー" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "ネットワークが利用できません" @@ -5338,10 +5368,6 @@ msgstr "無効なパターンです。N、N#K、またはオプション#K付き msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "無効なフォーマット、%1%であるはずです。" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "システムエージェント" @@ -5988,7 +6014,7 @@ msgstr "ボリューム" msgid "Size:" msgstr "サイズ:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "レイヤー%d、Z = %.2lfmmでG-codeパスの衝突が検出されました。衝突するオブジェクトをもっと離してください(%s <-> %s)。" @@ -6164,6 +6190,9 @@ msgstr "マルチデバイス" msgid "Project" msgstr "プロジェクト" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "はい" @@ -6292,6 +6321,12 @@ msgstr "プロジェクトを名前を付けて保存" msgid "Save current project as" msgstr "プロジェクトを名前を付けて保存" +msgid "Publish" +msgstr "公開する" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMFをインポート" @@ -8097,6 +8132,12 @@ msgstr "これらのプリセット内のG-codeがマシンに損傷を与えな msgid "Customized Preset" msgstr "カスタマイズされたプリセット" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "ファイルのエンコーディング方式はUTF8形式ではありません" @@ -8262,19 +8303,19 @@ msgstr "置換用のディレクトリが選択されていません" msgid "Replaced with 3D files from directory:\n" msgstr "ディレクトリの3Dファイルで置換しました:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ スキップ %s: 同一ファイル。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ スキップ %s: ファイルが存在しません。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ スキップ %s: 置換に失敗しました。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 置換しました %s。\n" @@ -8485,6 +8526,14 @@ msgstr "名前を付けて保存:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%sを送信しました、プリンターにて確認できます" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。" @@ -9404,6 +9453,14 @@ msgstr "非対応のプリセットを表示" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "プリンターとフィラメントのドロップダウンリストに、互換性のない/非対応のプリセットを表示します。これらのプリセットは選択できません。" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "実験的機能" @@ -9644,9 +9701,6 @@ msgstr "モデル公開ページに移動" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "注意: 準備するには数分かかる場合があります、暫くお待ち下さい。" -msgid "Publish" -msgstr "公開する" - msgid "Publish was canceled" msgstr "公開は取り消しました" @@ -9662,6 +9716,21 @@ msgstr "データをアップロード中" msgid "Jump to webpage" msgstr "ウェブページに移動" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%sを名前つけて保存" @@ -9672,9 +9741,21 @@ msgstr "ユーザープリセット" msgid "Preset Inside Project" msgstr "プロジェクト プリセット" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "親から分離" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "名称は使用できません" @@ -10416,22 +10497,6 @@ msgstr "このオプションを有効にしてもよろしいですか?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "インフィルパターンは通常、適切な印刷と意図した効果を確保するために回転を自動的に処理するように設計されています(例: ジャイロイド、キュービック)。現在のスパースインフィルパターンを回転させると、サポートが不十分になる可能性があります。慎重に進め、潜在的な印刷問題を十分に確認してください。このオプションを有効にしてもよろしいですか?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"レイヤー高さが小さすぎます。\n" -"min_layer_heightに設定されます\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" - -msgid "Adjust to the set range automatically?\n" -msgstr "設定範囲に自動調整しますか?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "実験的機能: フィラメント交換時により長い距離でフィラメントをリトラクト・カットしてフラッシュを最小化します。フラッシュを大幅に削減できますが、ノズル詰まりやその他の印刷問題のリスクが高まる可能性もあります。" @@ -10621,6 +10686,9 @@ msgstr "保留キーワードが見つかりました" msgid "Setting Overrides" msgstr "上書き設定" +msgid "Retraction when switching material" +msgstr "素材変更時のリトラクション" + msgid "Basic information" msgstr "基本情報" @@ -10751,6 +10819,12 @@ msgstr "互換性のあるプロセスプロファイル" msgid "Printable space" msgstr "造形可能領域" +msgid "Printer Agent" +msgstr "プリンターエージェント" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10877,9 +10951,6 @@ msgstr "積層ピッチの制限" msgid "Z-Hop" msgstr "Z-ホップ" -msgid "Retraction when switching material" -msgstr "素材変更時のリトラクション" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12258,6 +12329,9 @@ msgstr " は除外エリアに近すぎるため、衝突が発生します。\n msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " がクランピング検出エリアに近すぎ、衝突が発生します。\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "選択したノズル温度に互換性がありません。各フィラメントのノズル温度は、他のフィラメントの推奨ノズル温度範囲内に収まる必要があります。そうでない場合、ノズル詰まりやプリンターの損傷が発生する可能性があります。" @@ -12599,9 +12673,6 @@ msgstr "G-codeの代わりに3MFを使用" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "プリンターが印刷ジョブとして3MFファイルを受け付ける場合に有効にします。有効にすると、Orca Slicerはスライス済みファイルを通常の.gcodeファイルではなく.gcode.3mfとして送信します。" -msgid "Printer Agent" -msgstr "プリンターエージェント" - msgid "Select the network agent implementation for printer communication." msgstr "プリンター通信用のネットワークエージェント実装を選択します。" @@ -13320,9 +13391,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部ブリッジの速度です。値を%で指定した場合、bridge_speedを基準に計算されます。デフォルト値は150%です。" -msgid "Brim width" -msgstr "ブリム幅" - msgid "This is the distance from the model to the outermost brim line." msgstr "一番外側のブリム線がモデルと距離です。" @@ -13411,6 +13479,12 @@ msgstr "" "鋭角を検出する前にジオメトリが間引かれます。このパラメータは、間引きにおける偏差の最小長さを指定します。\n" "0で無効になります。" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "互換性のあるデバイス" @@ -14634,6 +14708,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ジャイロイド" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "トップ面のインフィル加速度です。遅くすると表面の仕上がりが向上させることができます" @@ -15233,6 +15313,12 @@ msgstr "プリンターが対応するG-code" msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + # AI Translated msgid "Pellet Modded Printer" msgstr "ペレット改造プリンター" @@ -16374,6 +16460,12 @@ msgstr "押出機切り替え時のロングリトラクション" msgid "Retraction distance when extruder change" msgstr "押出機切替時のリトラクション距離" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + # AI Translated msgid "Z-hop height" msgstr "Zホップの高さ" @@ -16488,6 +16580,9 @@ msgstr "再開時の追加長さ" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "移動後に引込みが補償されると、エクストルーダーはこの追加量のフィラメントを押し出します。 この設定はほとんど必要ありません。" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "ツールの交換後に吸込み分が補正されると、エクストルーダーはこの追加量のフィラメントを押し出します。" @@ -16963,6 +17058,12 @@ msgstr "ワイプタワー上でツール交換" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "ツール交換コマンド (Tx) を発行する前に、ツールヘッドを強制的にワイプタワーへ移動させます。タイプ2のワイプタワーを使用するマルチ押出機 (マルチツールヘッド) プリンターにのみ関係します。デフォルトでは、マルチツールヘッド機ではファームウェアがヘッドの交換を処理するためOrcaは移動をスキップしますが、その結果Txコマンドが造形物の上で発行される場合があります。ツール交換を常にワイプタワーの上で発行したい場合は、このオプションを有効にしてください。" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + # AI Translated msgid "No sparse layers (beta)" msgstr "スパース層なし (ベータ)" @@ -20389,9 +20490,6 @@ msgstr "実物プリンター" msgid "Print Host upload" msgstr "プリントホストのアップロード" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "プリンター通信用のネットワークエージェント実装を選択します。使用可能なエージェントは起動時に登録されます。" - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforgeプリンターを選択" @@ -21363,9 +21461,6 @@ msgstr "ログイン中に予期しない問題が発生しました。再試行 msgid "User canceled." msgstr "ユーザーがキャンセルしました。" -msgid "Head diameter" -msgstr "直径" - msgid "Max angle" msgstr "最大角度" @@ -22194,6 +22289,22 @@ msgstr "" "反りを避ける\n" "ABSのような反りやすい素材を印刷する場合、ヒートベッドの温度を適切に上げることで、反りが発生する確率を下げることができることをご存知ですか?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "レイヤー高さが小さすぎます。\n" +#~ "min_layer_heightに設定されます\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "レイヤー高さがプリンター設定 -> エクストルーダー -> レイヤー高さ制限の上限を超えています。印刷品質の問題が発生する可能性があります。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "設定範囲に自動調整しますか?\n" + +#~ msgid "Head diameter" +#~ msgstr "直径" + #~ msgid "Print order within a single layer." #~ msgstr "単一レイヤー内の印刷順序。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 5a6ac0438b..5e81f19e0d 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -4763,6 +4763,20 @@ msgstr "현재 챔버 온도가 재료의 안전 온도보다 높으므로 재 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "최소 챔버 온도(%d℃)가 목표 챔버 온도(%d℃)보다 높습니다. 최소값은 챔버가 목표 온도까지 계속 가열되는 동안 출력을 시작하는 기준값이므로 목표값을 초과해서는 안 됩니다. 이 값은 목표값으로 제한됩니다." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "조정" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4884,6 +4898,12 @@ msgstr "" "예 - 아라크네 벽 생성기 활성화\n" "아니오 - 아라크네 벽 생성기 비활성화 및 퍼지 스킨 [변위] 모드 설정" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "브림 너비" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "나선형 모드는 벽 루프가 1이고, 서포트가 비활성화되고, 프로빙에 의한 클럼핑 감지가 비활성화되고, 상단 셸 레이어가 0이고, 희소 인필 밀도가 0이고 타임랩스 유형이 전통적인 경우에만 작동합니다." @@ -4899,6 +4919,10 @@ msgstr "" "예 - 이 설정을 변경하고 나선 모드를 자동으로 활성화합니다\n" "아니오 - 이번에는 나선 모드 사용을 포기합니다" +# AI Translated +msgid "N/A" +msgstr "해당 없음" + msgid "Printing" msgstr "출력 중" @@ -5138,6 +5162,12 @@ msgstr "교정 Gcode를 생성하지 못했습니다" msgid "Calibration error" msgstr "교정 오류" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "네트워크를 사용할 수 없음" @@ -5350,10 +5380,6 @@ msgstr "잘못된 패턴입니다. N, N#K 또는 항목당 선택적 #K가 있 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "잘못된 형식입니다. 필요한 벡터 형식: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "해당 없음" - # AI Translated msgid "System agents" msgstr "시스템 에이전트" @@ -6001,7 +6027,7 @@ msgstr "용량:" msgid "Size:" msgstr "크기:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "레이어 %d, Z = %.2lf mm에서 Gcode 경로 충돌이 발견되었습니다. 충돌하는 객체를 더 멀리 분리하세요 (%s <-> %s)." @@ -6178,6 +6204,9 @@ msgstr "멀티 디바이스" msgid "Project" msgstr "프로젝트" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "예" @@ -6306,6 +6335,12 @@ msgstr "프로젝트 다른 이름으로 저장" msgid "Save current project as" msgstr "현재 프로젝트 다른 이름으로 저장" +msgid "Publish" +msgstr "게시" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF 가져오기" @@ -8115,6 +8150,12 @@ msgstr "이러한 사전 설정 내의 Gcode가 기계 손상을 방지할 수 msgid "Customized Preset" msgstr "사용자 정의 프리셋" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 파일 내부의 구성 요소 이름이 UTF8 형식이 아닙니다!" @@ -8288,22 +8329,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "다음 디렉터리의 3D 파일로 교체했습니다:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 건너뜀 %s: 동일한 파일입니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 건너뜀 %s: 파일이 존재하지 않습니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 건너뜀 %s: 교체하지 못했습니다.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s을(를) 교체했습니다.\n" @@ -8518,6 +8559,14 @@ msgstr "슬라이스 파일을 다음으로 저장:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s 파일이 프린터의 저장 공간으로 전송되었으며 프린터에서 볼 수 있습니다." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요." @@ -9491,6 +9540,14 @@ msgstr "지원되지 않는 사전 설정 표시" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "프린터 및 필라멘트 드롭다운 목록에 호환되지 않거나 지원되지 않는 사전 설정을 표시합니다. 이러한 사전 설정은 선택할 수 없습니다." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "실험적 기능" @@ -9733,9 +9790,6 @@ msgstr "모델 게시 웹 페이지로 이동" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "참고: 준비하는 데 몇 분 정도 걸릴 수 있습니다. 조금만 기다려 주십시오." -msgid "Publish" -msgstr "게시" - msgid "Publish was canceled" msgstr "게시가 취소되었습니다" @@ -9752,6 +9806,21 @@ msgstr "데이터 업로드 중" msgid "Jump to webpage" msgstr "웹 페이지로 이동" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s을(를) 다음으로 저장" @@ -9762,10 +9831,22 @@ msgstr "사용자 사전 설정" msgid "Preset Inside Project" msgstr "프로젝트 내부 사전 설정" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "상위 항목에서 분리" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "이름을 사용할 수 없습니다." @@ -10519,22 +10600,6 @@ msgstr "이 옵션을 사용하시겠습니까?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "채우기 패턴은 일반적으로 올바른 출력과 의도한 효과를 위해 회전을 자동으로 처리하도록 설계되어 있습니다(예: 자이로이드, 큐빅). 현재 드문 채우기 패턴을 회전시키면 지지력이 부족해질 수 있습니다. 신중하게 진행하고 출력 문제가 발생하지 않는지 충분히 확인하십시오. 이 옵션을 활성화하시겠습니까?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"레이어 높이가 너무 작습니다.\n" -"min_layer_height로 설정됩니다.\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." - -msgid "Adjust to the set range automatically?\n" -msgstr "설정 범위에 자동으로 맞춰지나요?\n" - -msgid "Adjust" -msgstr "조정" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "실험적 기능: 플러시를 최소화하기 위해 필라멘트 교체 중에 더 먼 거리에서 필라멘트를 집어넣고 절단합니다. 플러시를 눈에 띄게 줄일 수 있지만 노즐 막힘이나 기타 출력 문제의 위험이 높아질 수도 있습니다." @@ -10728,6 +10793,9 @@ msgstr "예약어를 찾았습니다" msgid "Setting Overrides" msgstr "설정 덮어쓰기" +msgid "Retraction when switching material" +msgstr "재료 전환 시 후퇴" + msgid "Basic information" msgstr "기본 정보" @@ -10861,6 +10929,14 @@ msgstr "호환 프로세스 사전설정" msgid "Printable space" msgstr "출력 가능 공간" +# AI Translated +msgid "Printer Agent" +msgstr "프린터 에이전트" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10993,9 +11069,6 @@ msgstr "레이어 높이 한도" msgid "Z-Hop" msgstr "Z올리기" -msgid "Retraction when switching material" -msgstr "재료 전환 시 후퇴" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12388,6 +12461,9 @@ msgstr " 이(가) 제외 영역에 너무 가깝습니다. 출력 시 충돌이 msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " 뭉침 감지 영역에 너무 가까워 충돌이 발생할 수 있습니다.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "선택한 노즐 온도가 서로 호환되지 않습니다. 각 필라멘트의 노즐 온도는 다른 필라멘트의 권장 노즐 온도 범위 안에 있어야 합니다. 그렇지 않으면 노즐 막힘이나 프린터 손상이 발생할 수 있습니다." @@ -12732,10 +12808,6 @@ msgstr "G-code 대신 3MF 사용" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "프린터가 출력 작업으로 3MF 파일을 허용하는 경우 이 옵션을 활성화하십시오. 활성화하면 Orca Slicer가 슬라이스된 파일을 일반 .gcode 파일 대신 .gcode.3mf로 전송합니다." -# AI Translated -msgid "Printer Agent" -msgstr "프린터 에이전트" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다." @@ -13446,9 +13518,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "내부 브릿지의 속도. 값을 백분율로 표현하면 bridge_speed를 기준으로 계산됩니다. 기본값은 150%입니다." -msgid "Brim width" -msgstr "브림 너비" - msgid "This is the distance from the model to the outermost brim line." msgstr "모델과 가장 바깥쪽 브림 선까지의 거리" @@ -13533,6 +13602,12 @@ msgstr "" "날카로운 각도를 감지하기 전에 형상이 무시됩니다. 이 매개변수는 무시하는 형상의 최소 길이를 나타냅니다.\n" "0으로 비활성화합니다" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "상향 호환 장치" @@ -14729,6 +14804,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "자이로이드" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "상단 표면 가속도. 낮은 값을 사용하면 상단 표면 품질이 향상될 수 있습니다" @@ -15291,6 +15372,12 @@ msgstr "프린터와 호환되는 Gcode 종류" msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "펠릿 프린터" @@ -16400,6 +16487,12 @@ msgstr "압출기 교체 시 긴 수축" msgid "Retraction distance when extruder change" msgstr "압출기 교체 시 수축 거리" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z올리기 높이" @@ -16498,6 +16591,9 @@ msgstr "재 시작 시 추가 길이" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "이동 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다. 이 설정은 거의 필요하지 않습니다." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "툴 체인지 후 후퇴가 보상되면 압출기는 이 추가 양의 필라멘트를 밀어냅니다." @@ -16922,6 +17018,12 @@ msgstr "프라임 타워에서 툴 체인지" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "툴 체인지 명령(Tx)을 실행하기 전에 툴헤드가 반드시 프라임 타워로 이동하도록 합니다. 유형 2 프라임 타워를 사용하는 다중 압출기(멀티 툴헤드) 프린터에만 해당됩니다. 기본적으로 Orca는 멀티 툴헤드 장비에서 펌웨어가 헤드 교체를 처리하므로 이동을 생략하는데, 이 때문에 Tx 명령이 출력물 위에서 실행될 수 있습니다. 툴 체인지가 항상 프라임 타워 위에서 실행되도록 하려면 이 옵션을 활성화하십시오." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "희소 레이어 없음(베타)" @@ -20261,10 +20363,6 @@ msgstr "물리 프린터" msgid "Print Host upload" msgstr "출력 호스트 업로드" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "프린터 통신에 사용할 네트워크 에이전트 구현을 선택합니다. 사용 가능한 에이전트는 시작 시 등록됩니다." - # AI Translated msgid "Select a Flashforge printer" msgstr "Flashforge 프린터 선택" @@ -21217,9 +21315,6 @@ msgstr "로그인을 시도하는 동안 예기치 않은 문제가 발생했습 msgid "User canceled." msgstr "사용자가 취소했습니다." -msgid "Head diameter" -msgstr "헤드 직경" - msgid "Max angle" msgstr "최대 각도" @@ -22057,6 +22152,22 @@ msgstr "" "뒤틀림 방지\n" "ABS와 같이 뒤틀림이 발생하기 쉬운 소재를 출력할 때, 히트베드 온도를 적절하게 높이면 뒤틀림 가능성을 줄일 수 있다는 사실을 알고 계셨나요?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "레이어 높이가 너무 작습니다.\n" +#~ "min_layer_height로 설정됩니다.\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "레이어 높이가 프린터 설정 -> 압출기 -> 레이어의 제한을 초과합니다.높이 제한으로 인해 출력 품질 문제가 발생할 수 있습니다." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "설정 범위에 자동으로 맞춰지나요?\n" + +#~ msgid "Head diameter" +#~ msgstr "헤드 직경" + #~ msgid "Print order within a single layer." #~ msgstr "단일 레이어 내의 출력 순서" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 9a6b7ae590..aac9ef9dc6 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -4728,6 +4728,20 @@ msgstr "Dabartinė kameros temperatūra yra aukštesnė už saugią medžiagos t msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimali kameros temperatūra (%d℃) yra aukštesnė nei tikslinė kameros temperatūra (%d℃). Minimali vertė yra slenkstis, kurį pasiekus pradedamas spausdinimas, kol kamera vis dar kaitinama iki tikslinės temperatūros, todėl ji neturėtų viršyti tikslinės. Vertė bus apribota iki tikslinės temperatūros." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Sureguliuoti" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4847,6 +4861,12 @@ msgstr "" "Taip – įjungti „Arachne“ sienelių generatorių\n" "Ne – išjungti „Arachne“ sienelių generatorių ir nustatyti „Šiurkštaus paviršius“ režimą [Slinktis]" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Pado apvado plotis" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralinis režimas veikia tik tada, kai sienelės kilpų skaičius yra 1, atramos išjungtos, sulipimo aptikimas zonduojant išjungtas, viršutinių apvalkalo sluoksnių yra 0, reto užpildo tankis yra 0 %, o laiko intervalų vaizdo įrašo tipas – tradicinis." @@ -4862,6 +4882,9 @@ msgstr "" "Taip – pakeisti šiuos nustatymus ir automatiškai įjungti spiralinį režimą\n" "Ne – nenaudoti spiralinio režimo" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Spausdinimas" @@ -5101,6 +5124,12 @@ msgstr "Nepavyko sugeneruoti kalibravimo G-kodo" msgid "Calibration error" msgstr "Kalibravimo klaida" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Tinklas neprieinamas" @@ -5313,9 +5342,6 @@ msgstr "Neteisingas šablonas. Naudokite N, N#K arba kableliais atskirtą sąra msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Netinkamas formatas. Tinkamas vektorinis formatas: \"%1%\"" -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Sisteminiai agentai" @@ -5961,7 +5987,7 @@ msgstr "Tūris:" msgid "Size:" msgstr "Dydis:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Rasta G-kodo trajektorijų konfliktų %d sluoksnyje, Z = %.2lfmm. Prašome labiau atskirti konfliktuojančius objektus (%s <-> %s)." @@ -6142,6 +6168,9 @@ msgstr "Kelių įrenginių valdymas (Multi-device)" msgid "Project" msgstr "Projektas" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Taip" @@ -6269,6 +6298,12 @@ msgstr "Įrašyti projektą kaip" msgid "Save current project as" msgstr "Įrašyti dabartinį projektą kaip" +msgid "Publish" +msgstr "Talpinti" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8074,6 +8109,12 @@ msgstr "Patvirtinkite, kad šiuose profiliuose esantis G-kodas yra saugus, kad i msgid "Customized Preset" msgstr "Pritaikytas profilis" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Komponentų pavadinimai STEP faile nėra UTF-8 formato!" @@ -8239,19 +8280,19 @@ msgstr "" "Pakeista 3D failais iš katalogo:\n" "\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Praleistas %s: tas pats failas.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Praleistas %s: failas neegzistuoja.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Praleistas %s: nepavyko pakeisti.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Pakeistas %s.\n" @@ -8461,6 +8502,14 @@ msgstr "Išsaugoti susluoksniuotą failą kaip:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Failas %s išsiųstas į spausdintuvo laikmeną ir gali būti peržiūrimas spausdintuve." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Purkštuko tipas nenustatytas. Nustatykite purkštuką ir bandykite dar kartą." @@ -9329,6 +9378,14 @@ msgstr "Rodyti nepalaikomus profilius" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Rodyti nesuderinamus / nepalaikomus profilius spausdintuvų ir gijų išskleidžiamuosiuose sąrašuose. Šių profilių pasirinkti negalima." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Eksperimentinis" @@ -9562,9 +9619,6 @@ msgstr "Pereiti į modelio talpinimo interneto puslapį" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Pastaba: paruošimas gali užtrukti kelias minutes. Būkite kantrūs." -msgid "Publish" -msgstr "Talpinti" - msgid "Publish was canceled" msgstr "Publikavimas buvo atšauktas" @@ -9580,6 +9634,21 @@ msgstr "Įkeliami duomenys" msgid "Jump to webpage" msgstr "Pereiti į interneto puslapį" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Išsaugoti %s kaip" @@ -9590,9 +9659,21 @@ msgstr "Naudotojo profilis" msgid "Preset Inside Project" msgstr "Profilis projekto viduje" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Atskirti nuo tėvinio profilio" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Nėra pavadinimo." @@ -10330,24 +10411,6 @@ msgstr "Ar tikrai norite įjungti šią parinktį?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Užpildymo modeliai paprastai yra suprojektuoti taip, kad automatiškai tvarkytų sukimąsi, siekiant užtikrinti tinkamą spausdinimą ir pasiekti numatytus efektus (pvz., Gyroid, Cubic). Sukant esamą retą užpildymo modelį, gali atsirasti nepakankamas atraminis paviršius. Prašome elgtis atsargiai ir atidžiai patikrinti, ar nėra galimų spausdinimo problemų. Ar tikrai norite įjungti šią parinktį?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Per mažas sluoksnio aukštis.\n" -"Jis bus nustatytas į min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." - -msgid "Adjust to the set range automatically?\n" -msgstr "" -"Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" -"\n" - -msgid "Adjust" -msgstr "Sureguliuoti" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Eksperimentinė funkcija: gijos įtraukimas ir nukirpimas didesniu atstumu keičiant giją, siekiant sumažinti išvalymą (flush). Nors tai gali pastebimai sumažinti išvalymą, taip pat gali padidėti purkštuko užsikimšimo ar kitų spausdinimo komplikacijų rizika." @@ -10547,6 +10610,9 @@ msgstr "Rasti rezervuoti raktažodžiai" msgid "Setting Overrides" msgstr "Nustatymų perrašymas" +msgid "Retraction when switching material" +msgstr "Įtraukimas keičiant medžiagą" + msgid "Basic information" msgstr "Pagrindinė informacija" @@ -10673,6 +10739,12 @@ msgstr "Suderinami apdorojimo profiliai" msgid "Printable space" msgstr "Erdvė spausdinimui" +msgid "Printer Agent" +msgstr "Spausdintuvo agentas" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10798,9 +10870,6 @@ msgstr "Sluoksnio aukščio ribos" msgid "Z-Hop" msgstr "Z šuolis" -msgid "Retraction when switching material" -msgstr "Įtraukimas keičiant medžiagą" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12146,6 +12215,9 @@ msgstr "" " yra per arti sulipimo aptikimo zonos, todėl įvyks susidūrimai.\n" "\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Pasirinktos purkštuko temperatūros yra nesuderinamos. Kiekvienos gijos purkštuko temperatūra turi patekti į kitų gijų rekomenduojamos temperatūros diapazoną. Priešingu atveju gali užsikimšti purkštukas arba sugesti spausdintuvas." @@ -12459,9 +12531,6 @@ msgstr "Vietoj G-kodo naudoti 3MF" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Įjunkite, jei spausdintuvas spausdinimo užduotims priima 3MF failus. Kai įjungta, „Orca Slicer“ sugeneruotą failą siunčia kaip „.gcode.3mf“, o ne kaip paprastą „.gcode“ failą." -msgid "Printer Agent" -msgstr "Spausdintuvo agentas" - msgid "Select the network agent implementation for printer communication." msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti." @@ -13134,9 +13203,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Vidinių tiltelių spausdinimo greitis. Jei reikšmė nurodoma procentais, ji apskaičiuojama pagal „bridge_speed“ (tiltelių greitį). Numatytoji reikšmė – 150 %." -msgid "Brim width" -msgstr "Pado apvado plotis" - msgid "This is the distance from the model to the outermost brim line." msgstr "Atstumas nuo modelio iki išorinės krašto linijos" @@ -13217,6 +13283,12 @@ msgstr "" "Prieš aptinkant aštrius kampus, geometrija yra supaprastinama (decimuojama). Šis parametras nurodo minimalų nuokrypio ilgį supaprastinimui atlikti.\n" "Įrašykite 0, kad išjungtumėte." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "atgaliniu būdu suderinamas įrenginys" @@ -14370,6 +14442,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroidas" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Viršutinio paviršiaus užpildo pagreitis. Naudojant mažesnę vertę gali pagerėti viršutinio paviršiaus kokybė." @@ -14914,6 +14992,12 @@ msgstr "Su kokiu G kodu suderinamas spausdintuvas." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Modifikuotas granulinis spausdintuvas" @@ -15955,6 +16039,12 @@ msgstr "Ilgas įtraukimas keičiant ekstruderį" msgid "Retraction distance when extruder change" msgstr "Įtraukimo atstumas keičiant ekstruderį" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "„Z-hop“ (pakėlimo) aukštis" @@ -16049,6 +16139,9 @@ msgstr "Papildomas ilgis po sugrąžinimo" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Kai po judėjimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį. Šis nustatymas reikalingas retai." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Kai po įrankio pakeitimo kompensuojamas gijos įtraukimas, ekstruderis papildomai išstums šį gijos kiekį." @@ -16461,6 +16554,12 @@ msgstr "Įrankio keitimas virš valymo bokšto" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Priverstinai nukreipti spausdinimo galvutę prie valymo bokšto prieš vykdant įrankio keitimo komandą (Tx). Aktualu tik spausdintuvams su keliais ekstruderiais (keliomis galvutėmis), naudojantiems 2 tipo valymo bokštą. Pagal numatytuosius nustatymus „OrcaSlicer“ praleidžia šį judesį kelių galvučių įrenginiuose, nes galvučių sukeitimą valdo aparatinė programinė įranga, todėl Tx komanda gali būti įvykdyta virš spausdinamos detalės. Įjunkite šią parinktį, jei norite, kad įrankio keitimas visada vyktų virš valymo bokšto." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Nėra retų sluoksnių (beta)" @@ -19702,9 +19801,6 @@ msgstr "Fizinis spausdintuvas" msgid "Print Host upload" msgstr "Įkėlimas spausdinimui tinkle" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Pasirinkite tinklo agento modulį ryšiui su spausdintuvu palaikyti. Prieinami agentai užregistruojami paleidimo metu." - msgid "Select a Flashforge printer" msgstr "Pasirinkite „Flashforge“ spausdintuvą" @@ -20552,9 +20648,6 @@ msgstr "Bandant prisijungti įvyko kažkas netikėto. Bandykite dar kartą." msgid "User canceled." msgstr "Vartotojas atšaukė." -msgid "Head diameter" -msgstr "Galvutės skersmuo" - msgid "Max angle" msgstr "Maksimalus kampas" @@ -21336,6 +21429,24 @@ msgstr "" "Venkite deformacijų (warping)\n" "Ar žinojote, kad spausdinant medžiagas, kurios yra linkusios trauktis ir riestis (pvz., ABS), tinkamas kaitinamojo pagrindo temperatūros padidinimas gali sumažinti deformacijų (warping) tikimybę?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Per mažas sluoksnio aukštis.\n" +#~ "Jis bus nustatytas į min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Sluoksnio aukštis viršija ribą, nurodytą Spausdintuvo nustatymai -> Ekstruderis -> Sluoksnio aukščio ribos, tai gali sukelti spausdinimo kokybės problemų." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "" +#~ "Sureguliuoti pagal nustatytą diapazoną automatiškai?\n" +#~ "\n" + +#~ msgid "Head diameter" +#~ msgstr "Galvutės skersmuo" + #~ msgid "Print order within a single layer." #~ msgstr "Elementų spausdinimo eiliškumas vieno sluoksnio ribose." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 1ae53b2888..4d37e65fee 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5150,6 +5150,20 @@ msgstr "De huidige kamertemperatuur is hoger dan de veilige temperatuur van het msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "De minimale kamertemperatuur (%d℃) is hoger dan de doelkamertemperatuur (%d℃). De minimale waarde is de drempel waarbij het printen start terwijl de kamer verder opwarmt naar de doelwaarde; deze mag die dus niet overschrijden. De waarde wordt begrensd tot de doelwaarde." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Aanpassen" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5277,6 +5291,12 @@ msgstr "" "Ja - Arachne-wandgenerator inschakelen\n" "Nee - Arachne-wandgenerator uitschakelen en de modus [Displacement] van Vage buitenkant instellen" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Rand breedte" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "De spiraalmodus werkt alleen wanneer Wanden 1 is, ondersteuning is uitgeschakeld, klontdetectie via aftasten is uitgeschakeld, het aantal bovenste buitenlagen 0 is, de dichtheid van de dunne vulling (infill) 0 is en het timelapse-type traditioneel is." @@ -5293,6 +5313,9 @@ msgstr "" "Ja - Pas de instellingen aan en zet de vaas modus automatisch aan\n" "Nee - Pas de vaas modus deze keer niet toe" +msgid "N/A" +msgstr "N/B" + msgid "Printing" msgstr "Printen" @@ -5582,6 +5605,12 @@ msgstr "Cali G-code niet gegenereerd" msgid "Calibration error" msgstr "Kalibratiefout" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Netwerk niet beschikbaar" @@ -5821,9 +5850,6 @@ msgstr "Ongeldig patroon. Gebruik N, N#K of een door komma's gescheiden lijst me msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Onjuist formaat. Het Vector formaat wordt verwacht: \"%1%\"" -msgid "N/A" -msgstr "N/B" - # AI Translated msgid "System agents" msgstr "Systeemagenten" @@ -6513,7 +6539,7 @@ msgid "Size:" msgstr "Maat:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Er zijn conflicten tussen G-code-paden gevonden op laag %d, Z = %.2lfmm. Plaats de conflicterende objecten verder uit elkaar (%s <-> %s)." @@ -6714,6 +6740,9 @@ msgstr "Meerdere apparaten" msgid "Project" msgstr "Project" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Ja" @@ -6844,6 +6873,12 @@ msgstr "Bewaar project als" msgid "Save current project as" msgstr "Bewaar huidig project als" +msgid "Publish" +msgstr "Publiceren" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF importeren" @@ -8824,6 +8859,12 @@ msgstr "Controleer of de G-codes in deze presets veilig zijn om schade aan de ma msgid "Customized Preset" msgstr "Aangepaste voorinstelling" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Naam van componenten in step-bestand is niet UTF8-formaat!" @@ -8999,22 +9040,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Vervangen door 3D-bestanden uit de map:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Overgeslagen %s: hetzelfde bestand.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Overgeslagen %s: bestand bestaat niet.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Overgeslagen %s: vervangen is mislukt.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Vervangen %s.\n" @@ -9235,6 +9276,14 @@ msgstr "Bewaar het geslicede bestand als:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Het bestand %s is naar de opslagruimte van de printer gestuurd en kan op de printer worden bekeken." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Het mondstuktype is niet ingesteld. Stel het mondstuk in en probeer het opnieuw." @@ -10243,6 +10292,14 @@ msgstr "Niet-ondersteunde voorinstellingen tonen" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Toon incompatibele/niet-ondersteunde voorinstellingen in de keuzelijsten voor printer en filament. Deze voorinstellingen kunnen niet worden geselecteerd." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Experimentele functies" @@ -10494,9 +10551,6 @@ msgstr "Ga naar de website om het model te publiceren" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Notitie: het voorbereiden kan enkele minuten duren. Even geduld alstublieft." -msgid "Publish" -msgstr "Publiceren" - msgid "Publish was canceled" msgstr "Het publiceren is geannuleerd" @@ -10513,6 +10567,21 @@ msgstr "Gegevens uploaden" msgid "Jump to webpage" msgstr "Ga naar de website" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Bewaar %s als" @@ -10523,10 +10592,22 @@ msgstr "Gebruikersvoorinstelling" msgid "Preset Inside Project" msgstr "Voorinstelling binnen project" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Losmaken van bovenliggend element" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Naam is niet beschikbaar." @@ -11336,22 +11417,6 @@ msgstr "Weet u zeker dat u deze optie wilt inschakelen?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Vulpatronen zijn doorgaans ontworpen om rotatie automatisch af te handelen, zodat ze goed printen en hun beoogde effect bereiken (bijv. Gyroide, Kubisch). Het roteren van het huidige patroon voor de dunne vulling (infill) kan tot onvoldoende ondersteuning leiden. Ga voorzichtig te werk en controleer grondig op mogelijke printproblemen. Weet u zeker dat u deze optie wilt inschakelen?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Laaghoogte is te klein.\n" -"Het zal worden ingesteld op min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." - -msgid "Adjust to the set range automatically?\n" -msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" - -msgid "Adjust" -msgstr "Aanpassen" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentele functie: Het filament op grotere afstand terugtrekken en afsnijden tijdens filamentwisselingen om flush te minimaliseren. Hoewel het het doorspoelen aanzienlijk kan verminderen, kan het ook het risico op een verstopt mondstuk of andere printcomplicaties vergroten." @@ -11551,6 +11616,9 @@ msgstr "Gereserveerde zoekworden gevonden" msgid "Setting Overrides" msgstr "Overschrijvingen instellen" +msgid "Retraction when switching material" +msgstr "Terugtrekken (retraction) bij het wisselen van filament" + msgid "Basic information" msgstr "Basisinformatie" @@ -11689,6 +11757,14 @@ msgstr "Geschikte proces profielen" msgid "Printable space" msgstr "Ruimte waarbinnen geprint kan worden" +# AI Translated +msgid "Printer Agent" +msgstr "Printeragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11829,9 +11905,6 @@ msgstr "Limieten voor laaghoogte" msgid "Z-Hop" msgstr "Z-hop" -msgid "Retraction when switching material" -msgstr "Terugtrekken (retraction) bij het wisselen van filament" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -13323,6 +13396,9 @@ msgstr " bevindt zich te dicht bij het uitsluitingsgebied en er zullen botsingen msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligt te dicht bij het gebied voor klontdetectie, waardoor er botsingen zullen ontstaan.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De geselecteerde mondstuktemperaturen zijn niet compatibel. De mondstuktemperatuur van elk filament moet binnen het aanbevolen mondstuktemperatuurbereik van de andere filamenten vallen. Anders kan het mondstuk verstopt raken of kan de printer beschadigd raken." @@ -13686,10 +13762,6 @@ msgstr "3MF gebruiken in plaats van G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Schakel dit in als de printer een 3MF-bestand als printopdracht accepteert. Indien ingeschakeld verzendt Orca Slicer het geslicede bestand als een .gcode.3mf in plaats van als een gewoon .gcode-bestand." -# AI Translated -msgid "Printer Agent" -msgstr "Printeragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer." @@ -14443,9 +14515,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Snelheid van interne bruggen. Als de waarde als percentage wordt uitgedrukt, wordt deze berekend op basis van bridge_speed. De standaardwaarde is 150%." -msgid "Brim width" -msgstr "Rand breedte" - msgid "This is the distance from the model to the outermost brim line." msgstr "Dit is de afstand van het model tot de buitenste randlijn." @@ -14537,6 +14606,12 @@ msgstr "" "De geometrie wordt vereenvoudigd voordat scherpe hoeken worden gedetecteerd. Deze parameter geeft de minimale lengte van de afwijking voor die vereenvoudiging aan.\n" "0 om uit te schakelen." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "opwaarts compatibele machine" @@ -15846,6 +15921,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroide" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Versnelling van de topoppervlakte-invulling. Gebruik van een lagere waarde kan de kwaliteit van de bovenlaag verbeteren." @@ -16456,6 +16537,12 @@ msgstr "Het type G-code waarmee de printer compatibel is." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + # AI Translated msgid "Pellet Modded Printer" msgstr "Printer omgebouwd voor pellets" @@ -17653,6 +17740,12 @@ msgstr "Lange terugtrekking bij extruderwissel" msgid "Retraction distance when extruder change" msgstr "Terugtrekafstand bij extruderwissel" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + # AI Translated msgid "Z-hop height" msgstr "Z-hop-hoogte" @@ -17763,6 +17856,9 @@ msgstr "Extra lengte bij herstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Als retracten wordt gecompenseerd na een beweging, wordt deze extra hoeveelheid filament geëxtrudeerd. Deze instelling is zelden van toepassing." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Als retracten wordt gecompenseerd na een toolwisseling, wordt deze extra hoeveelheid filament geëxtrudeerd." @@ -18255,6 +18351,12 @@ msgstr "Toolwissel op het afveegblok" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Dwing de printkop naar het afveegblok te bewegen voordat de opdracht voor de toolwissel (Tx) wordt gegeven. Alleen relevant voor printers met meerdere extruders (meerdere printkoppen) die een afveegblok van type 2 gebruiken. Standaard slaat Orca deze verplaatsing op machines met meerdere printkoppen over, omdat de firmware de kopwissel afhandelt, waardoor de Tx-opdracht boven het geprinte onderdeel kan worden gegeven. Schakel deze optie in als u wilt dat de toolwissel altijd boven het afveegblok wordt uitgevoerd." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + # AI Translated msgid "No sparse layers (beta)" msgstr "Geen dunne lagen (bèta)" @@ -21860,10 +21962,6 @@ msgstr "Fysieke printer" msgid "Print Host upload" msgstr "Host-upload afdrukken" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecteer de implementatie van de netwerkagent voor de communicatie met de printer. Beschikbare agenten worden bij het opstarten geregistreerd." - # AI Translated msgid "Select a Flashforge printer" msgstr "Selecteer een Flashforge-printer" @@ -22918,9 +23016,6 @@ msgstr "Er is iets onverwachts gebeurd bij het inloggen. Probeer het opnieuw." msgid "User canceled." msgstr "Gebruiker geannuleerd." -msgid "Head diameter" -msgstr "Kopdiameter" - # AI Translated msgid "Max angle" msgstr "Maximale hoek" @@ -23781,6 +23876,22 @@ msgstr "" "Kromtrekken voorkomen\n" "Wist je dat bij het printen van materialen die gevoelig zijn voor kromtrekken, zoals ABS, een juiste verhoging van de temperatuur van het warmtebed de kans op kromtrekken kan verkleinen?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Laaghoogte is te klein.\n" +#~ "Het zal worden ingesteld op min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "De laaghoogte overschrijdt de limiet in Printerinstellingen -> Extruder -> Laaghoogtelimieten, dit kan problemen met de afdrukkwaliteit veroorzaken." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Automatisch aanpassen aan het ingestelde bereik?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kopdiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Printvolgorde binnen één laag." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index a0701dca09..e9430155ea 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -4843,6 +4843,20 @@ msgstr "Obecna temperatura komory jest wyższa niż bezpieczna temperatura dla f msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimalna temperatura komory (%d℃) jest wyższa niż docelowa temperatura komory (%d℃). Wartość minimalna to próg, przy którym rozpoczyna się druk, podczas gdy komora nadal nagrzewa się do wartości docelowej, więc nie powinna jej przekraczać. Zostanie ograniczona do wartości docelowej." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Dostosuj" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4965,6 +4979,12 @@ msgstr "" "Tak — włącz generator ścian Arachne\n" "Nie — wyłącz generator ścian Arachne i ustaw tryb [Przesunięcie] skóry fuzzy" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Szerokość brimu" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Tryb spiralny działa tylko wtedy, gdy liczba pętli ściany wynosi 1, podpory są wyłączone, wykrywanie zlepiania przez sondowanie jest wyłączone, liczba warstw górnej powłoki wynosi 0, gęstość wypełnienia wynosi 0, a typ timelapse jest tradycyjny." @@ -4981,6 +5001,10 @@ msgstr "" "Tak - Zmień te ustawienia automatycznie i włącz tryb Wazy\n" "Nie - Zrezygnuj tym razem z używania trybu Wazy" +# AI Translated +msgid "N/A" +msgstr "Nie dotyczy" + msgid "Printing" msgstr "Drukowanie" @@ -5226,6 +5250,12 @@ msgstr "Nie udało się wygenerować kodu kalibracji" msgid "Calibration error" msgstr "Błąd kalibracji" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Sieć niedostępna" @@ -5440,10 +5470,6 @@ msgstr "Nieprawidłowy wzorzec. Użyj N, N#K lub listy rozdzielonej przecinkami msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Nieprawidłowy format. Oczekiwano formatu wektorowego: „%1%”" -# AI Translated -msgid "N/A" -msgstr "Nie dotyczy" - # AI Translated msgid "System agents" msgstr "Agenci systemowi" @@ -6109,7 +6135,7 @@ msgstr "Objętość:" msgid "Size:" msgstr "Rozmiar:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Wykryto konflikty ścieżek G-code na warstwie %d, Z = %.2lfmm. Proszę oddalić od siebie obiekty będące w konflikcie (%s <-> %s)." @@ -6295,6 +6321,9 @@ msgstr "Wiele urządzeń" msgid "Project" msgstr "Projekt" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Tak" @@ -6425,6 +6454,12 @@ msgstr "Zapisz projekt jako" msgid "Save current project as" msgstr "Zapisz bieżący projekt jako" +msgid "Publish" +msgstr "Opublikuj" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8273,6 +8308,12 @@ msgstr "Proszę potwierdź, że G-code w tych profilach jest bezpieczny, aby zap msgid "Customized Preset" msgstr "Dostosowany profil" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Nazwa komponentów w pliku step nie jest w formacie UTF8!" @@ -8444,22 +8485,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Zastąpiono plikami 3D z katalogu:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Pominięto %s: ten sam plik.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Pominięto %s: plik nie istnieje.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Pominięto %s: nie udało się zastąpić.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Zastąpiono %s.\n" @@ -8675,6 +8716,14 @@ msgstr "Zapisz plik po wykonaniu cięcia jako:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Plik %s został wysłany do pamięci drukarki i można go obejrzeć na urządzeniu." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nie ustawiono typu dyszy Wprowadź ustawienia dyszy i spróbuj ponownie." @@ -9647,6 +9696,14 @@ msgstr "Pokaż nieobsługiwane profile" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Pokazuj niekompatybilne/nieobsługiwane profile na listach rozwijanych drukarek i filamentów. Tych profili nie można wybrać." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Funkcje eksperymentalne" @@ -9889,9 +9946,6 @@ msgstr "Przejdź do strony publikacji modelu" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Uwaga: Przygotowanie może zająć kilka minut. Proszę o cierpliwość." -msgid "Publish" -msgstr "Opublikuj" - msgid "Publish was canceled" msgstr "Publikacja została anulowana" @@ -9908,6 +9962,21 @@ msgstr "Przesyłanie danych" msgid "Jump to webpage" msgstr "Przejdź na stronę" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Zapisz %s jako" @@ -9918,10 +9987,22 @@ msgstr "Profil użytkownika" msgid "Preset Inside Project" msgstr "Profil wewnątrz projektu" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Odłącz od elementu nadrzędnego" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Nazwa jest niedostępna." @@ -10684,22 +10765,6 @@ msgstr "Czy na pewno włączyć tę opcję?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Wzory wypełnienia są zwykle projektowane tak, aby samodzielnie obsługiwać obrót, co zapewnia prawidłowy druk i zamierzony efekt (np. Gyroidalny, Sześcienny). Obracanie bieżącego wzoru wypełnienia może prowadzić do niewystarczającego podparcia. Zachowaj ostrożność i dokładnie sprawdź, czy nie występują problemy z drukiem. Czy na pewno chcesz włączyć tę opcję?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Wysokość warstwy jest zbyt mała.\n" -"Ustawione zostanie na min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." - -msgid "Adjust to the set range automatically?\n" -msgstr "Dostosować automatycznie do ustawionego zakresu?\n" - -msgid "Adjust" -msgstr "Dostosuj" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funkcja eksperymentalna: Polega na wycofywaniu filamentu na większą odległość w celu zminimalizowania płukania, a następne jego odcięcie. Choć może to znacząco zmniejszyć ilość zużytego filamentu, może również zwiększyć ryzyko zatknięcia dyszy lub innych problemów z drukowaniem." @@ -10899,6 +10964,9 @@ msgstr "Znaleziono zarezerwowane słowa kluczowe" msgid "Setting Overrides" msgstr "Nadpisywane Ustawień" +msgid "Retraction when switching material" +msgstr "Retrakcja podczas zmiany filamentu" + msgid "Basic information" msgstr "Podstawowe informacje" @@ -11033,6 +11101,14 @@ msgstr "Kompatybilne profile procesów" msgid "Printable space" msgstr "Przestrzeń do druku" +# AI Translated +msgid "Printer Agent" +msgstr "Agent drukarki" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11165,9 +11241,6 @@ msgstr "Ograniczenia wysokości warstwy" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retrakcja podczas zmiany filamentu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12559,6 +12632,9 @@ msgstr " jest zbyt blisko obszaru wykluczenia, mogą wystąpić kolizje.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " jest zbyt blisko obszaru wykrywania zalepienia dyszy, co doprowadzi do kolizji.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Wybrane temperatury dyszy są niezgodne. Temperatura dyszy każdego filamentu musi mieścić się w zalecanym zakresie temperatur dyszy pozostałych filamentów. W przeciwnym razie może dojść do zatkania dyszy lub uszkodzenia drukarki." @@ -12901,10 +12977,6 @@ msgstr "Użyj 3MF zamiast G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Włącz tę opcję, jeśli drukarka przyjmuje plik 3MF jako zadanie druku. Po włączeniu Orca Slicer wysyła plik po cięciu jako .gcode.3mf zamiast zwykłego pliku .gcode." -# AI Translated -msgid "Printer Agent" -msgstr "Agent drukarki" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką." @@ -13617,9 +13689,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Prędkość wewnętrznych mostów. Jeśli wartość jest wyrażona w procentach, będzie obliczana na podstawie prędkości mostu. Wartość domyślna wynosi 150%." -msgid "Brim width" -msgstr "Szerokość brimu" - msgid "This is the distance from the model to the outermost brim line." msgstr "Odległość od modelu do najbardziej zewnętrznej linii brimu" @@ -13703,6 +13772,12 @@ msgstr "" "Kształt zostanie zredukowany przed wykryciem ostrych kątów. Ten parametr wskazuje minimalną długość odchylenia dla redukcji.\n" "0, aby dezaktywować" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "drukarka kompatybilna i wzwyż" @@ -14896,6 +14971,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroidalny" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Przyspieszenie dla wypełnienia górnej powierzchni. Użycie niższej wartości może poprawić jakość górnej powierzchni" @@ -15459,6 +15540,12 @@ msgstr "Z jakim rodzajem G-code drukarka jest kompatybilna." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Drukarka do druku granulatem" @@ -16571,6 +16658,12 @@ msgstr "Długa retrakcja podczas zmian ekstruderów" msgid "Retraction distance when extruder change" msgstr "Długość retrakcji podczas zmian ekstruderów" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Wysokość Z-hop" @@ -16669,6 +16762,9 @@ msgstr "Dodatkowa ilość dla powrotu" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Gdy retrakcja jest kompensowana po przemieszczeniu, ekstruder przepycha tę dodatkową ilość filamentu. To opcja jest rzadko potrzebna." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Jeśli retrakcja jest korygowana po zmianie narzędzia, extruder przepchnie taką dodatkową ilość filamentu." @@ -17099,6 +17195,12 @@ msgstr "Zmiana narzędzia na wieży czyszczącej" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Wymusza przemieszczenie głowicy do wieży czyszczącej przed wydaniem polecenia zmiany narzędzia (Tx). Dotyczy tylko drukarek wieloekstruderowych (wielogłowicowych) korzystających z wieży czyszczącej typu 2. Domyślnie Orca pomija to przemieszczenie na maszynach wielogłowicowych, ponieważ zamianą głowic zajmuje się oprogramowanie sprzętowe, przez co polecenie Tx może zostać wydane nad drukowaną częścią. Włącz tę opcję, jeśli chcesz, aby zmiana narzędzia zawsze następowała nad wieżą czyszczącą." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Warstwy bez czyszczenia (beta)" @@ -20445,10 +20547,6 @@ msgstr "Fizyczna drukarka" msgid "Print Host upload" msgstr "Przesyłanie do hosta drukowania" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Wybierz implementację agenta sieciowego do komunikacji z drukarką. Dostępni agenci są rejestrowani przy uruchamianiu." - # AI Translated msgid "Select a Flashforge printer" msgstr "Wybierz drukarkę Flashforge" @@ -21401,9 +21499,6 @@ msgstr "Wystąpił problem podczas próby logowania, proszę spróbować ponowni msgid "User canceled." msgstr "Anulowane przez użytkownika." -msgid "Head diameter" -msgstr "Średnica łącznika" - msgid "Max angle" msgstr "Maksymalny kąt" @@ -22234,6 +22329,22 @@ msgstr "" "Unikaj odkształceń\n" "Czy wiesz, że podczas drukowania filamentami podatnymi na odkształcenia, takimi jak ABS, odpowiednie zwiększenie temperatury podgrzewanej płyty może zmniejszyć prawdopodobieństwo odkształceń?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Wysokość warstwy jest zbyt mała.\n" +#~ "Ustawione zostanie na min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Wysokość warstwy przekracza limit w Ustawieniach Drukarki -> Extruder -> Limity wysokości warstwy, co może powodować problemy z jakością druku." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Dostosować automatycznie do ustawionego zakresu?\n" + +#~ msgid "Head diameter" +#~ msgstr "Średnica łącznika" + #~ msgid "Print order within a single layer." #~ msgstr "Kolejność druku obiektów w obrębie jednej warstwy. Domyślnie lub według listy obiektów" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 595e46b8cf..b001ea24b8 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -4577,6 +4577,20 @@ msgstr "A temperatura da câmara atual está mais alta do que a temperatura segu msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "A temperatura mínima da câmara (%d℃) é superior à temperatura alvo da câmara (%d℃). O valor mínimo é o limite no qual a impressão começa enquanto a câmara continua aquecendo em direção ao alvo; portanto, não deve execedê-lo. O valor será limitado ao alvo." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Ajustar" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4696,6 +4710,12 @@ msgstr "" "Sim - Habilitar Gerador de Parede Arachne\n" "Não - Desabilitar Gerador de Parede Arachne e setar o modo [Deslocamento] da Textura Difusa" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Largura da borda" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "O modo espiral só funciona quando as voltas da parede são 1, o suporte está desativado, a detecção de aglomeração por sondagem está desativada, as camadas da casca de topo são 0, a densidade de preenchimento esparso é 0 e o tipo de timelapse é tradicional." @@ -4711,6 +4731,9 @@ msgstr "" "Sim - Alterar essas configurações e ativar o modo espiral/vaso automaticamente\n" "Não - Cancelar ativação do modo espiral" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Imprimindo" @@ -4950,6 +4973,12 @@ msgstr "Falha ao gerar o G-code de calibração" msgid "Calibration error" msgstr "Erro de calibração" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "Rede indisponível" @@ -5156,9 +5185,6 @@ msgstr "Padrão inválido. Use N, N#K, ou uma lista separa por vírgulas com #K msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato inválido. Formato de vetor esperado: \"%1%\"" -msgid "N/A" -msgstr "N/D" - msgid "System agents" msgstr "Agentes do sistema" @@ -5793,7 +5819,7 @@ msgstr "Volume:" msgid "Size:" msgstr "Tamanho:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Foram encontrados conflitos de caminhos de G-code na camada %d, Z = %.2lfmm. Por favor, separe mais os objetos em conflito (%s <-> %s)." @@ -5974,6 +6000,9 @@ msgstr "Multi-dispositivo" msgid "Project" msgstr "Projeto" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Sim" @@ -6101,6 +6130,12 @@ msgstr "Salvar projeto como" msgid "Save current project as" msgstr "Salvar o projeto atual como" +msgid "Publish" +msgstr "Publicar" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7861,6 +7896,12 @@ msgstr "Por favor, confirme se o G-code dentro dessas predefinições é seguro msgid "Customized Preset" msgstr "Predefinição Personalizada" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Os nomes dos componentes dentro do arquivo STEP não estão no formato UTF-8!" @@ -8020,19 +8061,19 @@ msgstr "Diretório para substituição não foi selecionado" msgid "Replaced with 3D files from directory:\n" msgstr "Substituído por arquivos 3D do diretório:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s Ignorados: mesmo arquivo.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s Ignorados: arquivo não existe.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s Ignorados: falha ao substituir.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s Substituídos.\n" @@ -8241,6 +8282,14 @@ msgstr "Salvar arquivo fatiado como:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "O arquivo %s foi enviado para o espaço de armazenamento da impressora e pode ser visualizado na impressora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "O tipo de bico não está configurado. Configure o bico e tente novamente." @@ -9113,6 +9162,14 @@ msgstr "Mostrar predefinições não suportadas" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Exibir predefinições incompatíveis e não suportadas nas listas de impressora e filamento. Essas predefinições não podem ser selecionadas." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Recursos Experimentais" @@ -9346,9 +9403,6 @@ msgstr "Ir para a página web de publicação de modelos" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: A preparação pode levar vários minutos. Por favor, seja paciente." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "Publicação cancelada" @@ -9364,6 +9418,21 @@ msgstr "Enviando dados" msgid "Jump to webpage" msgstr "Ir para a página web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Salvar %s como" @@ -9374,9 +9443,21 @@ msgstr "Predefinição do Usuário" msgid "Preset Inside Project" msgstr "Predefinição Dentro do Projeto" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Separar do pai" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "O nome não está disponível." @@ -10096,24 +10177,6 @@ msgstr "Tem certeza de que deseja habilitar esta opção?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Padrões de preenchimento são projetados para lidar com a rotação automaticamente para garantir a impressão adequada e atingir os efeitos pretendidos (Ex. Giroide, Cúbico). Girar o padrão de preenchimento esparso atual pode causar suporte insuficiente. Prossiga com cautela e verifique cuidadosamente se há possíveis problemas de impressão. Tem certeza de que deseja habilitar esta opção?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" -"A altura da camada é muito pequena.\n" -"Ela será definida como altura mínima da camada\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ajustar automaticamente à faixa definida?\n" - -msgid "Adjust" -msgstr "Ajustar" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Funcionalidade experimental: Retrair e cortar o filamento a uma distância maior durante mudanças de filamento para minimizar a purga. Embora possa reduzir notavelmente a purga, ele também pode elevar o risco de bolhas no bico ou outras complicações de impressão." @@ -10308,6 +10371,9 @@ msgstr "Palavras-chave reservadas encontradas" msgid "Setting Overrides" msgstr "Sobrescrever configurações" +msgid "Retraction when switching material" +msgstr "Retração ao trocar material" + msgid "Basic information" msgstr "Informações básicas" @@ -10435,6 +10501,12 @@ msgstr "Perfis de processo compatíveis" msgid "Printable space" msgstr "Espaço de impressão" +msgid "Printer Agent" +msgstr "Agente de Impressora" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10560,9 +10632,6 @@ msgstr "Limites de altura da camada" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Retração ao trocar material" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -11893,6 +11962,9 @@ msgstr " está muito perto de uma área de exclusão, e colisões vão ocorrer.\ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " está muito perto da área de detecção de aglomeração, e ocorrerão colisões.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "As temperaturas dos bicos selecionadas são incompatíveis. A temperatura do bico de cada filamento deve estar dentro da faixa de temperatura recomendada para os demais filamentos. Caso contrário, pode ocorrer entupimento do bico ou danos à impressora." @@ -12206,9 +12278,6 @@ msgstr "Usar 3MF em vez de G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Ative esta opção se a impressora aceitar um arquivo 3MF como trabalho de impressão. Quando ativada, o OrcaSlicer envia o arquivo fatiado como .gcode.3mf, em vez de um arquivo .gcode comum." -msgid "Printer Agent" -msgstr "Agente de Impressora" - msgid "Select the network agent implementation for printer communication." msgstr "Selecione a implementação do agente de rede para comunicação com a impressora." @@ -12888,9 +12957,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Velocidade de pontes internas. Se o valor for expresso como uma porcentagem, ele será calculado com base na bridge_speed. O valor padrão é 150%." -msgid "Brim width" -msgstr "Largura da borda" - msgid "This is the distance from the model to the outermost brim line." msgstr "Essa é a distância do modelo até a linha da borda mais externa." @@ -12970,6 +13036,12 @@ msgstr "" "A geometria será decimada antes de detectar ângulos agudos. Este parâmetro indica o comprimento mínimo da divergência para a decimação.\n" "0 para desativar." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "uáquina compatível ascendente" @@ -14104,6 +14176,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Giroide" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Esta é a aceleração do preenchimento da superfície superior. Usar um valor menor pode melhorar a qualidade da superfície superior." @@ -14639,6 +14717,12 @@ msgstr "Com que tipo de G-code a impressora é compatível." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Impressora Modificada para Pellets" @@ -15681,6 +15765,12 @@ msgstr "Retração longa na troca de extrusora" msgid "Retraction distance when extruder change" msgstr "Distância de retração na troca de extrusora" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Altura de Z-hop" @@ -15774,6 +15864,9 @@ msgstr "Comprimento extra na retração" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Quando a retração é compensada após o movimento de deslocamento, a extrusora empurrará essa quantidade adicional de filamento. Esta configuração é raramente necessária." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Quando a retração é compensada após a troca de ferramenta, a extrusora empurrará essa quantidade adicional de filamento." @@ -16182,6 +16275,12 @@ msgstr "Troca de ferramenta na torre de purga" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Força o cabeçote de impressão a se deslocar até a torre de purga antes de emitir o comando de troca de ferramenta (Tx). Relevante apenas para impressoras com múltiplas extrusoras (múltiplos cabeçotes de impressão) que utilizam uma torre de purga Tipo 2. Por padrão, o Orca ignora o deslocamento em máquinas com múltiplos cabeçotes de impressão, pois o firmware gerencia a troca do cabeçote, o que pode resultar na emissão do comando Tx acima da peça impressa. Habilite esta opção se desejar que a troca de ferramenta seja sempre emitida acima da torre de purga." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Sem camadas esparsas (beta)" @@ -16733,7 +16832,7 @@ msgstr "Volume de preparo" msgid "This is the volume of material to prime the extruder with on the tower." msgstr "Este é o volume de material para preparar a extrusora na torre." -#,fuzzy +#, fuzzy msgid "Prime volume mode" msgstr "Modo de volume de preparação" @@ -19369,9 +19468,6 @@ msgstr "Impressora Física" msgid "Print Host upload" msgstr "Upload do Host de Impressão" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Selecione a implementação do agente de rede para comunicação com a impressora. Os agentes disponíveis são registrados na inicialização." - msgid "Select a Flashforge printer" msgstr "Selecione uma impressora Flashforge" @@ -20213,9 +20309,6 @@ msgstr "Algo inesperado aconteceu ao tentar conectar, por favor tente novamente. msgid "User canceled." msgstr "Cancelado pelo usuário." -msgid "Head diameter" -msgstr "Diâmetro da cabeça" - msgid "Max angle" msgstr "Ângulo máx" @@ -20949,6 +21042,24 @@ msgstr "" "Evitar empenamento\n" "Você sabia que ao imprimir materiais propensos ao empenamento como ABS, aumentar adequadamente a temperatura da mesa aquecida pode reduzir a probabilidade de empenamento?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" +#~ "A altura da camada é muito pequena.\n" +#~ "Ela será definida como altura mínima da camada\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "A altura da camada excede o limite em Configurações da Impressora -> Extrusora -> Limites de altura da camada, isso pode causar problemas de qualidade de impressão." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ajustar automaticamente à faixa definida?\n" + +#~ msgid "Head diameter" +#~ msgstr "Diâmetro da cabeça" + #~ msgid "Print order within a single layer." #~ msgstr "Ordem de impressão dentro de uma única camada." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index c2fbcb54be..919710f917 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -4715,6 +4715,20 @@ msgstr "Текущая температура внутри термокамер msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Стартовая температура внутри термокамеры (%d℃) превышает целевую (%d℃). Подразумевается, что печать начинается заранее, поэтому стартовая температура не должна превышать её. Значение будет уменьшено." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Подстроиться" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4839,6 +4853,12 @@ msgid "" "No - Disable Arachne Wall Generator and set [Displacement] mode of the Fuzzy Skin" msgstr "Использовать нечёткую оболочку с движком Arachne?" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Ширина каймы" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "" "Для печати в режиме вазы необходимы следующие настройки:\n" @@ -4859,6 +4879,11 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "Использовать эти настройки и режим вазы?" +# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо +# символов +msgid "N/A" +msgstr "–" + msgid "Printing" msgstr "Печать" @@ -5107,6 +5132,12 @@ msgstr "Не удалось сгенерировать калибровочны msgid "Calibration error" msgstr "Ошибка калибровки" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + msgid "Network unavailable" msgstr "Сеть недоступна" @@ -5320,11 +5351,6 @@ msgstr "Недопустимый шаблон. Используйте N, N#K и msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Недопустимый формат. Ожидаемый векторный формат: \"%1%\"" -# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо -# символов -msgid "N/A" -msgstr "–" - msgid "System agents" msgstr "Системные агенты" @@ -5991,7 +6017,7 @@ msgstr "Объём:" msgid "Size:" msgstr "Размер:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "В G-коде на %d слое (z = %.2lf мм) обнаружен конфликт путей. Пожалуйста, разместите конфликтующие модели дальше друг от друга (%s <-> %s)." @@ -6198,6 +6224,9 @@ msgstr "Принтеры" msgid "Project" msgstr "Проект" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Да" @@ -6334,6 +6363,12 @@ msgstr "Сохранить проект как" msgid "Save current project as" msgstr "Сохранить текущий проект как" +msgid "Publish" +msgstr "Опубликовать" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8132,6 +8167,12 @@ msgstr "Во избежание повреждения принтера убед msgid "Customized Preset" msgstr "Пользовательский профиль" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Имена компонентов внутри файла STEP не в формате UTF8." @@ -8299,19 +8340,19 @@ msgstr "Расположение для замены не указано" msgid "Replaced with 3D files from directory:\n" msgstr "Заменено файлами из расположения:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Пропущен %s: идентичный файл.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Пропущен %s: файл не существует.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Пропущен %s: не удалось заменить.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Заменён %s.\n" @@ -8520,6 +8561,14 @@ msgstr "Сохранить нарезанный файл как:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Файл %s отправлен в память принтера и может быть просмотрен на нём." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз." @@ -9400,6 +9449,14 @@ msgstr "" "\n" "Примечание: профили остаются недоступными для выбора." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Экспериментальные настройки" @@ -9638,9 +9695,6 @@ msgstr "Перейти на веб-страницу публикации мод msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Примечание: подготовка может занять несколько минут. Пожалуйста, наберитесь терпения." -msgid "Publish" -msgstr "Опубликовать" - msgid "Publish was canceled" msgstr "Публикация была отменена" @@ -9656,6 +9710,21 @@ msgstr "Отправка данных" msgid "Jump to webpage" msgstr "Перейти на страницу" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Сохранить %s как" @@ -9666,9 +9735,21 @@ msgstr "Пользовательский профиль" msgid "Preset Inside Project" msgstr "Профиль внутри проекта" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Сделать независимым" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Имя недоступно." @@ -9686,7 +9767,9 @@ msgstr "" "несовместим с текущим принтером." msgid "Please note that saving will overwrite the current preset." -msgstr "Обратите внимание: при сохранении произойдёт\nперезапись текущего профиля." +msgstr "" +"Обратите внимание: при сохранении произойдёт\n" +"перезапись текущего профиля." msgid "The name cannot be the same as a preset alias name." msgstr "Имя не должно совпадать с именем предустановленного профиля." @@ -10389,22 +10472,6 @@ msgstr "Вы действительно хотите задействовать msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Многие шаблоны заполнения разработаны на основе автоматического поворота по определённым правилам для поддержания правильной печати и желаемого эффекта (например, «Гироид» или «Куб»). Изменение правила поворота текущего шаблона может привести к его провисанию. Будьте осторожны и внимательно проверяйте результат на наличие потенциальных проблем с печатью." -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Высота слоя слишком мала.\n" -"Будет установлено значение min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" - -msgid "Adjust" -msgstr "Подстроиться" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "[Экспериментальная функция] Втягивание и обрезка прутка на большем расстоянии во время его замены для минимизации очистки. Хотя это значительно сокращает величину очистки, это может повысить риск возникновения затора или вызвать другие проблемы при печати." @@ -10604,6 +10671,9 @@ msgstr "Найдены зарезервированные ключевые сл msgid "Setting Overrides" msgstr "Замещение настроек" +msgid "Retraction when switching material" +msgstr "Откат при смене материала" + msgid "Basic information" msgstr "Основные" @@ -10751,6 +10821,12 @@ msgstr "Совместимые настройки" msgid "Printable space" msgstr "Область печати" +msgid "Printer Agent" +msgstr "Сетевой агент" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10879,9 +10955,6 @@ msgstr "Ограничение высоты слоя" msgid "Z-Hop" msgstr "Подъём головы при откате" -msgid "Retraction when switching material" -msgstr "Откат при смене материала" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12218,6 +12291,9 @@ msgstr " находится слишком близко к области иск msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " находится слишком близко к зоне обнаружения налипаний, столкновения неизбежны.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Обнаружен недопустимый перепад температур. Каждый из используемых материалов должен иметь в профиле температуру печати в пределах допустимого диапазона других материалов. В противном случае сопло может забиться и повредить принтер." @@ -12539,9 +12615,6 @@ msgstr "Сжатие G-кода перед отправкой" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Рекомендуется для принтеров, поддерживающих печать из архивов 3MF. Файлы печати будут отправляться с расширением \".gcode.3mf\"." -msgid "Printer Agent" -msgstr "Сетевой агент" - msgid "Select the network agent implementation for printer communication." msgstr "Реализация сетевого агента для обмена информацией с принтером." @@ -13232,9 +13305,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Скорость печати внутреннего моста. Можно указать процент от скорости внешнего моста (bridge_speed). По умолчанию – 150%." -msgid "Brim width" -msgstr "Ширина каймы" - msgid "This is the distance from the model to the outermost brim line." msgstr "Расстояние от модели до внешней линии каймы." @@ -13316,6 +13386,12 @@ msgstr "" "Геометрия модели будет упрощена перед обнаружением острых углов. Этот параметр задаёт минимальную длину отклонения для её упрощения.\n" "Установите 0 для отключения." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "условия для совместимых принтеров" @@ -14308,13 +14384,19 @@ msgid "Interface layer pre-extrusion distance" msgstr "Дистанция избыточной подачи при смене" msgid "Pre-extrusion distance for prime tower interface layer (where different materials meet)." -msgstr "Протяжённость первичного движения прочистки после смены материала. Позволяет быстро набрать давление в сопле и сбросить перегретый материал.\n\nПримечание: фактическая длина может быть ограничена шириной башни." +msgstr "" +"Протяжённость первичного движения прочистки после смены материала. Позволяет быстро набрать давление в сопле и сбросить перегретый материал.\n" +"\n" +"Примечание: фактическая длина может быть ограничена шириной башни." msgid "Interface layer pre-extrusion length" msgstr "Длина прутка для избыточной подачи" msgid "Pre-extrusion length for prime tower interface layer (where different materials meet)." -msgstr "Длина прутка, которую необходимо продавить на этапе избыточной подачи.\n\n0 – отключить этот этап." +msgstr "" +"Длина прутка, которую необходимо продавить на этапе избыточной подачи.\n" +"\n" +"0 – отключить этот этап." msgid "Tower ironing area" msgstr "Разглаживание кончиков" @@ -14626,6 +14708,12 @@ msgstr "ТПМП Фишера-Коха S" msgid "Gyroid" msgstr "Гироид" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Ускорение на верхней поверхности. Использование меньшего значения может улучшить качество верхней поверхности." @@ -15213,6 +15301,12 @@ msgstr "Выбор типа G-кода для совместимости с пр msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Гранульная модификация принтера" @@ -15396,8 +15490,7 @@ msgstr "Наклон опор" msgid "" "Controls how aggressively short or unsupported Lightning branches are pruned.\n" "This angle is converted internally to a per-layer distance." -msgstr "" -"Допустимый наклон опор молнии. Чем выше, тем быстрее и экономичнее распространяются её ветви." +msgstr "Допустимый наклон опор молнии. Чем выше, тем быстрее и экономичнее распространяются её ветви." # "Выпрямление" здесь, вопреки первой мысли – это как раз-таки наоборот искажение шаблона по ходу печати для сокращения количества ветвей. Короче, опять путаница из-за того, что генерация ветвей происходит сверху вниз. При печати снизу вверх шаблон именно что искажается. msgid "Straightening angle" @@ -16309,8 +16402,7 @@ msgid "" "The length of fast retraction after wipe, relative to retraction length.\n" "The value will be clamped by 100% minus the retract amount before the wipe value." msgstr "" -"Быстрый откат после очистки, выраженный в процентах от общей длины отката. В некоторых случаях позволяет значительно снизить количество «паутины»." -"\n" +"Быстрый откат после очистки, выраженный в процентах от общей длины отката. В некоторых случаях позволяет значительно снизить количество «паутины».\n" "Примечание: суммарное значение не должно превышать 100% и будет скорректировано автоматически." msgid "Retract on layer change" @@ -16344,6 +16436,12 @@ msgstr "Длинный откат перед сменой экструдера" msgid "Retraction distance when extruder change" msgstr "Длина отката перед сменой экструдера" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Высота подъёма" @@ -16461,6 +16559,9 @@ msgstr "Доп. подача после отката" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Дополнительная длина подачи при возврате прутка после отката. Требуется крайне редко (например, для компенсации багов прошивки принтера)." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Дополнительная длина подачи после смены насадки." @@ -16474,7 +16575,9 @@ msgid "Deretraction speed" msgstr "Скорость возврата" msgid "Speed for reloading filament into the nozzle. Zero means same speed of retraction." -msgstr "Скорость возврата материала в сопло после отката.\n0 – использовать скорость отката." +msgstr "" +"Скорость возврата материала в сопло после отката.\n" +"0 – использовать скорость отката." msgid "Deretraction speed (extruder change)" msgstr "Скорость возврата (смена экструдера)" @@ -16945,6 +17048,12 @@ msgstr "" "\n" "Внимание: применимо только к многоэкструдерным принтерам с черновой башней 2 типа." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Без разреженных слоёв (beta)" @@ -17942,13 +18051,21 @@ msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. msgstr "Во избежание подтёков температура сопла будет снижена на время рэмминга. Поэтому время рэмминга должно быть больше времени охлаждения. 0 значит отключено." msgid "The maximum volumetric speed for ramming before extruder change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход для рэмминга перед сменой экструдера.\n-1 – использовать максимальный расход." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой экструдера.\n" +"-1 – использовать максимальный расход." msgid "To prevent oozing, the nozzle temperature will be cooled during ramming. Note: only a cooldown command and fan activation are triggered, reaching the target temperature is not guaranteed. 0 means disabled." -msgstr "Во избежание подтёков температура сопла будет снижена на время рэмминга.\n0 – не менять температуру.\n\nПримечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется." +msgstr "" +"Во избежание подтёков температура сопла будет снижена на время рэмминга.\n" +"0 – не менять температуру.\n" +"\n" +"Примечание: срабатывают только команда охлаждения и включение вентилятора; достижение целевой температуры не гарантируется." msgid "The maximum volumetric speed for ramming before a hotend change, where -1 means using the maximum volumetric speed." -msgstr "Максимальный объёмный расход для рэмминга перед сменой хотэнда.\n-1 – использовать максимальный расход." +msgstr "" +"Максимальный объёмный расход для рэмминга перед сменой хотэнда.\n" +"-1 – использовать максимальный расход." msgid "length when change hotend" msgstr "Откат при смене хотэнда" @@ -19414,10 +19531,14 @@ msgid "Continue anyway?" msgstr "Всё равно продолжить?" msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить адаптацию к расходу для автоматического исправления?\nНет – игнорировать предупреждение." +msgstr "" +"Включить адаптацию к расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?" -msgstr "Включить адаптацию к соплу и расходу для автоматического исправления?\nНет – игнорировать предупреждение." +msgstr "" +"Включить адаптацию к соплу и расходу для автоматического исправления?\n" +"Нет – игнорировать предупреждение." msgid "Start retraction length: " msgstr "Начальная длина отката: " @@ -20341,9 +20462,6 @@ msgstr "Физический принтер" msgid "Print Host upload" msgstr "Загрузка на хост печати" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Реализация сетевого агента для обмена информацией с принтером. Доступные реализации определяются при запуске." - msgid "Select a Flashforge printer" msgstr "Выберите принтер Flashforge" @@ -21202,9 +21320,6 @@ msgstr "При попытке войти произошла какая-то ош msgid "User canceled." msgstr "Отменено пользователем." -msgid "Head diameter" -msgstr "Диаметр уха" - msgid "Max angle" msgstr "Макс. угол" @@ -21959,6 +22074,22 @@ msgstr "" "Предотвращение коробления материала\n" "Знаете ли вы, что при печати материалами, склонными к короблению, таких как ABS, повышение температуры подогреваемого стола может снизить эту вероятность?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Высота слоя слишком мала.\n" +#~ "Будет установлено значение min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Высота слоя не может превышать ограничения, установленные в настройках принтера → Экструдер → Ограничение высоты слоя. Это может вызвать проблемы с качеством печати." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматически подстроиться под заданный в настройках диапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Диаметр уха" + #~ msgid "Print order within a single layer." #~ msgstr "Последовательность печати моделей в пределах одного слоя." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 5686fb7d8f..1caffffd36 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5213,6 +5213,20 @@ msgstr "Kammarens aktuella temperatur är högre än materialets säkra temperat msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Kammarens minimitemperatur (%d℃) är högre än kammarens måltemperatur (%d℃). Minimivärdet är tröskeln där utskriften startar medan kammaren fortsätter värmas mot målet, så det bör inte överstiga målet. Värdet begränsas till måltemperaturen." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Justera" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5339,6 +5353,12 @@ msgstr "" "Ja – Aktivera Arachne-väggeneratorn\n" "Nej – Inaktivera Arachne-väggeneratorn och ställ in läget [Förskjutning] för ojämn yta" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Brim bredd" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiralläget fungerar bara när antal väggar är 1, support är avstängt, detektering av klumpbildning med sondering är avstängd, antal översta skallager är 0, sparsam ifyllnadsdensitet är 0 och timelapse-typen är traditionell." @@ -5355,6 +5375,10 @@ msgstr "" "JA -Ändra dessa inställningar och möjliggör Spiral läge automatiskt\n" "NEJ -Avbryt Spiral läge denna gång" +# AI Translated +msgid "N/A" +msgstr "Ej tillämpligt" + msgid "Printing" msgstr "Utskrift pågår" @@ -5645,6 +5669,12 @@ msgstr "Misslyckades med att generera cali G kod" msgid "Calibration error" msgstr "Fel vid kalibrering" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Nätverket är inte tillgängligt" @@ -5896,10 +5926,6 @@ msgstr "Ogiltigt mönster. Använd N, N#K eller en kommaseparerad lista med valf msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Ogiltligt format. Förväntat vector format: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "Ej tillämpligt" - # AI Translated msgid "System agents" msgstr "Systemagenter" @@ -6596,7 +6622,7 @@ msgid "Size:" msgstr "Storlek:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Konflikter mellan G-code-banor hittades på lager %d, Z = %.2lfmm. Placera de objekt som krockar längre ifrån varandra (%s <-> %s)." @@ -6798,6 +6824,9 @@ msgstr "Flera enheter" msgid "Project" msgstr "Projekt" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Ja" @@ -6928,6 +6957,12 @@ msgstr "Spara Projekt som" msgid "Save current project as" msgstr "Spara nuvarande projekt som" +msgid "Publish" +msgstr "Publicera" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importera 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8916,6 +8951,12 @@ msgstr "Bekräfta att G-koderna i dessa inställningar är säkra för att förh msgid "Customized Preset" msgstr "Anpassad inställning" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Komponent namnet i STEP filen är inte UTF8 format!" @@ -9088,22 +9129,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Ersatt med 3D-filer från mappen:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Hoppade över %s: samma fil.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Hoppade över %s: filen finns inte.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Hoppade över %s: det gick inte att ersätta.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Ersatte %s.\n" @@ -9328,6 +9369,14 @@ msgstr "Spara beredningen som:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Filen %s har skickats till skrivarens lagringsutrymme och kan visas på skrivaren." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozzeltypen är inte angiven. Ange nozzeln och försök igen." @@ -10357,6 +10406,14 @@ msgstr "Visa förinställningar som inte stöds" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Visa inkompatibla förinställningar och förinställningar som inte stöds i rullgardinslistorna för skrivare och filament. Dessa förinställningar kan inte väljas." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Experimentella funktioner" @@ -10608,9 +10665,6 @@ msgstr "Växla till modell publicerings hemsidan" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Notera: Förberedelserna kan ta flera minuter. Vänligen vänta." -msgid "Publish" -msgstr "Publicera" - msgid "Publish was canceled" msgstr "Publiceringen avbröts" @@ -10627,6 +10681,21 @@ msgstr "Laddar upp data" msgid "Jump to webpage" msgstr "Växla till hemsidan" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Spara %s som" @@ -10637,10 +10706,22 @@ msgstr "Användar förinställning" msgid "Preset Inside Project" msgstr "Projekt förinställning" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Koppla loss från överordnad" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Namnet ej tillgängligt." @@ -11459,23 +11540,6 @@ msgstr "Är du säker på att du vill aktivera det här alternativet?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Ifyllnadsmönster är oftast konstruerade för att hantera rotation automatiskt så att de skrivs ut korrekt och ger avsedd effekt (t.ex. Gyroid, Kubisk). Att rotera det aktuella sparsamma ifyllnadsmönstret kan ge otillräckligt stöd. Var försiktig och kontrollera noga om det uppstår utskriftsproblem. Är du säker på att du vill aktivera det här alternativet?" -# AI Translated -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Lagerhöjden är för liten.\n" -"Den ställs in på min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." - -msgid "Adjust to the set range automatically?\n" -msgstr "Justera automatiskt till det inställda området?\n" - -msgid "Adjust" -msgstr "Justera" - # AI Translated msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Experimentell funktion: Filamentet dras tillbaka och kapas på ett längre avstånd vid filamentbyten för att minimera rensningen. Det kan minska rensningen avsevärt, men kan också öka risken för igensatt nozzel eller andra utskriftsproblem." @@ -11707,6 +11771,9 @@ msgstr "Hittade reserverade nyckelord" msgid "Setting Overrides" msgstr "Åsidosätter inställningar" +msgid "Retraction when switching material" +msgstr "Reduktion vid material byte" + msgid "Basic information" msgstr "Allmän information" @@ -11848,6 +11915,14 @@ msgstr "Kompatibla process profiler" msgid "Printable space" msgstr "Utskriftsbar yta" +# AI Translated +msgid "Printer Agent" +msgstr "Skrivaragent" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." + # AI Translated #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format @@ -11992,9 +12067,6 @@ msgstr "Lagerhöjds begränsning" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Reduktion vid material byte" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -13486,6 +13558,9 @@ msgstr " är för nära uteslutningsområdet, och kollisioner kommer att orsakas msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ligger för nära området för klumpdetektering, vilket kommer att orsaka kollisioner.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "De valda nozzeltemperaturerna är inkompatibla. Varje filaments nozzeltemperatur måste ligga inom de andra filamentens rekommenderade nozzeltemperaturintervall. Annars kan nozzeln sättas igen eller skrivaren skadas." @@ -13856,10 +13931,6 @@ msgstr "Använd 3MF i stället för G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Aktivera detta om skrivaren tar emot en 3MF-fil som utskriftsjobb. När det är aktiverat skickar Orca Slicer den beredda filen som en .gcode.3mf i stället för en vanlig .gcode-fil." -# AI Translated -msgid "Printer Agent" -msgstr "Skrivaragent" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren." @@ -14616,9 +14687,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Hastighet för inre bridges. Om värdet anges i procent beräknas det utifrån bridge_speed. Standardvärdet är 150 %." -msgid "Brim width" -msgstr "Brim bredd" - msgid "This is the distance from the model to the outermost brim line." msgstr "Avståndet från modellen till yttersta brim linjen" @@ -14707,6 +14775,12 @@ msgstr "" "Geometrin decimeras innan skarpa vinklar detekteras. Den här parametern anger avvikelsens minsta längd för decimeringen.\n" "0 för att avaktivera." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "uppåt kompatibel maskin" @@ -16039,6 +16113,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Acceleration av fyllning av toppytan. Att använda ett lägre värde kan förbättra ytkvaliteten" @@ -16651,6 +16731,12 @@ msgstr "Vilken typ av G-kod är skrivaren kompatibel med" msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + # AI Translated msgid "Pellet Modded Printer" msgstr "Skrivare ombyggd för pellets" @@ -17868,6 +17954,12 @@ msgstr "Lång reduktion vid extruderbyte" msgid "Retraction distance when extruder change" msgstr "Reduktionssträcka vid extruderbyte" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + # AI Translated msgid "Z-hop height" msgstr "Z-hop-höjd" @@ -17983,6 +18075,9 @@ msgstr "Extra längd vid omstart" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "När reduktionen kompenseras efter flyttrörelsen trycker extrudern fram den här extra mängden filament. Den här inställningen behövs sällan." +msgid "Extra length on restart (Toolchange)" +msgstr "" + # AI Translated msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "När reduktionen kompenseras efter verktygsbyte trycker extrudern fram den här extra mängden filament." @@ -18477,6 +18572,12 @@ msgstr "Verktygsbyte vid prime tornet" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Tvinga verktygshuvudet att flytta till prime tornet innan verktygsbyteskommandot (Tx) skickas. Endast relevant för skrivare med flera extrudrar (flera verktygshuvuden) som använder ett prime torn av typ 2. Som standard hoppar Orca över flytten på maskiner med flera verktygshuvuden, eftersom den fasta programvaran hanterar huvudbytet, vilket kan leda till att Tx-kommandot skickas ovanför den utskrivna delen. Aktivera det här alternativet om du vill att verktygsbytet alltid ska ske ovanför prime tornet i stället." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + # AI Translated msgid "No sparse layers (beta)" msgstr "Inga glesa lager (beta)" @@ -22101,10 +22202,6 @@ msgstr "Fysisk printer" msgid "Print Host upload" msgstr "Uppladdning utskriftsvärd" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Välj vilken nätverksagentimplementation som ska användas för kommunikation med skrivaren. Tillgängliga agenter registreras vid start." - # AI Translated msgid "Select a Flashforge printer" msgstr "Välj en Flashforge-skrivare" @@ -23181,10 +23278,6 @@ msgstr "Något oväntat hände vid inloggningen, försök igen." msgid "User canceled." msgstr "Användaren avbröt." -# AI Translated -msgid "Head diameter" -msgstr "Huvuddiameter" - # AI Translated msgid "Max angle" msgstr "Maxvinkel" @@ -24071,6 +24164,24 @@ msgstr "" "Undvik vridning\n" "Visste du att när du skriver ut material som är benägna att vrida, såsom ABS, kan en lämplig ökning av värmebäddens temperatur minska sannolikheten för vridning?" +# AI Translated +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Lagerhöjden är för liten.\n" +#~ "Den ställs in på min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Lagerhöjden överskrider gränsen i Skrivarinställningar -> Extruder -> Lagerhöjds gränser, detta kan orsaka problem med utskriftskvaliteten." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Justera automatiskt till det inställda området?\n" + +# AI Translated +#~ msgid "Head diameter" +#~ msgstr "Huvuddiameter" + # AI Translated #~ msgid "Print order within a single layer." #~ msgstr "Utskriftsordning inom ett enskilt lager." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index a419ba320e..7f86af0e5e 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -4720,6 +4720,20 @@ msgstr "อุณหภูมิห้องพิมพ์ปัจจุบั msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "อุณหภูมิห้องพิมพ์ต่ำสุด (%d℃) สูงกว่าอุณหภูมิห้องพิมพ์เป้าหมาย (%d℃) ค่าต่ำสุดคือเกณฑ์ที่การพิมพ์จะเริ่มต้นในขณะที่ห้องพิมพ์ยังคงร้อนขึ้นไปสู่เป้าหมาย จึงไม่ควรเกินค่าเป้าหมาย ระบบจะจำกัดค่าให้เท่ากับเป้าหมาย" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "ปรับ" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4840,6 +4854,12 @@ msgstr "" "ใช่ - เปิดใช้งาน Arachne Wall Generator\n" "ไม่ - ปิดการใช้งาน Arachne Wall Generator และตั้งค่าโหมด [Displacement] ของ Fuzzy Skin" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "ความกว้าง ขอบยึดชิ้นงาน" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "โหมดเกลียวจะทำงานเฉพาะเมื่อลูปติดผนังเป็น 1, ปิดใช้งานส่วนรองรับ, การตรวจจับการจับตัวเป็นก้อนโดยการตรวจวัดถูกปิดใช้งาน, ชั้นเปลือกด้านบนเป็น 0, ความหนาแน่นของไส้ในแบบกระจายเป็น 0 และประเภทไทม์แลปส์เป็นแบบดั้งเดิม" @@ -4855,6 +4875,9 @@ msgstr "" "ใช่ - เปลี่ยนการตั้งค่าเหล่านี้และเปิดใช้งานโหมดเกลียวโดยอัตโนมัติ\n" "ไม่ - เลิกใช้โหมดเกลียวในครั้งนี้" +msgid "N/A" +msgstr "ไม่มี" + msgid "Printing" msgstr "กำลังพิมพ์" @@ -5094,6 +5117,12 @@ msgstr "ไม่สามารถสร้าง cali G-code" msgid "Calibration error" msgstr "ข้อผิดพลาดในการสอบเทียบ" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "เครือข่ายไม่พร้อมใช้งาน" @@ -5306,9 +5335,6 @@ msgstr "รูปแบบไม่ถูกต้อง ใช้ N, N#K หร msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "รูปแบบไม่ถูกต้อง รูปแบบเวกเตอร์ที่ต้องการ: \"%1%\"" -msgid "N/A" -msgstr "ไม่มี" - # AI Translated msgid "System agents" msgstr "เอเจนต์ระบบ" @@ -5952,7 +5978,7 @@ msgstr "ปริมาณ:" msgid "Size:" msgstr "ขนาด:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "พบความขัดแย้งของเส้นทางรหัส G ที่เลเยอร์ %d, Z = %.2lfmm โปรดแยกวัตถุที่ขัดแย้งกันให้ไกลออกไป (%s <-> %s)" @@ -6133,6 +6159,9 @@ msgstr "หลายอุปกรณ์" msgid "Project" msgstr "โปรเจกต์" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "ใช่" @@ -6260,6 +6289,12 @@ msgstr "บันทึกโปรเจกต์เป็น" msgid "Save current project as" msgstr "บันทึกโครงการปัจจุบันเป็น" +msgid "Publish" +msgstr "เผยแพร่" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "นำเข้า 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8038,6 +8073,12 @@ msgstr "โปรดยืนยันว่ารหัส G ภายในค msgid "Customized Preset" msgstr "ค่าที่ตั้งไว้ล่วงหน้าที่กำหนดเอง" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "ชื่อของส่วนประกอบภายในไฟล์ STEP ไม่ใช่รูปแบบ UTF8!" @@ -8199,19 +8240,19 @@ msgstr "ไม่ได้เลือกไดเรกทอรีสำหร msgid "Replaced with 3D files from directory:\n" msgstr "แทนที่ด้วยไฟล์ 3D จากไดเรกทอรี:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ ข้าม %s: ไฟล์เดียวกัน\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ ข้าม %s: ไม่มีไฟล์อยู่\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ ข้าม %s: ไม่สามารถแทนที่ได้\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔แทนที่ %s\n" @@ -8421,6 +8462,14 @@ msgstr "บันทึกไฟล์ที่สไลซ์เป็น:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "ไฟล์ %s ถูกส่งไปยังพื้นที่เก็บข้อมูลของเครื่องพิมพ์แล้ว และสามารถดูได้บนเครื่องพิมพ์" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง" @@ -9299,6 +9348,14 @@ msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้ msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "แสดงค่าที่ตั้งไว้ล่วงหน้าที่ไม่เข้ากันหรือไม่รองรับในรายการเลือกเครื่องพิมพ์และเส้นพลาสติก ไม่สามารถเลือกค่าที่ตั้งไว้ล่วงหน้าเหล่านี้ได้" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "ฟีเจอร์ทดลอง" @@ -9535,9 +9592,6 @@ msgstr "ข้ามไปที่โมเดลเผยแพร่หน้ msgid "Note: The preparation may take several minutes. Please be patient." msgstr "หมายเหตุ: การเตรียมการอาจใช้เวลาหลายนาที กรุณาอดทน." -msgid "Publish" -msgstr "เผยแพร่" - msgid "Publish was canceled" msgstr "การเผยแพร่ถูกยกเลิก" @@ -9553,6 +9607,21 @@ msgstr "กำลังอัพโหลดข้อมูล" msgid "Jump to webpage" msgstr "ข้ามไปที่หน้าเว็บ" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "บันทึก %s เป็น" @@ -9563,9 +9632,21 @@ msgstr "พรีเซ็ตผู้ใช้" msgid "Preset Inside Project" msgstr "พรีเซ็ตภายในโปรเจ็กต์" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "แยกออกจากพรีเซ็ตแม่" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "ชื่อไม่พร้อมใช้งาน" @@ -10305,22 +10386,6 @@ msgstr "คุณแน่ใจหรือไม่ว่าต้องกา msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "โดยทั่วไปรูปแบบไส้ในได้รับการออกแบบให้รองรับการหมุนโดยอัตโนมัติเพื่อให้แน่ใจว่าการพิมพ์ถูกต้องและบรรลุผลตามที่ต้องการ (เช่น Gyroid, ลูกบาศก์) การหมุนรูปแบบ ไส้ใน แบบกระจัดกระจายในปัจจุบันอาจทำให้ส่วนรองรับไม่เพียงพอ โปรดดำเนินการด้วยความระมัดระวังและตรวจสอบปัญหาการพิมพ์ที่อาจเกิดขึ้นอย่างละเอียด คุณแน่ใจหรือไม่ว่าต้องการเปิดใช้งานตัวเลือกนี้" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"ความสูงของเลเยอร์น้อยเกินไป\n" -"มันจะตั้งค่าเป็น min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" - -msgid "Adjust to the set range automatically?\n" -msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" - -msgid "Adjust" -msgstr "ปรับ" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "คุณลักษณะการทดลอง: การดึงกลับและตัดเส้นพลาสติกออกในระยะห่างที่มากขึ้นระหว่างการเปลี่ยนเส้นพลาสติกเพื่อลดการไล่เส้น แม้ว่าจะสามารถลดการไล่เส้นได้อย่างเห็นได้ชัด แต่ก็อาจเพิ่มความเสี่ยงของการอุดตันของหัวฉีดหรือภาวะแทรกซ้อนในการพิมพ์อื่นๆ อีกด้วย" @@ -10513,6 +10578,9 @@ msgstr "พบคีย์เวิร์ดที่สงวนไว้" msgid "Setting Overrides" msgstr "การตั้งค่าการแทนที่" +msgid "Retraction when switching material" +msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" + msgid "Basic information" msgstr "ข้อมูลพื้นฐาน" @@ -10642,6 +10710,12 @@ msgstr "โปรไฟล์กระบวนการที่เข้าก msgid "Printable space" msgstr "พื้นที่ที่สามารถพิมพ์ได้" +msgid "Printer Agent" +msgstr "ตัวแทนเครื่องพิมพ์" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10767,9 +10841,6 @@ msgstr "การจำกัดความสูงของเลเยอร msgid "Z-Hop" msgstr "ยกแกน Z" -msgid "Retraction when switching material" -msgstr "การร่นกลับเมื่อเปลี่ยนวัสดุ" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12111,6 +12182,9 @@ msgstr "อยู่ใกล้เขตหวงห้ามมากเกิ msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "อยู่ใกล้พื้นที่การตรวจจับการจับตัวกันมากเกินไป และจะเกิดการชนกัน\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "อุณหภูมิหัวฉีดที่เลือกเข้ากันไม่ได้ อุณหภูมิหัวฉีดของเส้นพลาสติกแต่ละเส้นต้องอยู่ในช่วงอุณหภูมิหัวฉีดที่แนะนำของเส้นพลาสติกอื่นๆ มิฉะนั้นอาจเกิดการอุดตันของหัวฉีดหรือเครื่องพิมพ์เสียหายได้" @@ -12426,9 +12500,6 @@ msgstr "ใช้ 3MF แทน G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "เปิดใช้งานหากเครื่องพิมพ์รับไฟล์ 3MF เป็นงานพิมพ์ เมื่อเปิดใช้งาน OrcaSlicer จะส่งไฟล์ที่สไลซ์แล้วเป็น .gcode.3mf แทนไฟล์ .gcode ธรรมดา" -msgid "Printer Agent" -msgstr "ตัวแทนเครื่องพิมพ์" - msgid "Select the network agent implementation for printer communication." msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์" @@ -13103,9 +13174,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "ความเร็วของสะพานภายใน หากค่าแสดงเป็นเปอร์เซ็นต์ ค่าดังกล่าวจะถูกคำนวณตาม bridge_speed ค่าเริ่มต้นคือ 150%" -msgid "Brim width" -msgstr "ความกว้าง ขอบยึดชิ้นงาน" - msgid "This is the distance from the model to the outermost brim line." msgstr "ระยะห่างจากแบบจำลองถึงเส้นขอบยึดชิ้นงานด้านนอกสุด" @@ -13185,6 +13253,12 @@ msgstr "" "รูปทรงจะถูกทำลายก่อนที่จะตรวจจับมุมแหลม พารามิเตอร์นี้ระบุความยาวขั้นต่ำของการเบี่ยงเบนสำหรับการทำลาย\n" "0 เพื่อปิดการใช้งาน" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "เครื่องที่รองรับขึ้นไป" @@ -14351,6 +14425,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "ไจรอยด์" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "ความเร่งของไส้ในพื้นผิวด้านบน การใช้ค่าที่ต่ำกว่าอาจปรับปรุงคุณภาพพื้นผิวด้านบนได้" @@ -14893,6 +14973,12 @@ msgstr "เครื่องพิมพ์ G-code ชนิดใดที่ msgid "Klipper" msgstr "คลิปเปอร์" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "เครื่องพิมพ์ Modded เม็ด" @@ -15945,6 +16031,12 @@ msgstr "การถอยกลับนานเมื่อเปลี่ย msgid "Retraction distance when extruder change" msgstr "ระยะการดึงกลับเมื่อชุดดันเส้นเปลี่ยน" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "ความสูงยกแกน Z" @@ -16039,6 +16131,9 @@ msgstr "ความยาวพิเศษเมื่อรีสตาร์ msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "เมื่อชดเชยการดึงกลับหลังการเคลื่อนที่เดินทาง ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้ การตั้งค่านี้ไม่ค่อยจำเป็น" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "เมื่อชดเชยการดึงกลับหลังเปลี่ยนเครื่องมือ ชุดดันเส้นจะดันเส้นพลาสติกเพิ่มเติมในปริมาณนี้" @@ -16451,6 +16546,12 @@ msgstr "การเปลี่ยนเครื่องมือบน Wipe msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "บังคับให้หัวเครื่องมือเคลื่อนที่ไปที่ Wipe Tower ก่อนที่จะออกคำสั่งเปลี่ยนเครื่องมือ (Tx) เกี่ยวข้องเฉพาะกับเครื่องพิมพ์ที่มีชุดดันเส้นหลายเครื่อง (หลายหัวเครื่องมือ) ที่ใช้แผ่นเช็ดแบบ Type 2 ตามค่าเริ่มต้น Orca จะข้ามการเดินทางบนเครื่องที่มีหัวเครื่องมือหลายหัวเนื่องจากเฟิร์มแวร์จัดการการสลับหัว ซึ่งอาจส่งผลให้มีการออกคำสั่ง Tx เหนือส่วนที่พิมพ์ เปิดใช้งานตัวเลือกนี้หากคุณต้องการให้ทำการเปลี่ยนแปลงเครื่องมือเหนือ Wipe Tower แทนเสมอ" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "ไม่มีชั้นกระจัดกระจาย (เบต้า)" @@ -19681,9 +19782,6 @@ msgstr "เครื่องพิมพ์ทางกายภาพ" msgid "Print Host upload" msgstr "อัพโหลดโฮสต์การพิมพ์" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "เลือกการใช้งานตัวแทนเครือข่ายสำหรับการสื่อสารของเครื่องพิมพ์ ตัวแทนที่มีอยู่จะได้รับการลงทะเบียนเมื่อเริ่มต้น" - msgid "Select a Flashforge printer" msgstr "เลือกเครื่องพิมพ์ Flashforge" @@ -20575,9 +20673,6 @@ msgstr "เกิดสิ่งที่ไม่คาดคิดขณะพ msgid "User canceled." msgstr "ผู้ใช้ยกเลิก" -msgid "Head diameter" -msgstr "เส้นผ่านศูนย์กลางหัว" - msgid "Max angle" msgstr "มุมสูงสุด" @@ -21361,6 +21456,22 @@ msgstr "" "หลีกเลี่ยงการบิดเบี้ยว\n" "คุณรู้หรือไม่ว่าเมื่อพิมพ์วัสดุที่มีแนวโน้มที่จะเกิดการบิดเบี้ยว เช่น ABS การเพิ่มอุณหภูมิฐานพิมพ์อย่างเหมาะสมสามารถลดความน่าจะเป็นของการบิดเบี้ยวได้" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "ความสูงของเลเยอร์น้อยเกินไป\n" +#~ "มันจะตั้งค่าเป็น min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "ความสูงของเลเยอร์เกินขีดจำกัดในการตั้งค่าเครื่องพิมพ์ -> ชุดดันเส้น -> ขีดจำกัดความสูงของเลเยอร์ ซึ่งอาจทำให้เกิดปัญหาคุณภาพการพิมพ์" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "ปรับเป็นช่วงที่ตั้งไว้อัตโนมัติ?\n" + +#~ msgid "Head diameter" +#~ msgstr "เส้นผ่านศูนย์กลางหัว" + #~ msgid "Print order within a single layer." #~ msgstr "สั่งพิมพ์ภายในชั้นเดียว" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 467b3c355b..944c322a94 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-08-01 20:32+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -4812,6 +4812,20 @@ msgstr "Mevcut hazne sıcaklığı malzemenin güvenli sıcaklığından yüksek msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Minimum oda sıcaklığı (%d℃), hedef oda sıcaklığından (%d℃) yüksek. Minimum değer, oda hedefe doğru ısınmaya devam ederken baskının başladığı eşiktir; bu nedenle hedefi aşmamalıdır. Değer hedefe sınırlandırılacak." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Ayarla" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4932,6 +4946,12 @@ msgstr "" "Evet - Arachne Duvarı Oluşturucusunu Etkinleştir\n" "Hayır - Arachne Duvarı Oluşturucusunu Devre Dışı Bırak ve Pütürlü Yüzey [Yer Değiştirme] modunu ayarla" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Kenar genişliği" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Spiral mod yalnızca duvar döngüleri 1 olduğunda, destek devre dışı bırakıldığında, problama yoluyla topaklanma tespiti devre dışı bırakıldığında, üst kabuk katmanları 0 olduğunda, seyrek dolgu yoğunluğu 0 olduğunda ve hızlandırılmış tip geleneksel olduğunda çalışır." @@ -4947,6 +4967,10 @@ msgstr "" "Evet - Bu ayarları değiştirin ve spiral modunu otomatik olarak etkinleştirin\n" "Hayır - Bu sefer spiral modunu kullanmaktan vazgeçin" +# AI Translated +msgid "N/A" +msgstr "Yok" + msgid "Printing" msgstr "Baskı" @@ -5186,6 +5210,12 @@ msgstr "Cali G-code oluşturma başarısız oldu" msgid "Calibration error" msgstr "Kalibrasyon hatası" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Ağ kullanılamıyor" @@ -5398,10 +5428,6 @@ msgstr "Geçersiz kalıp. N, N#K veya giriş başına isteğe bağlı #K ile vir msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Geçersiz format. Beklenen vektör formatı: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "Yok" - # AI Translated msgid "System agents" msgstr "Sistem aracıları" @@ -6050,7 +6076,7 @@ msgstr "Hacim:" msgid "Size:" msgstr "Boyut:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "%d katmanında gcode yollarında çakışmalar bulundu, Z = %.2lfmm. Lütfen çakışan nesneleri daha uzağa ayırın (%s <-> %s)." @@ -6232,6 +6258,9 @@ msgstr "Çoklu cihaz" msgid "Project" msgstr "Proje" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Evet" @@ -6360,6 +6389,12 @@ msgstr "Projeyi farklı kaydet" msgid "Save current project as" msgstr "Mevcut projeyi farklı kaydet" +msgid "Publish" +msgstr "Yayınla" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF'yi içe aktar" @@ -8170,6 +8205,12 @@ msgstr "Lütfen bu ön ayarlar içindeki G-kodlarının makineye herhangi bir za msgid "Customized Preset" msgstr "Özel Ayar" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Step dosyasındaki bileşenlerin adı UTF8 formatında değil!" @@ -8335,19 +8376,19 @@ msgstr "Değiştirme için dizin seçilmedi" msgid "Replaced with 3D files from directory:\n" msgstr "Dizindeki 3D dosyalarla değiştirildi:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ %s atlandı: aynı dosya.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ %s atlandı: dosya mevcut değil.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ %s atlandı: değiştirilemedi.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ %s değiştirildi.\n" @@ -8557,6 +8598,14 @@ msgstr "Dilimlenmiş dosyayı şu şekilde kaydedin:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s dosyası yazıcının depolama alanına gönderildi ve yazıcıda görüntülenebiliyor." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozul tipi ayarlanmamış. Lütfen nozulu ayarlayın ve tekrar deneyin." @@ -9477,6 +9526,14 @@ msgstr "Desteklenmeyen ön ayarları göster" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Yazıcı ve filament açılır listelerinde uyumsuz/desteklenmeyen ön ayarları gösterir. Bu ön ayarlar seçilemez." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Deneysel Özellikler" @@ -9716,9 +9773,6 @@ msgstr "Model yayınlama web sayfasına git" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Not: Hazırlık birkaç dakika sürebilir. Lütfen sabırlı olun." -msgid "Publish" -msgstr "Yayınla" - msgid "Publish was canceled" msgstr "Yayınlama iptal edildi" @@ -9734,6 +9788,21 @@ msgstr "Veriler yükleniyor" msgid "Jump to webpage" msgstr "Web sayfasına atla" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s'yi farklı kaydet" @@ -9744,9 +9813,21 @@ msgstr "Kullanıcı Ön Ayarı" msgid "Preset Inside Project" msgstr "Ön ayar içerisinde proje" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "Ebeveynden ayrıl" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Ad kullanılamıyor." @@ -10496,22 +10577,6 @@ msgstr "Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Dolgu desenleri genellikle, doğru baskı alınmasını ve istenen etkilerin (ör. Gyroid, Kübik) elde edilmesini sağlamak için döndürme işlemini otomatik olarak yapacak şekilde tasarlanmıştır. Mevcut seyrek dolgu desenini döndürmek, yetersiz destekle sonuçlanabilir. Lütfen dikkatli ilerleyin ve olası baskı sorunlarını iyice kontrol edin. Bu seçeneği etkinleştirmek istediğinizden emin misiniz?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Katman yüksekliği çok küçük.\n" -"min_layer_height olarak ayarlanacak\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." - -msgid "Adjust to the set range automatically?\n" -msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" - -msgid "Adjust" -msgstr "Ayarla" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Deneysel özellik: Filament değişiklikleri sırasında, floşu en aza indirmek için filamenti daha büyük bir mesafeden geri çekmek ve kesmek. Flush’u önemli ölçüde azaltabilmesine rağmen, aynı zamanda nozul tıkanmaları veya diğer yazdırma komplikasyonları riskini de artırabilir." @@ -10710,6 +10775,9 @@ msgstr "Ayrılmış anahtar kelimeler bulundu" msgid "Setting Overrides" msgstr "Ayarların Üzerine Yazma" +msgid "Retraction when switching material" +msgstr "Malzemeyi Değiştirirken Geri Çekme" + msgid "Basic information" msgstr "Temel Bilgiler" @@ -10843,6 +10911,12 @@ msgstr "Uyumlu süreç profilleri" msgid "Printable space" msgstr "Plaka Ayarı" +msgid "Printer Agent" +msgstr "Yazıcı Aracısı" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10973,9 +11047,6 @@ msgstr "Katman Yüksekliği Sınırları" msgid "Z-Hop" msgstr "Z Sıçraması" -msgid "Retraction when switching material" -msgstr "Malzemeyi Değiştirirken Geri Çekme" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12353,6 +12424,9 @@ msgstr " dışlama alanına çok yakın ve çarpışmalara neden olacak.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " topaklanma algılama alanına çok yakın, çarpışmalar meydana gelecektir.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Seçilen nozul sıcaklıkları uyumsuz. Her filamentin nozul sıcaklığı, diğer filamentlerin önerilen nozul sıcaklığı aralığında olmalıdır. Aksi hâlde nozul tıkanması veya yazıcıda hasar oluşabilir." @@ -12688,9 +12762,6 @@ msgstr "G-code yerine 3MF kullan" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Yazıcı, baskı işi olarak 3MF dosyası kabul ediyorsa bunu etkinleştirin. Etkinleştirildiğinde Orca Slicer, dilimlenmiş dosyayı düz bir .gcode dosyası yerine .gcode.3mf olarak gönderir." -msgid "Printer Agent" -msgstr "Yazıcı Aracısı" - msgid "Select the network agent implementation for printer communication." msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin." @@ -13376,9 +13447,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "İç köprülerin hızı. Değer yüzde olarak ifade edilirse köprü hızına göre hesaplanacaktır. Varsayılan değer %150’dir." -msgid "Brim width" -msgstr "Kenar genişliği" - msgid "This is the distance from the model to the outermost brim line." msgstr "Modelden en dış kenar çizgisine kadar olan mesafe." @@ -13463,6 +13531,12 @@ msgstr "" "Keskin açılar algılanmadan önce geometri azaltılacaktır. Bu parametre, azaltma için minimum sapma uzunluğunu belirtir.\n" "Devre dışı bırakmak için 0." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "yukarı doğru uyumlu makine" @@ -14647,6 +14721,12 @@ msgstr "Tpms-fk" msgid "Gyroid" msgstr "Jiroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Üst yüzey dolgusunun hızlandırılması. Daha düşük bir değerin kullanılması üst yüzey kalitesini iyileştirebilir." @@ -15205,6 +15285,12 @@ msgstr "Yazıcının ne tür bir gcode ile uyumlu olduğu." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Pelet modlu yazıcı" @@ -16292,6 +16378,12 @@ msgstr "Ekstruder değiştiğinde uzun geri çekilme" msgid "Retraction distance when extruder change" msgstr "Ekstruder değiştiğinde geri çekilme mesafesi" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z-Sıçrama yüksekliği" @@ -16391,6 +16483,9 @@ msgstr "Yeniden başlatma sırasında ekstra uzunluk" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "İlerleme hareketinden sonra geri çekilme telafi edildiğinde, ekstruder bu ek filament miktarını itecektir. Bu ayara nadiren ihtiyaç duyulur." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Takım değiştirildikten sonra geri çekilme telafi edildiğinde, ekstruder bu ilave filament miktarını itecektir." @@ -16808,6 +16903,12 @@ msgstr "Silme kulesinde takım değişimi" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Takım değişimi komutu (Tx) verilmeden önce baskı kafasını silme kulesine gitmeye zorlar. Yalnızca Tip 2 silme kulesi kullanan çok ekstruderli (çok baskı kafalı) yazıcılar için geçerlidir. Orca, çok baskı kafalı makinelerde bu seyahati varsayılan olarak atlar çünkü kafa değişimini ürün yazılımı yönetir; bu da Tx komutunun yazdırılan parçanın üzerinde verilmesine yol açabilir. Takım değişiminin her zaman silme kulesinin üzerinde verilmesini istiyorsanız bu seçeneği etkinleştirin." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Seyrek katman yok (beta)" @@ -20092,9 +20193,6 @@ msgstr "Fiziksel Yazıcı" msgid "Print Host upload" msgstr "Yazıcı Bağlantı Ayarları" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Yazıcı iletişimi için ağ aracısı uygulamasını seçin. Kullanılabilir aracılar başlangıçta kaydedilir." - # AI Translated msgid "Select a Flashforge printer" msgstr "Bir Flashforge yazıcısı seçin" @@ -21037,9 +21135,6 @@ msgstr "Giriş yapmaya çalışırken beklenmeyen bir şey oldu, lütfen tekrar msgid "User canceled." msgstr "Kullanıcı iptal edildi." -msgid "Head diameter" -msgstr "Kafa çapı" - msgid "Max angle" msgstr "Maksimum açı" @@ -21768,7 +21863,8 @@ msgstr "" "Baskılarınızı plakalara ayırın\n" "Çok sayıda parçası olan bir modeli baskıya hazır ayrı kalıplara bölebileceğinizi biliyor muydunuz? Bu, tüm parçaları takip etme sürecini basitleştirecektir." -#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer Height] +#: resources/data/hints.ini: [hint:Speed up your print with Adaptive Layer +#: Height] msgid "" "Speed up your print with Adaptive Layer Height\n" "Did you know that you can print a model even faster by using the Adaptive Layer Height option? Check it out!" @@ -21841,7 +21937,8 @@ msgstr "" "Gücü artırın\n" "Modelin gücünü artırmak için daha fazla duvar halkası ve daha yüksek seyrek dolgu yoğunluğu kullanabileceğinizi biliyor muydunuz?" -#: resources/data/hints.ini: [hint:When do you need to print with the printer door opened] +#: resources/data/hints.ini: [hint:When do you need to print with the printer +#: door opened] msgid "" "When do you need to print with the printer door opened?\n" "Did you know that opening the printer door can reduce the probability of extruder/hotend clogging when printing lower temperature filament with a higher enclosure temperature? There is more info about this in the Wiki." @@ -21857,6 +21954,22 @@ msgstr "" "Eğilmeyi önleyin\n" "ABS gibi bükülmeye yatkın malzemelere baskı yaparken, ısıtma yatağı sıcaklığının uygun şekilde arttırılmasının bükülme olasılığını azaltabileceğini biliyor muydunuz?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Katman yüksekliği çok küçük.\n" +#~ "min_layer_height olarak ayarlanacak\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Katman yüksekliği, Yazıcı Ayarları -> Ekstruder -> Katman yüksekliği sınırları bölümündeki sınırı aşıyor bu durum baskı kalitesi sorunlarına neden olabilir." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Ayarlanan aralığa otomatik olarak ayarlansın mı?\n" + +#~ msgid "Head diameter" +#~ msgstr "Kafa çapı" + #~ msgid "Print order within a single layer." #~ msgstr "Tek bir katmanda yazdırma sırası." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 9204a67ec3..5c162e8475 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk \n" "Language-Team: Ukrainian\n" @@ -4716,6 +4716,20 @@ msgstr "Поточна температура камери вища, ніж бе msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Мінімальна температура камери (%d℃) вища за цільову температуру камери (%d℃). Мінімальне значення — це поріг, за якого починається друк, поки камера продовжує нагріватися до цільової температури, тому воно не повинно її перевищувати. Значення буде обмежено цільовим." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Налаштувати" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4839,6 +4853,12 @@ msgstr "" "Так - Увімкнути генератор стінок Arachne\n" "Ні - Вимкнути генератор стінок Arachne і встановити режим [Зміщення] для шорсткої поверхні" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Ширина кайми" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Спіральний режим працює лише тоді, коли кількість стінок дорівнює 1, підтримки вимкнено, виявлення налипання зондуванням вимкнено, кількість верхніх шарів оболонки дорівнює 0, щільність часткового заповнення дорівнює 0, а тип таймлапсу — традиційний." @@ -4855,6 +4875,9 @@ msgstr "" "Так – змінити ці налаштування та автоматично включити режим спіральна ваза\n" "Ні - цього разу відмовитися від використання режиму спіральна ваза" +msgid "N/A" +msgstr "Н/Д" + msgid "Printing" msgstr "Друк" @@ -5104,6 +5127,12 @@ msgstr "Не вдалося згенерувати калібрувальний msgid "Calibration error" msgstr "Помилка калібрування" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Мережа недоступна" @@ -5318,9 +5347,6 @@ msgstr "Некоректний шаблон. Використовуйте N, N#K msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Невірний формат. Очікуваний векторний формат: \"%1%\"" -msgid "N/A" -msgstr "Н/Д" - # AI Translated msgid "System agents" msgstr "Системні агенти" @@ -5978,7 +6004,7 @@ msgid "Size:" msgstr "Розмір:" # AI Translated -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Виявлено конфлікти шляхів G-коду на шарі %d, Z = %.2lf мм. Будь ласка, рознесіть конфліктуючі обʼєкти далі один від одного (%s <-> %s)." @@ -6170,6 +6196,9 @@ msgstr "Багато пристроїв" msgid "Project" msgstr "Проєкт" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Так" @@ -6297,6 +6326,12 @@ msgstr "Зберегти проєкт як" msgid "Save current project as" msgstr "Зберегти поточний проєкт як" +msgid "Publish" +msgstr "Публікувати" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8143,6 +8178,12 @@ msgstr "Будь ласка, підтвердьте, що G-коди в цих msgid "Customized Preset" msgstr "Пристосований пресет" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Назви компонентів усередині файлу STEP не у форматі UTF8!" @@ -8306,19 +8347,19 @@ msgstr "Каталог для заміни не вибрано" msgid "Replaced with 3D files from directory:\n" msgstr "Замінено 3D-файлами з каталогу:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Пропущено %s: той самий файл.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Пропущено %s: файл не існує.\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Пропущено %s: не вдалося замінити.\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Замінено %s.\n" @@ -8531,6 +8572,14 @@ msgstr "Зберегти нарізаний файл як:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Файл %s надіслано до памʼяті принтера та доступний для перегляду на принтері." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз." @@ -9446,6 +9495,14 @@ msgstr "Показати непідтримувані пресети" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Показати несумісні/непідтримувані пресети у випадаючому списку принтера і філаменту. Ці пресети не можна вибрати." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + msgid "Experimental Features" msgstr "Експериментальні функції" @@ -9682,9 +9739,6 @@ msgstr "Перейти на веб-сторінку публікації мод msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Примітка. Підготовка може тривати кілька хвилин. Будь ласка, будьте терплячі." -msgid "Publish" -msgstr "Публікувати" - msgid "Publish was canceled" msgstr "Публікація скасована" @@ -9700,6 +9754,21 @@ msgstr "Відвантаження даних" msgid "Jump to webpage" msgstr "Перейти на вебсторінку" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Зберегти %s як" @@ -9710,10 +9779,22 @@ msgstr "Пресети користувача" msgid "Preset Inside Project" msgstr "Налаштування проекту всередині" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Відʼєднати від батьківського" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Назва недоступна." @@ -10492,22 +10573,6 @@ msgstr "Ви впевнені, що хочете ввімкнути цю опц msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Шаблони заповнення зазвичай розроблені так, щоб автоматично враховувати обертання, забезпечувати належний друк і досягати задуманого ефекту (наприклад, Гіроїд, Кубічний). Обертання поточного шаблону часткового заповнення може призвести до недостатньої підтримки. Дійте обережно та ретельно перевіряйте можливі проблеми друку. Ви впевнені, що хочете увімкнути цю опцію?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Висота шару занадто мала.\n" -"Буде встановлено значення min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." - -msgid "Adjust to the set range automatically?\n" -msgstr "Автоматично налаштувати на встановлений діапазон?\n" - -msgid "Adjust" -msgstr "Налаштувати" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Експериментальна функція: Втягування та відрізання філаменту на більшій відстані під час зміни філаменту для мінімізації промивання. Хоча це може помітно зменшити промивання, це також може підвищити ризик засмічення сопла або інших ускладнень друку." @@ -10711,6 +10776,9 @@ msgstr "Знайдено зарезервовані ключові слова" msgid "Setting Overrides" msgstr "Налаштування перевизначень" +msgid "Retraction when switching material" +msgstr "Втягування під час зміни матеріалу" + msgid "Basic information" msgstr "Базова інформація" @@ -10848,6 +10916,13 @@ msgstr "Сумісні профілі процесів" msgid "Printable space" msgstr "Місце для друку" +msgid "Printer Agent" +msgstr "Агент принтера" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10978,9 +11053,6 @@ msgstr "Обмеження висоти шару" msgid "Z-Hop" msgstr "Стрибок-Z" -msgid "Retraction when switching material" -msgstr "Втягування під час зміни матеріалу" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12376,6 +12448,9 @@ msgstr " знаходиться надто близько до зони відч msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " розташовано занадто близько до зони виявлення налипання, і це спричинить зіткнення.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Вибрані температури сопла несумісні. Температура сопла кожного філаменту має входити в рекомендований діапазон температур сопла інших філаментів. Інакше можливе засмічення сопла або пошкодження принтера." @@ -12722,9 +12797,6 @@ msgstr "Використовувати 3MF замість G-коду" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Увімкніть, якщо принтер приймає файл 3MF як завдання друку. Якщо увімкнено, Orca Slicer надсилає нарізаний файл як .gcode.3mf замість звичайного файлу .gcode." -msgid "Printer Agent" -msgstr "Агент принтера" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером." @@ -13438,9 +13510,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Швидкість внутрішніх мостів. Якщо значення вказано у відсотках, воно буде розраховане на основі bridge_speed. Значення за замовчуванням: 150%." -msgid "Brim width" -msgstr "Ширина кайми" - msgid "This is the distance from the model to the outermost brim line." msgstr "Відстань від моделі до останньої зовнішньої лінії кайми" @@ -13525,6 +13594,12 @@ msgstr "" "Геометрія буде оброблена перед детектуванням гострих кутів. Цей параметр вказує мінімальну довжину відхилення для обробки.\n" "0 для вимкнення" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "висхідна сумісна машина" @@ -14734,6 +14809,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Гіроїд" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Прискорення заповнення верхньої поверхні. Використання меншого значенняможе покращити якість верхньої поверхні" @@ -15300,6 +15381,12 @@ msgstr "З яким gcode сумісний принтер" msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Принтер модифікований гранулами" @@ -16438,6 +16525,12 @@ msgstr "Довге втягування при зміні екструдера" msgid "Retraction distance when extruder change" msgstr "Відстань втягування при зміні екструдера" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Висота Z-підйому" @@ -16534,6 +16627,9 @@ msgstr "Додаткова довжина під час перезавантаж msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Коли втягування компенсується після переміщення, екструдер проштовхуєЦе додаткова кількість нитки. Ця установка рідко потрібна." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Коли втягування компенсується після заміни інструменту, екструдерпроштовхує цю додаткову кількість нитки." @@ -16960,6 +17056,12 @@ msgstr "Зміна інструмента на вежі протирання" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Примусово переміщати головку до вежі протирання перед видачею команди зміни інструмента (Tx). Стосується лише багатоекструдерних (багатоінструментальних) принтерів з вежею протирання типу 2. Типово Orca пропускає це переміщення на багатоінструментальних машинах, оскільки заміну головки виконує прошивка, через що команда Tx може бути видана над надрукованою деталлю. Увімкніть цю опцію, якщо хочете, щоб зміна інструмента завжди відбувалася над вежею протирання." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Без розріджених шарів (бета)" @@ -20304,10 +20406,6 @@ msgstr "Фізичний принтер" msgid "Print Host upload" msgstr "Завантаження хоста друку" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Виберіть реалізацію мережевого агента для звʼязку з принтером. Доступні агенти реєструються під час запуску." - msgid "Select a Flashforge printer" msgstr "Вибрати принтер Flashforge" @@ -21181,9 +21279,6 @@ msgstr "Під час спроби входу трапилося щось нес msgid "User canceled." msgstr "Користувача скасовано." -msgid "Head diameter" -msgstr "Діаметр голови" - msgid "Max angle" msgstr "Максимальний кут" @@ -21979,6 +22074,22 @@ msgstr "" "Уникнення деформації\n" "Чи знаєте ви, що при друку матеріалами, схильними до деформації, такими як ABS, відповідне підвищення температури столу може зменшити ймовірність деформації?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Висота шару занадто мала.\n" +#~ "Буде встановлено значення min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Висота шару перевищує ліміт у Налаштуваннях принтера -> Екструдер -> Ліміти висоти шару, це може призвести до проблем з якістю друку." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Автоматично налаштувати на встановлений діапазон?\n" + +#~ msgid "Head diameter" +#~ msgstr "Діаметр голови" + #~ msgid "Print order within a single layer." #~ msgstr "Друк замовлення в один шар" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index e6e7adf43d..f900f3fc1f 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -4975,6 +4975,20 @@ msgstr "Nhiệt độ buồng hiện tại cao hơn nhiệt độ an toàn của msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "Nhiệt độ buồng tối thiểu (%d℃) cao hơn nhiệt độ buồng mục tiêu (%d℃). Giá trị tối thiểu là ngưỡng để bắt đầu in trong khi buồng vẫn tiếp tục gia nhiệt tới mục tiêu, nên nó không được vượt quá giá trị mục tiêu. Nó sẽ được giới hạn về mức mục tiêu." +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "Điều chỉnh" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -5095,6 +5109,12 @@ msgstr "" "Yes - Bật trình tạo wall Arachne\n" "No - Tắt trình tạo wall Arachne và đặt chế độ [Displacement] của Fuzzy Skin" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Độ rộng brim" + # AI Translated msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "Chế độ xoắn ốc chỉ hoạt động khi vòng wall bằng 1, support bị tắt, phát hiện vón cục bằng dò bị tắt, số lớp vỏ trên bằng 0, mật độ infill thưa bằng 0 và loại timelapse là truyền thống." @@ -5111,6 +5131,10 @@ msgstr "" "Yes - Thay đổi các cài đặt này và bật chế độ spiral tự động\n" "No - Từ bỏ dùng chế độ spiral lần này" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Đang in" @@ -5399,6 +5423,12 @@ msgstr "Không thể tạo G-code hiệu chỉnh" msgid "Calibration error" msgstr "Lỗi hiệu chỉnh" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "Mạng không khả dụng" @@ -5621,10 +5651,6 @@ msgstr "Mẫu không hợp lệ. Dùng N, N#K, hoặc danh sách phân cách d msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Định dạng không hợp lệ. Mong đợi định dạng vector: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Tác nhân hệ thống" @@ -6317,7 +6343,7 @@ msgstr "Thể tích:" msgid "Size:" msgstr "Kích thước:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "Đã tìm thấy xung đột đường đi G-code tại lớp %d, Z = %.2lfmm. Vui lòng tách các vật thể xung đột ra xa hơn (%s <-> %s)." @@ -6516,6 +6542,9 @@ msgstr "Nhiều thiết bị" msgid "Project" msgstr "Dự án" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "Có" @@ -6645,6 +6674,12 @@ msgstr "Lưu dự án thành" msgid "Save current project as" msgstr "Lưu dự án hiện tại thành" +msgid "Publish" +msgstr "Xuất bản" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8549,6 +8584,12 @@ msgstr "Vui lòng xác nhận G-code trong các preset này an toàn để ngăn msgid "Customized Preset" msgstr "Preset tùy chỉnh" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Tên của các thành phần bên trong file STEP không phải định dạng UTF8!" @@ -8721,22 +8762,22 @@ msgid "Replaced with 3D files from directory:\n" msgstr "Đã thay thế bằng file 3D từ thư mục:\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ Đã bỏ qua %s: cùng một file.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ Đã bỏ qua %s: file không tồn tại.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ Đã bỏ qua %s: thay thế thất bại.\n" # AI Translated -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ Đã thay thế %s.\n" @@ -8952,6 +8993,14 @@ msgstr "Lưu file đã slice dưới dạng:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "File %s đã được gửi đến không gian lưu trữ của máy in và có thể được xem trên máy in." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Chưa đặt loại đầu phun. Vui lòng đặt đầu phun rồi thử lại." @@ -9947,6 +9996,14 @@ msgstr "Hiện cài đặt sẵn không được hỗ trợ" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "Hiện các cài đặt sẵn không tương thích/không được hỗ trợ trong danh sách thả xuống máy in và filament. Không thể chọn các cài đặt sẵn này." +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "Tính năng thử nghiệm" @@ -10194,9 +10251,6 @@ msgstr "Chuyển đến trang web xuất bản model" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Lưu ý: Chuẩn bị có thể mất vài phút. Vui lòng kiên nhẫn." -msgid "Publish" -msgstr "Xuất bản" - msgid "Publish was canceled" msgstr "Xuất bản đã bị hủy" @@ -10213,6 +10267,21 @@ msgstr "Đang tải dữ liệu lên" msgid "Jump to webpage" msgstr "Chuyển đến trang web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Lưu %s dưới dạng" @@ -10223,10 +10292,22 @@ msgstr "Preset người dùng" msgid "Preset Inside Project" msgstr "Preset bên trong dự án" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + # AI Translated msgid "Detach from parent" msgstr "Tách khỏi vật thể cha" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "Tên không khả dụng." @@ -11026,22 +11107,6 @@ msgstr "Bạn có chắc chắn muốn bật tùy chọn này?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "Mẫu infill thường được thiết kế để xử lý xoay tự động nhằm đảm bảo in đúng cách và đạt được hiệu quả dự kiến (ví dụ: Gyroid, Cubic). Xoay mẫu infill thưa hiện tại có thể dẫn đến support không đủ . Vui lòng tiến hành thận trọng và kiểm tra kỹ bất kỳ vấn đề in tiềm ẩn nào. Bạn có chắc chắn muốn bật tùy chọn này?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"Chiều cao lớp quá nhỏ.\n" -"Nó sẽ được đặt thành min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." - -msgid "Adjust to the set range automatically?\n" -msgstr "Điều chỉnh về phạm vi đặt tự động?\n" - -msgid "Adjust" -msgstr "Điều chỉnh" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "Tính năng thử nghiệm: Rút và cắt filament ở khoảng cách lớn hơn trong quá trình thay filament để giảm thiểu xả. Mặc dù có thể giảm đáng kể lượng xả, nó cũng có thể làm tăng nguy cơ tắc đầu phun hoặc các vấn đề in khác." @@ -11235,6 +11300,9 @@ msgstr "Tìm thấy từ khóa dành riêng" msgid "Setting Overrides" msgstr "Ghi đè cài đặt" +msgid "Retraction when switching material" +msgstr "Rút khi chuyển vật liệu" + msgid "Basic information" msgstr "Thông tin cơ bản" @@ -11366,6 +11434,14 @@ msgstr "Hồ sơ quy trình tương thích" msgid "Printable space" msgstr "Không gian in" +# AI Translated +msgid "Printer Agent" +msgstr "Tác nhân máy in" + +# AI Translated +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -11498,9 +11574,6 @@ msgstr "Giới hạn chiều cao lớp" msgid "Z-Hop" msgstr "Z-Hop" -msgid "Retraction when switching material" -msgstr "Rút khi chuyển vật liệu" - # AI Translated msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" @@ -12950,6 +13023,9 @@ msgstr " quá gần vùng loại trừ, và sẽ gây va chạm.\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr " ở quá gần vùng phát hiện vón cục, và sẽ gây ra va chạm.\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + # AI Translated msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "Nhiệt độ đầu phun đã chọn không tương thích. Nhiệt độ đầu phun của mỗi filament phải nằm trong dải nhiệt độ đầu phun được khuyến nghị của các filament còn lại. Nếu không, có thể xảy ra tắc đầu phun hoặc hư hỏng máy in." @@ -13291,10 +13367,6 @@ msgstr "Dùng 3MF thay cho G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "Bật tùy chọn này nếu máy in nhận file 3MF làm tác vụ in. Khi bật, Orca Slicer sẽ gửi file đã slice dưới dạng .gcode.3mf thay vì file .gcode thuần." -# AI Translated -msgid "Printer Agent" -msgstr "Tác nhân máy in" - # AI Translated msgid "Select the network agent implementation for printer communication." msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in." @@ -14002,9 +14074,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "Tốc độ của cầu bên trong. Nếu giá trị được biểu thị dưới dạng phần trăm, nó sẽ được tính dựa trên bridge_speed. Giá trị mặc định là 150%." -msgid "Brim width" -msgstr "Độ rộng brim" - msgid "This is the distance from the model to the outermost brim line." msgstr "Khoảng cách từ model đến đường brim ngoài cùng." @@ -14088,6 +14157,12 @@ msgstr "" "Hình học sẽ được giảm trước khi phát hiện góc sắc. Tham số này chỉ ra độ dài tối thiểu của độ lệch cho việc giảm.\n" "0 để vô hiệu hóa." +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "máy tương thích ngược" @@ -15305,6 +15380,12 @@ msgstr "TPMS-FK" msgid "Gyroid" msgstr "Gyroid" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "Gia tốc của infill bề mặt trên. Sử dụng giá trị thấp hơn có thể cải thiện chất lượng bề mặt trên." @@ -15868,6 +15949,12 @@ msgstr "Loại G-code mà máy in tương thích." msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "Máy in Pellet đã chỉnh sửa" @@ -16971,6 +17058,12 @@ msgstr "Rút dài khi đổi extruder" msgid "Retraction distance when extruder change" msgstr "Khoảng cách rút khi đổi extruder" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Chiều cao Z-hop" @@ -17069,6 +17162,9 @@ msgstr "Độ dài bổ sung khi khởi động lại" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "Khi rút được bù sau khi di chuyển, extruder sẽ đẩy lượng filament bổ sung này. Cài đặt này hiếm khi cần thiết." +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "Khi rút được bù sau khi thay công cụ, extruder sẽ đẩy lượng filament bổ sung này." @@ -17489,6 +17585,12 @@ msgstr "Đổi công cụ trên wipe tower" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "Buộc đầu công cụ di chuyển đến wipe tower trước khi phát lệnh đổi công cụ (Tx). Chỉ liên quan đến máy in nhiều extruder (nhiều đầu công cụ) dùng wipe tower Loại 2. Theo mặc định, Orca bỏ qua bước di chuyển này trên máy nhiều đầu công cụ vì firmware tự xử lý việc đổi đầu, điều này có thể khiến lệnh Tx được phát ra ngay phía trên phần đang in. Hãy bật tùy chọn này nếu bạn muốn việc đổi công cụ luôn diễn ra phía trên wipe tower." +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "Không có lớp thưa (beta)" @@ -20849,10 +20951,6 @@ msgstr "Máy in vật lý" msgid "Print Host upload" msgstr "Tải lên máy chủ in" -# AI Translated -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "Chọn cách triển khai tác nhân mạng cho việc giao tiếp với máy in. Các tác nhân khả dụng được đăng ký khi khởi động." - # AI Translated msgid "Select a Flashforge printer" msgstr "Chọn một máy in Flashforge" @@ -21832,9 +21930,6 @@ msgstr "Đã xảy ra điều gì đó không mong đợi khi cố gắng đăng msgid "User canceled." msgstr "Người dùng đã hủy." -msgid "Head diameter" -msgstr "Đường kính đầu" - msgid "Max angle" msgstr "Góc tối đa" @@ -22702,6 +22797,22 @@ msgstr "" "Tránh cong vênh\n" "Bạn có biết rằng khi in vật liệu dễ cong vênh như ABS, tăng nhiệt độ bàn nóng một cách thích hợp có thể giảm xác suất cong vênh không?" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "Chiều cao lớp quá nhỏ.\n" +#~ "Nó sẽ được đặt thành min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "Chiều cao lớp vượt quá giới hạn trong Cài đặt máy in -> Extruder -> Giới hạn chiều cao lớp, điều này có thể gây ra vấn đề chất lượng in." + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "Điều chỉnh về phạm vi đặt tự động?\n" + +#~ msgid "Head diameter" +#~ msgstr "Đường kính đầu" + #~ msgid "Print order within a single layer." #~ msgstr "Thứ tự in trong một lớp đơn." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 345891f250..f89ad1dd52 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -4574,6 +4574,20 @@ msgstr "当前腔体温度高于材料的安全温度,这可能导致材料软 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低机箱温度(%d℃)高于目标机箱温度(%d℃)。最低值是开始打印的阈值,此时机箱会持续朝目标温度加热,因此它不应超过目标值。该值将被限制到目标值。" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "调整" + # AI Translated msgid "" "Layer height too small\n" @@ -4696,6 +4710,12 @@ msgstr "" "是 - 启用Arachne墙生成器\n" "否 - 禁用Arachne墙生成器并将绒毛表面设置为[位移]模式" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Brim宽度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "螺旋模式仅在壁环为 1、支撑被禁用、探测结块检测被禁用、顶部壳层为 0、稀疏填充密度为 0 且延时类型为传统时才起作用。" @@ -4711,6 +4731,9 @@ msgstr "" "是 - 自动调整这些设置并开启旋转模式\n" "否 - 暂不使用旋转模式" +msgid "N/A" +msgstr "不适用" + msgid "Printing" msgstr "打印中" @@ -4950,6 +4973,12 @@ msgstr "生成校准gcode失败" msgid "Calibration error" msgstr "校准错误" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "网络不可用" @@ -5162,9 +5191,6 @@ msgstr "无效的模式。请使用 N、N#K 或逗号分隔的列表(每个条 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "无效格式,应该是\"%1%\"这种数组格式" -msgid "N/A" -msgstr "不适用" - # AI Translated msgid "System agents" msgstr "系统代理" @@ -5807,7 +5833,7 @@ msgstr "体积:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "发现G-code路径在层%d,高度为%.2lf mm处有冲突。请将有冲突的对象分离得更远(%s <-> %s)。" @@ -5988,6 +6014,9 @@ msgstr "多设备" msgid "Project" msgstr "项目" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "是" @@ -6115,6 +6144,12 @@ msgstr "项目另存为" msgid "Save current project as" msgstr "项目另存为" +msgid "Publish" +msgstr "发布" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "导入 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7869,6 +7904,12 @@ msgstr "请确认这些预设中的G-codes是否安全,以防止对机器造 msgid "Customized Preset" msgstr "自定义的预设" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 文件中的部件名称不是 UTF8 格式!" @@ -8028,19 +8069,19 @@ msgstr "未选择替换目录" msgid "Replaced with 3D files from directory:\n" msgstr "替换为目录中的 3D 文件:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 跳过 %s:同一文件。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 跳过%s:文件不存在。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 跳过%s:替换失败。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 替换了 %s。\n" @@ -8250,6 +8291,14 @@ msgstr "切片文件另存为:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "文件%s已经发送到打印机的存储空间,可以在打印机上浏览。" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "未设置喷嘴类型。请设置喷嘴并重试。" @@ -9121,6 +9170,14 @@ msgstr "显示不受支持的预设" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在打印机和耗材下拉列表中显示不兼容/不受支持的预设。这些预设无法被选择。" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "实验性功能" @@ -9357,9 +9414,6 @@ msgstr "跳转到发布页面" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "提示:发布前需要一些准备时间,请耐心等待。" -msgid "Publish" -msgstr "发布" - msgid "Publish was canceled" msgstr "发布已取消" @@ -9375,6 +9429,21 @@ msgstr "正在上传数据" msgid "Jump to webpage" msgstr "跳转到网页" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "另存%s为" @@ -9385,9 +9454,21 @@ msgstr "用户预设" msgid "Preset Inside Project" msgstr "项目预设" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "与父级分离" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "名称不可用。" @@ -10093,24 +10174,6 @@ msgstr "您确定要启用此选项吗?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充图案通常设计为自动处理旋转,以确保正确打印并实现其预期效果(例如,Gyroid、Cubic)。旋转当前的稀疏填充图案可能会导致支撑不足。请谨慎操作并彻底检查是否存在任何潜在的打印问题。您确定要启用此选项吗?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"层高太小。\n" -"将设置为min_layer_height\n" -"层高太小。\n" -"将自动设置为min_layer_height的值\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自动调整到范围内?\n" - -msgid "Adjust" -msgstr "调整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "实验性选项。在更换耗材丝时,将耗材丝回抽一段距离后再切断以最小化冲刷。虽然这可以显著减少冲刷,但也可能增加喷嘴堵塞或其他打印问题的风险。" @@ -10303,6 +10366,9 @@ msgstr "检测到保留的关键字" msgid "Setting Overrides" msgstr "参数覆盖" +msgid "Retraction when switching material" +msgstr "切换材料时的回抽量" + msgid "Basic information" msgstr "基础信息" @@ -10433,6 +10499,12 @@ msgstr "兼容的切片配置" msgid "Printable space" msgstr "可打印区域" +msgid "Printer Agent" +msgstr "打印机代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10558,9 +10630,6 @@ msgstr "层高限制" msgid "Z-Hop" msgstr "Z轴抬升" -msgid "Retraction when switching material" -msgstr "切换材料时的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -11911,6 +11980,9 @@ msgstr "离不可打印区域太近,会发生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "距离聚集检测区域太近,会引起碰撞。\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所选的喷嘴温度不兼容。每种耗材的喷嘴温度都必须落在其他耗材的推荐喷嘴温度范围内。否则可能会发生喷嘴堵塞或打印机损坏。" @@ -12224,9 +12296,6 @@ msgstr "使用 3MF 代替 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "如果打印机接受 3MF 文件作为打印任务,请启用此选项。启用后,Orca Slicer 将以 .gcode.3mf 格式发送切片文件,而不是普通的 .gcode 文件。" -msgid "Printer Agent" -msgstr "打印机代理" - msgid "Select the network agent implementation for printer communication." msgstr "选择打印机通信的网络代理实施。" @@ -12861,9 +12930,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "内部桥接的速度。如果该值以百分比表示,将基于桥接速度计算。默认值为150%。" -msgid "Brim width" -msgstr "Brim宽度" - msgid "This is the distance from the model to the outermost brim line." msgstr "从模型到最外圈brim走线的距离" @@ -12944,6 +13010,12 @@ msgstr "" "在检测尖锐角度之前,几何形状将被简化。此参数表示简化的最小偏差长度。\n" "设为0以停用" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "向上兼容的机器" @@ -14119,6 +14191,12 @@ msgstr "TPMS-FK结构" msgid "Gyroid" msgstr "螺旋体" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "顶面填充的加速度。使用较低值可能会改善顶面质量" @@ -14659,6 +14737,12 @@ msgstr "打印机兼容的G-code风格'" msgid "Klipper" msgstr "Klipper固件" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "颗粒改装打印机" @@ -15704,6 +15788,12 @@ msgstr "更换挤出机时长回缩" msgid "Retraction distance when extruder change" msgstr "更换挤出机时的回缩距离" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z抬升高度" @@ -15797,6 +15887,9 @@ msgstr "额外回填长度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每当空驶后回抽被补偿时,挤出机将推入额外数量的耗材丝。很少需要此设置。" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "当换色后回抽被补偿时,挤出机将推入额外数量的耗材丝。" @@ -16211,6 +16304,12 @@ msgstr "在擦拭塔上换头" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "在发出换头命令 (Tx) 之前,强制打印头先移动到擦拭塔。仅与使用第 2 类擦拭塔的多挤出机(多打印头)打印机相关。默认情况下,Orca 会在多打印头机器上跳过此移动,因为固件会处理换头,这可能导致 Tx 命令在打印件上方发出。如果您希望换头命令始终在擦拭塔上方发出,请启用此选项。" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "无稀疏层 (实验功能)" @@ -19433,9 +19532,6 @@ msgstr "物理打印机" msgid "Print Host upload" msgstr "打印主机上传" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "为打印机通信选择网络代理。可用的代理将在启动时列出。" - msgid "Select a Flashforge printer" msgstr "选择一台 Flashforge 打印机" @@ -20325,9 +20421,6 @@ msgstr "在尝试登录时发生了异常,请重试。" msgid "User canceled." msgstr "用户已取消。" -msgid "Head diameter" -msgstr "Brim 直径" - msgid "Max angle" msgstr "最大角度" @@ -21111,6 +21204,24 @@ msgstr "" "避免翘曲\n" "您知道吗?打印ABS这类易翘曲材料时,适当提高热床温度可以降低翘曲的概率。" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "层高太小。\n" +#~ "将设置为min_layer_height\n" +#~ "层高太小。\n" +#~ "将自动设置为min_layer_height的值\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "层高超出了打印机设置->挤出机->层高限制中的范围,这可能导致打印质量问题。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自动调整到范围内?\n" + +#~ msgid "Head diameter" +#~ msgstr "Brim 直径" + #~ msgid "Print order within a single layer." #~ msgstr "同一层内的打印顺序" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 9b37009978..88596d495a 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-29 17:40-0300\n" +"POT-Creation-Date: 2026-08-18 16:25+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -4691,6 +4691,20 @@ msgstr "目前列印裝置內部溫度高於線材的安全溫度,可能會導 msgid "The minimal chamber temperature (%d℃) is higher than the target chamber temperature (%d℃). The minimal value is the threshold at which printing starts while the chamber keeps heating toward the target, so it should not exceed it. It will be clamped to the target." msgstr "最低倉室溫度(%d℃)高於目標倉室溫度(%d℃)。最低值是列印開始的門檻,此時倉室會持續朝目標溫度加熱,因此不應超過目標值。系統會將其限制在目標值。" +#, c-format, boost-format +msgid "Layer height is too small. It will be set to the minimum (%g mm)." +msgstr "" + +msgid "Layer height is outside the limits set in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +msgstr "" + +#, c-format, boost-format +msgid "Adjust it to the limit (%g mm) automatically?" +msgstr "" + +msgid "Adjust" +msgstr "調整" + msgid "" "Layer height too small\n" "It has been reset to 0.2" @@ -4825,6 +4839,12 @@ msgstr "" "是 - 啟用 Arachne Wall 產生器\n" "否 - 停用 Arachne Wall 產生器,並將 Fuzzy Skin 設定為 [位移] 模式" +msgid "Brim ear radius" +msgstr "" + +msgid "Brim width" +msgstr "Brim 寬度" + msgid "Spiral mode only works when wall loops is 1, support is disabled, clumping detection by probing is disabled, top shell layers is 0, sparse infill density is 0 and timelapse type is traditional." msgstr "花瓶模式僅適用於牆體圈數為 1、停用支撐、停用偵測堵塞、頂部外殼層數為 0、稀疏填充密度為 0,且延時攝影類型為傳統模式時。" @@ -4840,6 +4860,9 @@ msgstr "" "是 - 自動調整這些設定並啟用花瓶模式\n" "否 - 不使用花瓶模式" +msgid "N/A" +msgstr "不適用" + msgid "Printing" msgstr "列印中" @@ -5079,6 +5102,12 @@ msgstr "產生校正代碼失敗" msgid "Calibration error" msgstr "校正錯誤" +msgid "This printer is not configured with the hardware this control needs." +msgstr "" + +msgid "This control is not supported on this printer." +msgstr "" + # AI Translated msgid "Network unavailable" msgstr "網路無法使用" @@ -5291,9 +5320,6 @@ msgstr "無效的格式。請使用 N、N#K 或逗號分隔的清單,每個項 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "無效格式,應該是「%1%」這種格式" -msgid "N/A" -msgstr "不適用" - # AI Translated msgid "System agents" msgstr "系統代理程式" @@ -5936,7 +5962,7 @@ msgstr "體積:" msgid "Size:" msgstr "尺寸:" -#, c-format, boost-format +#, boost-format msgid "Conflicts of G-code paths have been found at layer %d, Z = %.2lfmm. Please separate the conflicted objects farther (%s <-> %s)." msgstr "發現 G-code 路徑在 %d 層,Z = %.2lf mm 處的衝突。請將有衝突的物件分離得更遠(%s <-> %s)。" @@ -6118,6 +6144,9 @@ msgstr "多臺裝置" msgid "Project" msgstr "專案" +msgid "Device (Web)" +msgstr "" + msgid "Yes" msgstr "是" @@ -6245,6 +6274,12 @@ msgstr "另存專案為" msgid "Save current project as" msgstr "將目前專案另存為" +msgid "Publish" +msgstr "發布" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "匯入 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8030,6 +8065,12 @@ msgstr "請確認這些預設中的 G-code 是安全的,以防止對列印裝 msgid "Customized Preset" msgstr "自訂預設" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 檔案內部元件的名稱不是 UTF-8 格式!" @@ -8193,19 +8234,19 @@ msgstr "未選擇替換的目錄" msgid "Replaced with 3D files from directory:\n" msgstr "已從目錄替換為 3D 檔案:\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: same file.\n" msgstr "✖ 已跳過 %s:相同檔案。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: file does not exist.\n" msgstr "✖ 已跳過 %s:檔案不存在。\n" -#, c-format +#, c-format, boost-format msgid "✖ Skipped %s: failed to replace.\n" msgstr "✖ 已跳過 %s:無法替換。\n" -#, c-format +#, c-format, boost-format msgid "✔ Replaced %s.\n" msgstr "✔ 已替換 %s。\n" @@ -8416,6 +8457,14 @@ msgstr "切片檔案另存為:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "檔案 %s 已經傳送到列印裝置的儲存空間,可以在列印裝置上瀏覽。" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。" @@ -9294,6 +9343,14 @@ msgstr "顯示不支援的預設" msgid "Show incompatible/unsupported presets in the printer and filament dropdown lists. These presets cannot be selected." msgstr "在列印裝置和線材下拉選單中顯示不相容/不支援的預設。這些預設無法選取。" +msgid "(Experimental) Use printer agents instead of print hosts" +msgstr "" + +msgid "" +"Route print jobs for non-Bambu printers through printer plug-in agents instead of the classic print-host upload flow.\n" +"When disabled, OrcaSlicer uses the legacy print-host behavior." +msgstr "" + # AI Translated msgid "Experimental Features" msgstr "實驗性功能" @@ -9530,9 +9587,6 @@ msgstr "發布頁面" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "提示:發布前需要一些準備時間,請耐心等待。" -msgid "Publish" -msgstr "發布" - msgid "Publish was canceled" msgstr "發布已取消" @@ -9548,6 +9602,21 @@ msgstr "正在上傳資料" msgid "Jump to webpage" msgstr "跳至網頁" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "另存 %s 為" @@ -9558,9 +9627,21 @@ msgstr "使用者預設" msgid "Preset Inside Project" msgstr "項目預設" +msgid "Copies all inherited values from the parent into this preset and removes the parent relationship. Presets compatible only with the parent may become unsupported." +msgstr "" + msgid "Detach from parent" msgstr "從父預設分離" +msgid "Unique preset" +msgstr "" + +msgid "Parent preset" +msgstr "" + +msgid "This preset does not inherit from another preset." +msgstr "" + msgid "Name is unavailable." msgstr "名稱不可用。" @@ -10299,22 +10380,6 @@ msgstr "您確認要啟用此選項嗎?" msgid "Infill patterns are typically designed to handle rotation automatically to ensure proper printing and achieve their intended effects (e.g., Gyroid, Cubic). Rotating the current sparse infill pattern may lead to insufficient support. Please proceed with caution and thoroughly check for any potential printing issues. Are you sure you want to enable this option?" msgstr "填充模式通常設計為自動處理旋轉,以確保正確列印並實現其預期效果(例如:Gyroid、Cubic)。旋轉目前的稀疏填充模式可能會導致支撐不足。請謹慎操作,並仔細檢查任何潛在的列印問題。您確定要啟用此選項嗎?" -msgid "" -"Layer height is too small.\n" -"It will set to min_layer_height\n" -msgstr "" -"層高過薄\n" -"將改為 min_layer_height\n" - -msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." -msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" - -msgid "Adjust to the set range automatically?\n" -msgstr "是否自動調整至設定範圍?\n" - -msgid "Adjust" -msgstr "調整" - msgid "Experimental feature: Retracting and cutting off the filament at a greater distance during filament changes to minimize flush. Although it can notably reduce flush, it may also elevate the risk of nozzle clogs or other printing complications." msgstr "實驗性功能:在換線過程中以更大的距離收回並切斷線材,以減少沖洗量。儘管這可以顯著減少沖洗,但也可能增加噴嘴堵塞或其他列印問題的風險。" @@ -10507,6 +10572,9 @@ msgstr "偵測到保留的關鍵字" msgid "Setting Overrides" msgstr "參數覆蓋" +msgid "Retraction when switching material" +msgstr "切換線材時的回抽量" + msgid "Basic information" msgstr "基本資訊" @@ -10637,6 +10705,12 @@ msgstr "相容的切片設定" msgid "Printable space" msgstr "可列印區域" +msgid "Printer Agent" +msgstr "列印裝置代理" + +msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." +msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" + #. TRN: The first argument is the parameter's name; the second argument is its value. #, boost-format msgid "Invalid value provided for parameter %1%: %2%" @@ -10762,9 +10836,6 @@ msgstr "層高限制" msgid "Z-Hop" msgstr "Z 軸抬升" -msgid "Retraction when switching material" -msgstr "切換線材時的回抽量" - msgid "" "The Retract before wipe option could be only 100% when using the Firmware Retraction mode.\n" "\n" @@ -12113,6 +12184,9 @@ msgstr "離淨空區域太近,會發生碰撞。\n" msgid " is too close to clumping detection area, and collisions will be caused.\n" msgstr "離堵塞偵測區域太近,會發生碰撞。\n" +msgid " is partially outside the printable area, and it cannot be printed.\n" +msgstr "" + msgid "Selected nozzle temperatures are incompatible. Each filament's nozzle temperature must fall within the recommended nozzle temperature range of the other filaments. Otherwise, nozzle clogging or printer damage may occur." msgstr "所選的噴嘴溫度不相容。每種線材的噴嘴溫度都必須落在其他線材的建議噴嘴溫度範圍內。否則可能會發生噴嘴堵塞或列印裝置損壞。" @@ -12426,9 +12500,6 @@ msgstr "使用 3MF 取代 G-code" msgid "Enable this if the printer accepts a 3MF file as the print job. When enabled, Orca Slicer sends the sliced file as a .gcode.3mf, instead of a plain .gcode file." msgstr "若列印裝置接受 3MF 檔案作為列印作業,請啟用此選項。啟用後,Orca Slicer 會將切片後的檔案以 .gcode.3mf 形式傳送,而非單純的 .gcode 檔案。" -msgid "Printer Agent" -msgstr "列印裝置代理" - msgid "Select the network agent implementation for printer communication." msgstr "選擇用於列印裝置通訊的網路代理實作。" @@ -13074,9 +13145,6 @@ msgstr "" msgid "Speed of internal bridges. If the value is expressed as a percentage, it will be calculated based on the bridge_speed. Default value is 150%." msgstr "內部橋接速度。如果該值以百分比表示,將基於 bridge_speed 進行計算。預設值為 150%。" -msgid "Brim width" -msgstr "Brim 寬度" - msgid "This is the distance from the model to the outermost brim line." msgstr "從模型到 Brim 最外圈的距離" @@ -13157,6 +13225,12 @@ msgstr "" "在偵測尖銳角度之前,幾何形狀將被簡化。此參數表示簡化的最小偏差長度。\n" "設為 0 以停用" +msgid "Brim ears outer only" +msgstr "" + +msgid "Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections." +msgstr "" + msgid "upward compatible machine" msgstr "向上相容的裝置" @@ -14316,6 +14390,12 @@ msgstr "TPMS-FK結構" msgid "Gyroid" msgstr "螺旋體" +msgid "Sparse infill smooth factor" +msgstr "" + +msgid "Controls how strongly sparse infill corners are rounded. 0% keeps the original right-angle path, while 100% produces the largest possible curves between adjacent infill lines. Currently applies only to the Hilbert Curve." +msgstr "" + msgid "This is the acceleration of top surface infill. Using a lower value may improve top surface quality." msgstr "頂面填充的加速度。使用較低值可能會改善頂面列印品質" @@ -14856,6 +14936,12 @@ msgstr "列印裝置相容的 G-code 樣式" msgid "Klipper" msgstr "Klipper" +msgid "Skip G-code config block" +msgstr "" + +msgid "Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. This can help with printers whose firmware crashes when parsing these comment lines (e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, so importing it back into OrcaSlicer will not restore the configuration." +msgstr "" + msgid "Pellet Modded Printer" msgstr "顆粒改裝列印裝置" @@ -15909,6 +15995,12 @@ msgstr "更換擠出機時長回抽" msgid "Retraction distance when extruder change" msgstr "更換擠出機時的回抽距離" +msgid "Retraction Length (Toolchange)" +msgstr "" + +msgid "When retraction is triggered before changing tool, filament is pulled back by the specified amount (the length is measured on raw filament, before it enters the extruder)." +msgstr "" + msgid "Z-hop height" msgstr "Z 抬升高度" @@ -16002,6 +16094,9 @@ msgstr "額外回填長度" msgid "When the retraction is compensated after the travel move, the extruder will push this additional amount of filament. This setting is rarely needed." msgstr "每當空駛後回抽被補償時,擠出機將推入額外長度的線材。很少需要此設定。" +msgid "Extra length on restart (Toolchange)" +msgstr "" + msgid "When the retraction is compensated after changing tool, the extruder will push this additional amount of filament." msgstr "當換色後回抽被補償時,擠出機將推入額外長度的線材。" @@ -16405,6 +16500,12 @@ msgstr "在換料塔上換刀" msgid "Force the toolhead to travel to the wipe tower before issuing the tool change command (Tx). Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. By default Orca skips the travel on multi-toolhead machines because the firmware handles the head swap, which can result in the Tx command being issued above the printed part. Enable this option if you want the tool change to always be issued above the wipe tower instead." msgstr "強制工具頭在發出換刀指令 (Tx) 之前先移動到換料塔。僅適用於使用 Type 2 換料塔的多擠出機(多工具頭)列印裝置。預設情況下,Orca 會在多工具頭機器上略過此空駛,因為韌體會處理工具頭交換,這可能導致 Tx 指令在已列印零件上方發出。若您希望換刀一律改在換料塔上方發出,請啟用此選項。" +msgid "Wait for temperature on wipe tower" +msgstr "" + +msgid "Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on the tower instead of the model, and the travel overlaps with the heating. Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. The firmware or tool change macro must not wait for the temperature itself. When disabled, the temperature wait is issued right after the tool change command." +msgstr "" + msgid "No sparse layers (beta)" msgstr "取消稀疏層(Beta)" @@ -19622,9 +19723,6 @@ msgstr "實體列印裝置" msgid "Print Host upload" msgstr "列印主機上傳" -msgid "Select the network agent implementation for printer communication. Available agents are registered at startup." -msgstr "選擇列印裝置通訊的網路代理實施。可用代理在啟動時註冊。" - msgid "Select a Flashforge printer" msgstr "選取 Flashforge 列印裝置" @@ -20516,9 +20614,6 @@ msgstr "嘗試登入時發生了意外錯誤,請再試一次。" msgid "User canceled." msgstr "使用者取消。" -msgid "Head diameter" -msgstr "頭直徑" - msgid "Max angle" msgstr "最大角度" @@ -21323,6 +21418,22 @@ msgstr "" "避免翹曲\n" "您知道嗎?當列印容易翹曲的材料(如 ABS)時,適當提高熱床溫度可以降低翹曲的機率。" +#~ msgid "" +#~ "Layer height is too small.\n" +#~ "It will set to min_layer_height\n" +#~ msgstr "" +#~ "層高過薄\n" +#~ "將改為 min_layer_height\n" + +#~ msgid "Layer height exceeds the limit in Printer Settings -> Extruder -> Layer height limits, this may cause printing quality issues." +#~ msgstr "層高超過了印表裝置設定 -> 擠出機 -> 層高限制,這可能會導致列印品質問題。" + +#~ msgid "Adjust to the set range automatically?\n" +#~ msgstr "是否自動調整至設定範圍?\n" + +#~ msgid "Head diameter" +#~ msgstr "頭直徑" + #~ msgid "Print order within a single layer." #~ msgstr "每一層的列印順序" diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 5baff8282c..009f672d3e 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -82,7 +82,7 @@ wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPr PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, - _L("Publish"), + _L("Publish 3MF..."), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER) From 1b770e863843fe18937ee5378ebcf1f339c1f25f Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 19 Aug 2026 15:47:28 +0800 Subject: [PATCH 11/60] Bug fixes and test cases for import filament of published 3MF. Update translations --- localization/i18n/OrcaSlicer.pot | 7 +- localization/i18n/ca/OrcaSlicer_ca.po | 9 +- localization/i18n/cs/OrcaSlicer_cs.po | 9 +- localization/i18n/de/OrcaSlicer_de.po | 9 +- localization/i18n/en/OrcaSlicer_en.po | 7 +- localization/i18n/es/OrcaSlicer_es.po | 9 +- localization/i18n/eu/OrcaSlicer_eu.po | 9 +- localization/i18n/fr/OrcaSlicer_fr.po | 9 +- localization/i18n/hu/OrcaSlicer_hu.po | 9 +- localization/i18n/it/OrcaSlicer_it.po | 9 +- localization/i18n/ja/OrcaSlicer_ja.po | 9 +- localization/i18n/ko/OrcaSlicer_ko.po | 9 +- localization/i18n/lt/OrcaSlicer_lt.po | 9 +- localization/i18n/nl/OrcaSlicer_nl.po | 9 +- localization/i18n/pl/OrcaSlicer_pl.po | 9 +- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 9 +- localization/i18n/ru/OrcaSlicer_ru.po | 9 +- localization/i18n/sv/OrcaSlicer_sv.po | 9 +- localization/i18n/th/OrcaSlicer_th.po | 9 +- localization/i18n/tr/OrcaSlicer_tr.po | 9 +- localization/i18n/uk/OrcaSlicer_uk.po | 9 +- localization/i18n/vi/OrcaSlicer_vi.po | 9 +- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 9 +- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 9 +- src/libslic3r/PresetBundle.cpp | 562 ++++------- src/libslic3r/PresetBundle.hpp | 13 +- src/libslic3r/PublishSettings.cpp | 37 +- src/libslic3r/PublishSettings.hpp | 69 +- src/slic3r/GUI/ConfigValueFormatter.hpp | 10 +- src/slic3r/GUI/MainFrame.cpp | 4 +- src/slic3r/GUI/Plater.cpp | 48 +- src/slic3r/GUI/Plater.hpp | 5 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 173 ++-- src/slic3r/GUI/PublishSettingsDialog.hpp | 31 +- tests/libslic3r/test_3mf.cpp | 51 +- .../libslic3r/test_preset_bundle_loading.cpp | 936 +++++++++++------- 36 files changed, 1148 insertions(+), 1003 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index c55cdb7336..771a9e183c 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -5945,7 +5945,7 @@ msgstr "" msgid "Save current project as" msgstr "" -msgid "Publish" +msgid "Publish 3MF" msgstr "" msgid "Export a 3MF file with the selected settings embedded" @@ -8043,6 +8043,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 4eb07b2635..244cf7f41b 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -6406,8 +6406,8 @@ msgstr "Desa el projecte com a" msgid "Save current project as" msgstr "Desar el projecte actual com" -msgid "Publish" -msgstr "Publicar" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8632,6 +8632,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipus de broquet no està establert. Establiu el broquet i torneu-ho a provar." diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 95259e0895..f5a8cc6717 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -6367,8 +6367,8 @@ msgstr "Uložit projekt jako" msgid "Save current project as" msgstr "Uložit aktuální projekt jako" -msgid "Publish" -msgstr "Publikovat" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8592,6 +8592,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publikovat" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Typ trysky není nastaven. Nastavte prosím trysku a zkuste to znovu." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 0a86439cab..28113d16d3 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -6259,8 +6259,8 @@ msgstr "Projekt speichern als" msgid "Save current project as" msgstr "Aktuelles Projekt speichern als" -msgid "Publish" -msgstr "Veröffentlichen" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8464,6 +8464,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Veröffentlichen" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Die Düsenart ist nicht eingestellt. Bitte stellen Sie die Düse ein und versuchen Sie es erneut." diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 03b83309de..29b88cdab0 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -5941,7 +5941,7 @@ msgstr "" msgid "Save current project as" msgstr "" -msgid "Publish" +msgid "Publish 3MF" msgstr "" msgid "Export a 3MF file with the selected settings embedded" @@ -8039,6 +8039,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 74617b88ec..54c4bccafe 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -6116,8 +6116,8 @@ msgstr "Guardar proyecto como" msgid "Save current project as" msgstr "Guardar el proyecto actual como" -msgid "Publish" -msgstr "Publicar" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8267,6 +8267,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipo de boquilla no está establecido. Configure la boquilla e inténtelo de nuevo." diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index cceccaa64c..8a9a0a6ad3 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana \n" "Language-Team: \n" @@ -6161,8 +6161,8 @@ msgstr "Gorde proiektua honela" msgid "Save current project as" msgstr "Gorde uneko proiektua honela" -msgid "Publish" -msgstr "Argitaratu" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8334,6 +8334,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Argitaratu" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Pita mota ez dago ezarrita. Ezarri pita eta saiatu berriro." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 5aeed754f0..5197f6c725 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -6208,8 +6208,8 @@ msgstr "Enregistrer le projet sous" msgid "Save current project as" msgstr "Enregistrer le projet actuel sous" -msgid "Publish" -msgstr "Publier" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8390,6 +8390,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publier" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Le type de buse n'est pas défini. Veuillez définir la buse et réessayer." diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 6c9c5669d8..e87f21c567 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -6310,8 +6310,8 @@ msgstr "Projekt mentése másként" msgid "Save current project as" msgstr "Jelenlegi projekt mentése másként" -msgid "Publish" -msgstr "Közzététel" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8516,6 +8516,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Közzététel" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "A fúvókatípus nincs beállítva. Állítsd be a fúvókát, majd próbáld újra." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index 8533b8b39f..8ce671c1e6 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -6312,8 +6312,8 @@ msgstr "Salva progetto con nome" msgid "Save current project as" msgstr "Salva progetto corrente con nome" -msgid "Publish" -msgstr "Pubblica" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8515,6 +8515,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Pubblica" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Il tipo di ugello non è impostato. Impostare l'ugello e riprovare." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 2c28d2b166..ca68f62e0c 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -6321,8 +6321,8 @@ msgstr "プロジェクトを名前を付けて保存" msgid "Save current project as" msgstr "プロジェクトを名前を付けて保存" -msgid "Publish" -msgstr "公開する" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8534,6 +8534,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "公開する" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 5e81f19e0d..a9d1a699af 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -6335,8 +6335,8 @@ msgstr "프로젝트 다른 이름으로 저장" msgid "Save current project as" msgstr "현재 프로젝트 다른 이름으로 저장" -msgid "Publish" -msgstr "게시" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8567,6 +8567,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "게시" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요." diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index aac9ef9dc6..9f16194315 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -6298,8 +6298,8 @@ msgstr "Įrašyti projektą kaip" msgid "Save current project as" msgstr "Įrašyti dabartinį projektą kaip" -msgid "Publish" -msgstr "Talpinti" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8510,6 +8510,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Talpinti" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Purkštuko tipas nenustatytas. Nustatykite purkštuką ir bandykite dar kartą." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 4d37e65fee..96840c0844 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -6873,8 +6873,8 @@ msgstr "Bewaar project als" msgid "Save current project as" msgstr "Bewaar huidig project als" -msgid "Publish" -msgstr "Publiceren" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -9284,6 +9284,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publiceren" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Het mondstuktype is niet ingesteld. Stel het mondstuk in en probeer het opnieuw." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index e9430155ea..5df6507348 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -6454,8 +6454,8 @@ msgstr "Zapisz projekt jako" msgid "Save current project as" msgstr "Zapisz bieżący projekt jako" -msgid "Publish" -msgstr "Opublikuj" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8724,6 +8724,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Opublikuj" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nie ustawiono typu dyszy Wprowadź ustawienia dyszy i spróbuj ponownie." diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index b001ea24b8..48ed2fb774 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -6130,8 +6130,8 @@ msgstr "Salvar projeto como" msgid "Save current project as" msgstr "Salvar o projeto atual como" -msgid "Publish" -msgstr "Publicar" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8290,6 +8290,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "O tipo de bico não está configurado. Configure o bico e tente novamente." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 919710f917..fd7b617642 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -6363,8 +6363,8 @@ msgstr "Сохранить проект как" msgid "Save current project as" msgstr "Сохранить текущий проект как" -msgid "Publish" -msgstr "Опубликовать" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8569,6 +8569,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Опубликовать" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 1caffffd36..2e2ca51d7c 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -6957,8 +6957,8 @@ msgstr "Spara Projekt som" msgid "Save current project as" msgstr "Spara nuvarande projekt som" -msgid "Publish" -msgstr "Publicera" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -9377,6 +9377,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Publicera" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozzeltypen är inte angiven. Ange nozzeln och försök igen." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 7f86af0e5e..1826f2be8e 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -6289,8 +6289,8 @@ msgstr "บันทึกโปรเจกต์เป็น" msgid "Save current project as" msgstr "บันทึกโครงการปัจจุบันเป็น" -msgid "Publish" -msgstr "เผยแพร่" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8470,6 +8470,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "เผยแพร่" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index 944c322a94..b2394caeb3 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-08-01 20:32+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -6389,8 +6389,8 @@ msgstr "Projeyi farklı kaydet" msgid "Save current project as" msgstr "Mevcut projeyi farklı kaydet" -msgid "Publish" -msgstr "Yayınla" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8606,6 +8606,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Yayınla" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozul tipi ayarlanmamış. Lütfen nozulu ayarlayın ve tekrar deneyin." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 5c162e8475..40290ba12c 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk \n" "Language-Team: Ukrainian\n" @@ -6326,8 +6326,8 @@ msgstr "Зберегти проєкт як" msgid "Save current project as" msgstr "Зберегти поточний проєкт як" -msgid "Publish" -msgstr "Публікувати" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8580,6 +8580,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Публікувати" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз." diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index f900f3fc1f..dc269a9519 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -6674,8 +6674,8 @@ msgstr "Lưu dự án thành" msgid "Save current project as" msgstr "Lưu dự án hiện tại thành" -msgid "Publish" -msgstr "Xuất bản" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -9001,6 +9001,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "Xuất bản" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Chưa đặt loại đầu phun. Vui lòng đặt đầu phun rồi thử lại." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index f89ad1dd52..93a9f0ae0b 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -6144,8 +6144,8 @@ msgstr "项目另存为" msgid "Save current project as" msgstr "项目另存为" -msgid "Publish" -msgstr "发布" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8299,6 +8299,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "发布" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "未设置喷嘴类型。请设置喷嘴并重试。" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 88596d495a..7a19a1c540 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-18 16:25+0800\n" +"POT-Creation-Date: 2026-08-19 14:59+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -6274,8 +6274,8 @@ msgstr "另存專案為" msgid "Save current project as" msgstr "將目前專案另存為" -msgid "Publish" -msgstr "發布" +msgid "Publish 3MF" +msgstr "" msgid "Export a 3MF file with the selected settings embedded" msgstr "" @@ -8465,6 +8465,9 @@ msgid "" "Please check whether the folder exists online or if other programs have the file open." msgstr "" +msgid "Publish" +msgstr "發布" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。" diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index fd3d4d24b6..7a90036fe2 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4752,9 +4752,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } } // !is_published - // 4) Load the project config values (the per extruder wipe matrix etc). - // In published mode the receiver must not inherit the author's filament/purge data, - // so only the plate/bed geometry project keys are applied. + // Load the project config values. In published mode only the plate/bed geometry keys + // cross over (the receiver must not inherit the author's filament/purge data). this->project_config.apply_only(config, is_published ? s_project_options_published : s_project_options); break; @@ -4774,29 +4773,23 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool this->update_compatible(PresetSelectCompatibleType::Never); this->update_multi_material_filament_presets(); - // A "published" 3MF project overlays only the author-selected published keys onto the - // user's currently-selected (edited) process preset. Scalar keys are applied directly; - // vector (multi-extruder) keys are applied only when the edited preset has a matching - // vector size. Keys that cannot be applied are collected for notification; legacy - // filament/printer keys in a published file fall through into skipped_keys. + // A "published" 3MF project overlays the author-selected published keys onto the user's + // currently-selected (edited) process preset. Keys that cannot be applied are collected for + // notification; filament-class keys in published_keys (which only the material pass knows + // how to apply) fall through into skipped_keys. if (is_published) { std::vector skipped_keys; std::set 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. + // Only re-select the edited filament preset when the material overlay changed + // something: 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 - // PublishSettings.hpp). Defense-in-depth: a hand-crafted 3MF could set - // published_keys to these regardless of the dialog, so skip them here too. + // Structural keys are never applied (they would rewrite the user's preset + // inheritance/structure). Defense-in-depth: a hand-crafted 3MF could list them despite + // the dialog, so skip them here too. const std::set &structural_keys = publish_structural_keys(); - // The printer overlay is restricted to the publishable retraction/z-hop allowlist. - // Printer-class keys outside it are contract-excluded: never applied and never - // reported as skipped (a hand-crafted 3MF listing machine_start_gcode or - // nozzle_diameter must not apply them and must not spam the warning). + // The printer overlay is restricted to the publishable retraction/z-hop allowlist; + // printer-class keys outside it are contract-excluded (never applied, never reported). const std::set &printer_allowlist = publishable_printer_keys(); const std::vector &printer_options = Preset::printer_options(); const std::set printer_option_set(printer_options.begin(), printer_options.end()); @@ -4805,10 +4798,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool for (const std::string &key : published_config->published_keys) { if (applied_keys.count(key) != 0) continue; // already applied - // A '#' suffix denotes a variant (per-extruder/per-filament) key; resolve the base key. + // A '#' suffix denotes a variant key; resolve the base key. const std::string base_key = key.substr(0, key.find('#')); - // Structural keys are intentionally never applied (not "skipped due to - // mismatch"), so bail out before the applied/skipped bookkeeping. + // Structural keys are never applied (not "skipped due to mismatch"), so bail + // out before the applied/skipped bookkeeping. if (structural_keys.count(base_key) != 0) continue; if (allowlist != nullptr && @@ -4822,23 +4815,29 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (src_opt == nullptr) continue; // key not present in the loaded config; record later if (src_opt->is_vector()) { - // Vector key: apply only when the edited preset has a matching vector size. const ConfigOption *dst_opt = target.option(base_key); - if (dst_opt == nullptr || !dst_opt->is_vector() || - static_cast(src_opt)->size() != static_cast(dst_opt)->size()) + if (dst_opt == nullptr || !dst_opt->is_vector()) continue; // cannot apply; will be reported as skipped - // A '#' variant index must be in range: ConfigOptionVector::set_at would - // otherwise resize the destination vector, corrupting the receiver's preset. + // 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 + // 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). if (key.size() > base_key.size()) { const size_t idx = static_cast(std::atoi(key.c_str() + base_key.size() + 1)); - if (idx >= static_cast(src_opt)->size()) + if (idx >= static_cast(src_opt)->size() || + idx >= static_cast(dst_opt)->size()) continue; // out-of-range variant: cannot apply; reported as skipped + } else if (static_cast(src_opt)->size() != + static_cast(dst_opt)->size()) { + // Whole-vector base key: the receiver must have a matching vector size, + // otherwise applying would overwrite a different number of elements. + continue; // cannot apply; will be reported as skipped } target.apply_only(config, {key}, true); applied_keys.insert(key); } else { - // A scalar key cannot carry a '#N' variant suffix; a hand-crafted file - // listing one is reported as skipped instead of being silently marked applied. + // A scalar key cannot carry a '#N' suffix; a hand-crafted file listing one + // is reported as skipped instead of being silently marked applied. if (key.find('#') != std::string::npos) continue; // Scalar key: apply only if present on the user's machine. @@ -4852,229 +4851,87 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool apply_published(this->prints.get_edited_preset().config, nullptr); apply_published(this->printers.get_edited_preset().config, &printer_allowlist); - // Material pass: apply the author's material-qualified keys onto the receiver's - // matching filament presets. The file config carries the author's per-slot identity - // (filament_type / filament_vendor remain in config; filament_ids was moved into a - // local earlier) and the per-slot material retraction values. - if (!published_config->material_keys.empty()) { - const ConfigOptionStrings *file_types = config.option("filament_type"); - const ConfigOptionStrings *file_vendors = config.option("filament_vendor"); - auto identity_matches = [](const std::string &id, const std::string &type, const std::string &vendor, - const std::string &slot_id, const std::string &slot_type, const std::string &slot_vendor) { - // When both sides carry a filament_id, equality is required; otherwise fall - // back to filament_type, with filament_vendor as an additional qualifier only - // when both sides have a non-empty vendor. - if (!id.empty() && !slot_id.empty()) - return id == slot_id; - if (type.empty() || type != slot_type) - return false; - if (!vendor.empty() && !slot_vendor.empty()) - return vendor == slot_vendor; - 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); - // a legacy entry (slot -1) uses the first matching author slot and applies to - // every matching receiver preset. - auto slot_identity = [&filament_ids, file_types, file_vendors](size_t slot, std::string &id, std::string &type, std::string &vendor) { - id = (slot < filament_ids.size()) ? filament_ids[slot] : std::string(); - type = (file_types && slot < file_types->size()) ? file_types->get_at(slot) : std::string(); - vendor = (file_vendors && slot < file_vendors->size()) ? file_vendors->get_at(slot) : std::string(); - }; - bool author_found = false; - size_t author_slot = 0; - size_t author_ordinal = 0; - if (entry.slot >= 0) { - // Collect every author slot carrying this identity, in slot order; the - // entry's slot must be among them, and its position is the ordinal used - // to pick the receiver's matching preset. - std::vector matching_author_slots; - for (size_t slot = 0; slot < filament_ids.size(); ++slot) { - std::string slot_id, slot_type, slot_vendor; - slot_identity(slot, slot_id, slot_type, slot_vendor); - if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, - slot_id, slot_type, slot_vendor)) - matching_author_slots.emplace_back(slot); - } - const auto ordinal_it = std::find(matching_author_slots.begin(), matching_author_slots.end(), size_t(entry.slot)); - if (ordinal_it != matching_author_slots.end()) { - author_slot = size_t(entry.slot); - author_ordinal = size_t(ordinal_it - matching_author_slots.begin()); - author_found = true; - } - // Out of range, or the slot does not carry this identity: silent skip below. - } else { - // Legacy: the first author slot whose identity matches. - for (size_t slot = 0; slot < filament_ids.size(); ++slot) { - std::string slot_id, slot_type, slot_vendor; - slot_identity(slot, slot_id, slot_type, slot_vendor); - if (identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, - slot_id, slot_type, slot_vendor)) { - author_slot = slot; - author_found = true; - break; - } - } - } - if (!author_found) - // No author slot carries this material: nothing to apply, nothing to report. - continue; - - const std::string material_label = entry.filament_id.empty() ? entry.filament_type : entry.filament_id; - auto report_skipped = [&skipped_keys, &material_label](const std::string &key, const std::string &slot_qualifier = std::string()) { - skipped_keys.emplace_back("material:" + material_label + - (slot_qualifier.empty() ? std::string() : " " + slot_qualifier) + - " (" + key + ")"); - }; - - // Collect the receiver's matching filament presets (distinct by preset name). - std::vector matched_preset_names; - std::set fallback_matched_names; - for (const std::string &preset_name : this->filament_presets) { - Preset *preset = this->filaments.find_preset(preset_name); - if (preset == nullptr) - continue; - const std::string slot_id = preset->filament_id; - // Null-guard the identity reads: a malformed user preset may lack - // filament_type / filament_vendor entirely (hand-edited preset file). - const ConfigOptionStrings *slot_types = preset->config.option("filament_type"); - const ConfigOptionStrings *slot_vendors = preset->config.option("filament_vendor"); - const std::string slot_type = (slot_types && !slot_types->values.empty()) ? slot_types->get_at(0) : std::string(); - const std::string slot_vendor = (slot_vendors && !slot_vendors->values.empty()) ? slot_vendors->get_at(0) : std::string(); - if (!identity_matches(entry.filament_id, entry.filament_type, entry.filament_vendor, - slot_id, slot_type, slot_vendor)) - continue; - if (std::find(matched_preset_names.begin(), matched_preset_names.end(), preset_name) == matched_preset_names.end()) - matched_preset_names.emplace_back(preset_name); - if (entry.filament_id.empty() || slot_id.empty()) - fallback_matched_names.insert(preset_name); - } - if (matched_preset_names.empty()) { - // No receiver material matches this entry: report each key as skipped. - for (const std::string &key : entry.keys) - report_skipped(key); - continue; - } - if (fallback_matched_names.size() > 1) { - // The type fallback matched more than one distinct receiver preset: never - // guess which one the author meant. - for (const std::string &key : entry.keys) - report_skipped(key); - continue; - } - - // Slotted entries target the receiver's matching preset at the author's - // ordinal; legacy entries apply to every matching receiver preset. - std::vector apply_to_preset_names; - if (entry.slot >= 0) { - if (author_ordinal >= matched_preset_names.size()) { - // The receiver has fewer matching presets than the author's ordinal: - // this slot's values cannot be placed, report each key. - const std::string slot_qualifier = "slot " + std::to_string(entry.slot); - for (const std::string &key : entry.keys) - report_skipped(key, slot_qualifier); - continue; - } - apply_to_preset_names.emplace_back(matched_preset_names[author_ordinal]); - } else { - apply_to_preset_names = matched_preset_names; - } - - for (const std::string &key : entry.keys) { - const std::string base_key = key.substr(0, key.find('#')); - if (structural_keys.count(base_key) != 0) - continue; // structural: silent - const ConfigOption *src_opt = config.option(base_key); - if (src_opt == nullptr || !src_opt->is_vector() || - author_slot >= static_cast(src_opt)->size()) { - report_skipped(key); - continue; - } - for (const std::string &preset_name : apply_to_preset_names) { - Preset *preset = this->filaments.find_preset(preset_name); - if (preset == nullptr) - continue; - ConfigOption *dst_opt = preset->config.option(base_key); - // 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. - if (dst_opt == nullptr || !dst_opt->is_vector() || - static_cast(dst_opt)->empty() || - dst_opt->type() != src_opt->type()) { - report_skipped(key); - continue; - } - static_cast(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: + // Material pass: 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. + // - type match: the full dump still applies wholesale (every setting, as if the slot's + // filament had been loaded from a normal save); a partial entry's keys are applied + // as usual; + // - type mismatch: the slot is replaced with the best visible candidate, scored by the + // published identity (exact filament_id, then vendor+type, then type only); a + // preset no other slot references wins on equal scores, and a shared exact-material + // preset is taken even though mutating it also affects the other slot; 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 the first available visible + // preset, applying the author's values on top of it; a partial entry keeps the + // receiver's material and reports its keys as skipped. + // All applied values (colour and keys) are written onto the slot's stored preset + // directly (mutate in place): the receiver's material keeps its identity and is simply + // overridden. To keep slot-to-slot aliasing (several slots referencing one preset) from + // leaking one slot's values into another, published slots sharing a preset with another + // slot are re-pointed at distinct presets before the values are applied. { - // 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(); + // Grow the receiver's slots only as far as the highest published slot (never + // shrink, never pull filler materials for unpublished slots). + bool has_published_entries = 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; + has_published_entries = true; if (entry.slot >= 0) target_slots = std::max(target_slots, size_t(entry.slot) + 1); } - if (has_new_semantics) { + if (has_published_entries) { // Defensive cap: never exceed the file's own filament count. target_slots = std::min(target_slots, num_filaments); - // Slots that carry published content (full/type/colour) must reference a stored - // preset that no other slot shares: the overlay mutates stored presets in place - // (colour and keys), so a shared preset would leak one slot's published values - // into every slot that references it. + // Slots carrying published content, steering the initial preset selection of + // newly grown slots. std::set published_slots; for (const PublishedMaterialEntry &entry : published_config->material_keys) - if ((entry.full || entry.publish_type || entry.publish_color) && entry.slot >= 0) + if (entry.slot >= 0) published_slots.insert(entry.slot); std::set used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); // Mirror first_visible_idx()'s start index so suppressed default presets are // never picked as a slot material. const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; + // Candidate preference for a published entry: exact setting_id (variant-level, + // since "Generic PLA" and "Generic PLA Matte" share filament_id), then exact + // filament_id, then vendor+type, then type only (a type-only pick may surface an + // unrelated preset, e.g. a different vendor's PLA). + auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry) -> int { + if (!entry.setting_id.empty() && candidate.setting_id == entry.setting_id) + return 3; + const ConfigOptionStrings *types = candidate.config.opt("filament_type"); + const ConfigOptionStrings *vendors = candidate.config.opt("filament_vendor"); + const std::string type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string(); + if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id) + return 2; + if (normalize_filament_type(type) == entry.publish_type_value) { + if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor) + return 1; + return 0; + } + return -1; + }; while (this->filament_presets.size() < target_slots) { const size_t new_slot_idx = this->filament_presets.size(); std::string initial_preset; if (published_slots.count(static_cast(new_slot_idx)) != 0) { - // Proactively assign a distinct matching candidate preset if this slot - // carries a published type... + // Prefer the best distinct candidate for the slot's published material + // (exact id, then vendor+type, then type only)... for (const PublishedMaterialEntry &entry : published_config->material_keys) { if (entry.slot != static_cast(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty()) continue; - for (size_t i = 0; i < this->filaments.size(); ++i) { + int best_score = -1; + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) continue; - if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { + const int score = candidate_score(candidate, entry); + if (score > best_score) { + best_score = score; initial_preset = candidate.name; - break; } } break; @@ -5091,62 +4948,65 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } } if (initial_preset.empty()) - // Unpublished filler slot, or every visible preset is already used: repeat - // the receiver's last preset, mirroring the "Add one filament" behaviour - // (PresetBundle::set_num_filaments). + // Unpublished filler slot, or every visible preset is already used: + // repeat the receiver's last preset ("Add one filament" behaviour). initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name : this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); used_preset_names.insert(initial_preset); } - // Slots that were grown before this block (e.g. by update_multi_material_filament_presets - // matching the extruder count) may still alias another slot; re-point them at a - // distinct preset. Slot 0, the receiver's own material, is never re-assigned. + // Published slots that alias another slot (multi-extruder with one filament) + // get re-pointed at distinct presets: the overlay mutates stored presets in + // place, so a shared preset would leak one slot's published values into every + // aliased slot. Slot 0 (the receiver's own material) is never re-assigned; + // when no unused candidate exists the aliasing stays (unavoidable). + auto referenced_elsewhere = [&](const std::string &preset_name, size_t except_slot) { + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (s != except_slot && this->filament_presets[s] == preset_name) + return true; + return false; + }; for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { - if (published_slots.count(static_cast(slot)) == 0) - continue; - bool shared = false; - for (size_t other = 0; other < this->filament_presets.size(); ++other) - if (other != slot && this->filament_presets[other] == this->filament_presets[slot]) { - shared = true; - break; - } - if (!shared) + if (published_slots.count(static_cast(slot)) == 0 || + !referenced_elsewhere(this->filament_presets[slot], slot)) continue; + // Prefer the best distinct candidate for the slot's published material + // (exact id, then vendor+type, then type only)... std::string replacement; + int best_score = -1; for (const PublishedMaterialEntry &entry : published_config->material_keys) { if (entry.slot != static_cast(slot) || !entry.publish_type || entry.publish_type_value.empty()) continue; - for (size_t i = 0; i < this->filaments.size(); ++i) { + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) + if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1))) continue; - if (normalize_filament_type(candidate.config.opt_string("filament_type", 0u)) == entry.publish_type_value) { + const int score = candidate_score(candidate, entry); + if (score > best_score) { + best_score = score; replacement = candidate.name; - break; } } break; } + // ...otherwise any distinct visible preset not referenced by another slot. if (replacement.empty()) { for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) - continue; - replacement = candidate.name; - break; + if (candidate.is_visible && !referenced_elsewhere(candidate.name, size_t(-1))) { + replacement = candidate.name; + break; + } } } if (replacement.empty()) - continue; // every visible preset is used: aliasing is unavoidable - used_preset_names.erase(this->filament_presets[slot]); + continue; // every visible preset is referenced: aliasing is unavoidable this->filament_presets[slot] = replacement; - used_preset_names.insert(replacement); + material_applied = true; } - // Mirror set_num_filaments' project_config vector handling ("Add one filament"): - // resize the per-slot colour/type/map vectors to the grown slot count and seed the - // new entries so the slots render with colours instead of blank chips. Only the - // new entries are seeded; the receiver's existing values are left untouched. + // Grow the per-slot colour/type/map project vectors to the new slot count and + // seed the new entries so the slots render with colours instead of blank chips + // (mirrors set_num_filaments; existing values are left untouched). ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour"); ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour"); ConfigOptionStrings *proj_colour_type = this->project_config.opt("filament_colour_type"); @@ -5184,8 +5044,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (proj_colour_type && slot < proj_colour_type->values.size()) proj_colour_type->values[slot] = "1"; // default colour type } - // Rebuild the flush volumes for the grown slot count (set_num_filaments does the - // same; without it the matrix would stay at the receiver's old size). + // Rebuild the flush volumes for the grown slot count (as set_num_filaments does). this->update_multi_material_filament_presets(); auto apply_slot_keys = [&](Preset &preset, const std::vector &slot_keys, int author_slot, @@ -5207,23 +5066,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool 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. + // Per-slot scalar copy: the receiver preset holds one value per key + // (vector of size 1), the file holds the per-slot vector. static_cast(dst_opt)->set_at(src_opt, 0, author_slot); material_applied = true; } }; + // The slot's values are applied directly onto the slot's stored preset (mutate + // in place); the per-entry type gate below may re-point the slot first. 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. + // Resolve the stored preset itself (real=true), never the edited snapshot: + // find_preset would return &m_edited_preset for the selected slot, and the + // re-select at the end re-snapshots from the stored preset. Preset *recv = this->filaments.find_preset(this->filament_presets[slot], false, true); if (recv == nullptr) continue; @@ -5234,83 +5092,96 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool 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, preferring one that no other - // slot references (a shared stored preset would leak this slot's - // published values into that slot). - std::string replacement, first_same_type; - 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; - if (first_same_type.empty()) - first_same_type = candidate.name; - bool used_elsewhere = false; - for (size_t s = 0; s < this->filament_presets.size(); ++s) - if (s != slot && this->filament_presets[s] == candidate.name) { - used_elsewhere = true; - break; + // Null-guard: a malformed receiver preset may lack filament_type. + const ConfigOptionStrings *recv_types = recv->config.opt("filament_type"); + const std::string recv_type = (recv_types != nullptr && !recv_types->values.empty()) ? recv_types->get_at(0) : std::string(); + if (normalize_filament_type(recv_type) != entry.publish_type_value) { + // Type mismatch: replace the slot with the best matching preset, + // scored by the published identity (exact filament_id, then + // vendor+type, then type only). A preset no other slot references + // wins on equal scores; a shared exact-material preset is taken even + // though mutating it also affects the other slot. + auto find_best = [&](bool unreferenced_only) -> std::pair { + int best_score = -1; + std::string best_name; + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible) + continue; + const int score = candidate_score(candidate, entry); + if (score <= best_score) + continue; + if (unreferenced_only) { + bool used = false; + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (s != slot && this->filament_presets[s] == candidate.name) { + used = true; + break; + } + if (used) + continue; } - if (!used_elsewhere) { - replacement = candidate.name; - break; + best_score = score; + best_name = candidate.name; } + return { best_score, best_name }; + }; + const auto [strict_score, strict_name] = find_best(true); + const auto [relaxed_score, relaxed_name] = find_best(false); + int score = strict_score; + std::string replacement = strict_name; + if (relaxed_score > strict_score) { + score = relaxed_score; + replacement = relaxed_name; } - if (replacement.empty()) - replacement = first_same_type; 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); + std::string replacement_line = "slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement; + // A pick that is not the exact published material is a substitute; + // an entry without identity fields cannot be judged, so it stays plain. + if (score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty())) + replacement_line += " (substitute: no exact material match)"; + published_config->material_replacements.emplace_back(std::move(replacement_line)); } 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("filament_type", true)) { - if (type_opt->values.empty()) - type_opt->values.emplace_back(); - type_opt->values[0] = entry.publish_type_value; + // No same-type library preset: fall back to the first available + // visible preset, preferring one no other slot references, and + // apply the author's full values on top of it (the dump carries + // filament_type, so the preset takes the author's type). + std::string fallback; + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible) + continue; + if (fallback.empty()) + fallback = candidate.name; + bool referenced = false; + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (this->filament_presets[s] == candidate.name) { + referenced = true; + break; + } + if (!referenced) { + fallback = candidate.name; + break; + } } - if (ConfigOptionStrings *id_opt = custom_cfg.opt("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); + if (!fallback.empty() && fallback != recv->name) { + const std::string old_name = recv->name; + this->filament_presets[slot] = fallback; + recv = this->filaments.find_preset(fallback, false, true); + material_applied = true; + published_config->material_replacements.emplace_back( + "slot " + std::to_string(slot) + ": " + old_name + " -> " + fallback + + " (substitute: no " + entry.publish_type_value + " available)"); + } + // No visible preset at all: keep the receiver's material and let + // the full dump mutate it below. } else { - // Partial publish with no replacement available: keep the - // receiver's material and report this slot's keys as skipped. + // Partial publish with no replacement: keep the receiver's + // material and report the slot's keys as skipped. for (const std::string &key : entry.keys) skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); apply_slot = false; @@ -5318,14 +5189,15 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } } - // 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. + // The values below are applied onto whatever stored preset the slot ended up + // on (original, type replacement or the full-publish fallback), in place. + + // Colour is slot-scoped and independent of the type gate; it is also 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. + // Create the key when the target preset lacks it: the colour is a + // requirement, not an override. if (ConfigOptionStrings *colour = recv->config.opt("filament_colour", true)) { if (colour->values.empty()) colour->values.emplace_back(); @@ -5364,11 +5236,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } 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. + // The material overlay mutates the 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 so the applied values surface in the GUI. if (material_applied && !this->filament_presets.empty()) this->filaments.select_preset_by_name(this->filament_presets.front(), true); } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index d568de30e3..ad78a7c59c 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -167,22 +167,21 @@ struct PresetBundleMetadata } }; -// Configuration describing a "published" 3MF project: the file carries a flag plus a list of -// author-selected setting keys. When loading such a project the user's currently-selected -// presets are kept and only the published keys are overlaid onto the edited presets. +// A "published" 3MF project: keeps the user's currently-selected presets and overlays only the +// author-selected published keys onto the edited presets. struct PublishedConfig { bool published = false; std::vector published_keys; - // Material-qualified published keys chosen by the author for the materials used in the - // project; applied on load only to the receiver's filament presets whose material - // identity matches (see PublishedMaterialEntry in PublishSettings.hpp). + // Per-slot published material keys, applied positionally (author slot N -> receiver slot N), + // gated by the author's optional type requirement and written onto the slot's stored preset + // in place (see PublishedMaterialEntry in PublishSettings.hpp). std::vector material_keys; // 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 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. + // published project, for the load notification. std::vector material_replacements; }; diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 0d1bd07989..2be7a869d1 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -30,11 +30,9 @@ std::string normalize_filament_type(const std::string& type) const std::set& publish_structural_keys() { - // Structural / non-publishable keys. The *_settings_id keys are also part of - // PresetCollection::skipped_in_dirty (Preset.cpp) and are excluded there too. - // This mirrors the structural keys stripped from configs in Preset.cpp - // (profile_print_params_same) plus other keys that must never be published - // because they would rewrite the user's preset inheritance/structure. + // Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty + // (Preset.cpp) / stripped from configs (profile_print_params_same); publishing them would + // rewrite the user's preset inheritance/structure. static const std::set structural_keys = { "printer_settings_id", "filament_settings_id", "print_settings_id", "sla_print_settings_id", "sla_material_settings_id", @@ -88,9 +86,8 @@ const std::vector& publishable_printer_z_hop_options() const std::set& publishable_printer_keys() { - // The union of the printer tab's "Retraction" and "Z-Hop" optgroups. The "Retraction when - // switching material" keys are intentionally excluded: toolchange retraction is - // device/profile territory, not a publishable behavior tweak. + // Union of the two optgroups; "Retraction when switching material" keys are excluded + // (toolchange retraction is device/profile territory, not a publishable behavior tweak). static const std::set printer_keys = [] { std::set keys; for (const PublishablePrinterOption &opt : publishable_printer_retraction_options()) @@ -113,9 +110,8 @@ std::vector collect_dirty_settings_keys(const PresetBundle& bundle) } }; - // Print and printer presets each track a single edited preset; filaments may span - // multiple slots (multi-material). Union the dirty keys of each collection's edited - // preset; this feeds only the Publish dialog's pre-check. + // Union the dirty keys of each collection's edited preset (filaments may span multiple + // slots); feeds only the Publish dialog's pre-check. append_dirty(bundle.prints.current_dirty_options(true)); append_dirty(bundle.printers.current_dirty_options(true)); append_dirty(bundle.filaments.current_dirty_options(true)); @@ -131,12 +127,11 @@ DynamicPrintConfig filter_published_config( DynamicPrintConfig filtered; std::set 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). + // Never masked (whole-vector serialization): identity, plate geometry, process/printer + // keys and partially-published material keys. std::set 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. + // "Full" entries only: base key -> author slots whose values must survive; other slots are + // masked to their defaults so a full publish does not leak unrelated slot data. std::map> full_slot_map; // 1. Mandatory material identity & slot count keys for 3MF validation/normalization @@ -183,8 +178,7 @@ DynamicPrintConfig filter_published_config( 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). + // Full-publish keys: mask 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()) @@ -195,9 +189,8 @@ DynamicPrintConfig filter_published_config( } } - // 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). + // Mask non-published vector slots with the option default; keys without a default stay + // unmasked (whole vector, matching partial-publish behavior). auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set &keep_slots) { auto *vec = dynamic_cast(&opt); if (vec == nullptr || vec->size() == 0 || def == nullptr || !def->default_value) @@ -212,7 +205,7 @@ DynamicPrintConfig filter_published_config( vec->set_at(def->default_value.get(), idx, 0); }; - // Copy selected options from full_config into filtered config + // Copy the selected options from full_config into the filtered config. for (const std::string &key : base_keys_to_include) { if (const ConfigOption *opt = full_config.option(key)) { ConfigOption *cloned = opt->clone(); diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index aadf0d3234..ecb51523f5 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -6,78 +6,63 @@ namespace Slic3r { class PresetBundle; -// Structural / non-publishable setting keys, shared by the Publish dialog and the published-3MF -// overlay path in PresetBundle::load_config_file_config. These keys must never be published -// because they would rewrite the user's preset inheritance/structure. This is the single -// source of truth for the denylist. +// Structural keys that must never be published (single source of truth for the denylist): +// publishing them would rewrite the user's preset inheritance/structure. const std::set& publish_structural_keys(); -// One option row of the printer tab's "Retraction" / "Z-Hop" optgroups (TabPrinter::build_fff, -// Tab.cpp). Key and icon id are kept together so the tab can later be migrated onto these -// lists; publishable_printer_keys() is their union, and the published-3MF loader/dialog must -// never accept printer keys outside it. +// 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. struct PublishablePrinterOption { const char *key; // config key, e.g. "retraction_length" const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length" }; -// The printer tab's "Retraction" optgroup options, in tab order. +// The printer tab's "Retraction" / "Z-Hop" optgroup options, in tab order. const std::vector& publishable_printer_retraction_options(); -// The printer tab's "Z-Hop" optgroup options, in tab order. const std::vector& publishable_printer_z_hop_options(); -// Printer-class retraction / z-hop keys that are publishable: the union of -// publishable_printer_retraction_options() and publishable_printer_z_hop_options(). The -// published-3MF overlay applies printer keys only when their base key is in this allowlist; -// any other printer-class key in a published file is contract-excluded (never applied, never -// reported as skipped). +// Union of the two optgroup option lists; the published-3MF overlay applies printer keys only +// when their base key is in this allowlist (anything else is contract-excluded). const std::set& publishable_printer_keys(); -// Returns the union of setting keys that differ from the base/system preset across the current -// print, printer and filament presets (feeds the Publish dialog's pre-check). +// Union of setting keys differing from the base/system preset across the current print, +// printer and filament presets (feeds the Publish dialog's pre-check). std::vector collect_dirty_settings_keys(const PresetBundle& bundle); -// A material-qualified set of published setting keys, chosen by the author for one of the -// materials used in the project. The identity fields let the receiver apply the keys only -// when a matching material is selected: filament_id is the most precise (stable across -// machines/vendors when present, empty for user presets); filament_type + filament_vendor -// are the fallback. Keys are base keys (no "#N" variant suffix). +// Per-slot published material keys, applied positionally (author slot N -> receiver slot N). +// The identity fields are carried for reference/notification labels only; the type gate +// (publish_type) is the author's explicit opt-in for requiring a material type. struct PublishedMaterialEntry { std::string filament_type; // material family, e.g. "PLA" (may be empty) std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty) std::string filament_id; // stable material id, e.g. "GFL99" (may be empty) - // 0-based author filament slot this entry's values came from; -1 = legacy/unspecified - // (files written before the slot field). Slotted entries apply to the receiver's Nth - // matching preset (N = the slot's ordinal among the author's matching slots); legacy - // entries apply to every matching receiver preset. + // Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id"); + // used on load to match the exact published variant, which filament_id alone cannot + // distinguish ("Generic PLA" and "Generic PLA Matte" share their inherited id). + std::string setting_id; + // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector 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. + // "Full Publish": serialize the whole filament preset (full_keys); the type gate then + // decides whether the receiver keeps its material (type match) or is replaced. 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. + // All non-structural filament keys of the author's slot preset; values travel in the file + // config, masked to the author's slot index. std::vector 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. + // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on + // mismatch the slot is replaced with a same-type filament from the receiver's library. bool publish_type{false}; std::string publish_type_value; - // Required filament colour for this slot, applied on load regardless of the type match. + // Required filament colour, 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. +// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact. std::string normalize_filament_type(const std::string& type); -// 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. +// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys, +// material keys, identity fields and plate geometry keys. class DynamicPrintConfig; DynamicPrintConfig filter_published_config( const DynamicPrintConfig &full_config, diff --git a/src/slic3r/GUI/ConfigValueFormatter.hpp b/src/slic3r/GUI/ConfigValueFormatter.hpp index e7ed549930..c51a8e8a17 100644 --- a/src/slic3r/GUI/ConfigValueFormatter.hpp +++ b/src/slic3r/GUI/ConfigValueFormatter.hpp @@ -11,18 +11,16 @@ class DynamicPrintConfig; namespace GUI { -// Return the value of the given option (identified by opt_key, which may contain -// a "#" suffix) formatted as a human readable string. +// Human-readable value of opt_key (may carry a "#" suffix) in config. wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config); -// Return the full label of the given option (identified by opt_key, which may contain -// a "#" suffix). Returns "N/A" when the option is not set. +// Full label of opt_key; "N/A" when the option is not set. wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config); -// Strip the "#" suffix (if any) from the given option key. +// Strip the "#" suffix (if any) from the option key. std::string get_pure_opt_key(const std::string& opt_key); -// Return the localized label of the currently selected value of an enum option. +// Localized label of the currently selected value of an enum option. wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1); } // namespace GUI diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3cb246529f..48f315453c 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -2837,11 +2837,11 @@ void MainFrame::init_menubar_as_editor() auto publish_handler = [this](wxCommandEvent&) { publish_project(); }; #ifndef __APPLE__ - append_menu_item(fileMenu, wxID_ANY, _L("Publish") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"), + append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + 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 + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"), + append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + 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 diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cef3613ae9..5bd1fa6d87 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7208,9 +7208,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - // BBS: a "published" 3MF project carries a flag plus a list of author-selected - // setting keys. When present, keep the user's currently-selected presets and - // overlay only the published keys onto the edited presets on load. + // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; + // on load keep the user's current presets and overlay only those keys. PublishedConfig published_config; if (model.model_info != nullptr) { auto published_it = model.model_info->metadata_items.find("published"); @@ -7249,6 +7248,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ entry.filament_vendor = mat["filament_vendor"].get(); if (mat.contains("filament_id") && mat["filament_id"].is_string()) entry.filament_id = mat["filament_id"].get(); + if (mat.contains("setting_id") && mat["setting_id"].is_string()) + entry.setting_id = mat["setting_id"].get(); } if (m.contains("slot") && m["slot"].is_number_integer()) entry.slot = m["slot"].get(); @@ -7257,7 +7258,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const auto &k : *entry_keys_it) if (k.is_string()) entry.keys.emplace_back(k.get()); - // Filament-publishing-v2 fields; absent in legacy files. + // Fields always written by the current exporter. if (m.contains("full") && m["full"].is_boolean()) entry.full = m["full"].get(); const auto entry_full_keys_it = m.find("full_keys"); @@ -7282,10 +7283,10 @@ std::vector Plater::priv::load_files(const std::vector& 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. + // BBS: a "published" 3MF loads as a new project: its path must not become the + // project filename (Save/Ctrl-S would overwrite the shared file), and the + // published metadata is consumed above and stripped so a later save is 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) { @@ -13309,10 +13310,10 @@ void Plater::load_project(wxString const& filename2, 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. + // A "published" 3MF loads as a new project: its path must not become the project + // filename (Save/Ctrl-S prompts for a destination instead of overwriting it); + // reset() already cleared the project name, so restore the default title and keep + // the file in recents. p->set_project_name(_L("Untitled")); if (!filename.IsEmpty()) wxGetApp().mainframe->add_to_recent_projects(filename); @@ -16225,10 +16226,9 @@ void Plater::export_core_3mf() export_3mf(path_u8, SaveStrategy::Silence); } -// Export the current project as a "published" 3MF. This is a pure export: unlike save_project(), -// it never touches the project's file name, dirty state, backup path or title, and the -// published metadata is attached to the model only for the duration of the export so the -// in-memory project stays exactly as it was (a later Save Project produces a normal 3MF). +// Export the current project as a "published" 3MF: a pure export that never touches the +// project's file name, dirty state, backup path or title, and attaches the published metadata +// to the model only for the duration of the export (a later Save Project is a normal 3MF). int Plater::export_published_3mf(const std::vector& published_keys, const std::vector& material_keys) { wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:")); @@ -16240,14 +16240,14 @@ int Plater::export_published_3mf(const std::vector& 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}, {"setting_id", e.setting_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 - // in-memory project pristine (the published flag lives only in the exported file). + // Save the previous metadata so it can be restored after the export, keeping the in-memory + // project pristine (the published flag lives only in the exported file). const bool had_model_info = (model.model_info != nullptr); const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end()); const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end()); @@ -16261,15 +16261,13 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items["published_keys"] = j.dump(); model.model_info->metadata_items["published_material_keys"] = jm.dump(); - // Minimal published export: filter full_config to only the published keys, material keys, - // identity fields, and plate geometry keys, and omit project-embedded preset dumps. + // Minimal published export: filter full_config to the published keys, material keys, + // identity fields and plate geometry keys, and omit project-embedded preset dumps. DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); - // Same file layout save_project() uses for its project files, plus SaveStrategy::Silence and SaveStrategy::MinimalPublished: - // without it export_3mf() calls set_project_filename() on success, which would make this - // pure export the current project file. Silence keeps the project state untouched, exactly - // like export_core_3mf(). + // Same file layout as save_project(), plus Silence (so export_3mf does not set the project + // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index d6ec283409..826fe29a5a 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -495,9 +495,8 @@ public: void export_gcode_3mf(bool export_all = false); void send_gcode_finish(wxString name); void export_core_3mf(); - // Export the current project as a "published" 3MF: embeds the author-selected settings - // (published_keys / published_material_keys) into the file's metadata. A pure export: the - // in-memory project (filename, dirty state, model_info metadata) is left untouched. + // Export a "published" 3MF embedding the author-selected settings in the file metadata; a + // pure export that leaves the in-memory project untouched. int export_published_3mf(const std::vector& published_keys, const std::vector& material_keys); static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function notify_func = {}); void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 009f672d3e..f62d7af3a4 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -23,8 +23,8 @@ namespace Slic3r { namespace GUI { namespace { -// Menu ids for show_menu(). Dedicated range above the standard ids so the popup cannot -// collide with application-level bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). +// Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level +// bindings (e.g. MainFrame's recent-files wxID_FILE1.. range). enum { kPublishSelectAll = wxID_HIGHEST + 1, kPublishDeselectAll, @@ -49,9 +49,7 @@ PublishMaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& return identity; } -// "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in -// PresetBundle::load_vendor_configs_from_json (PresetBundle.cpp) and -// PresetCollection::set_custom_preset_alias (Preset.cpp). +// "Generic PLA @System" -> "Generic PLA"; mirrors the alias derivation in PresetBundle.cpp. std::string material_display_name(const std::string& preset_name) { const size_t at = preset_name.find_first_of('@'); @@ -62,8 +60,8 @@ std::string material_display_name(const std::string& preset_name) return bare.empty() ? preset_name : bare; } -// Human-readable section title for a filament slot: the resolved preset name, -// falling back to the filament type, then to the generic "Material". +// Section title for a filament slot: the resolved preset name, then the filament type, then +// the generic "Material". wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPrintConfig& full) { if (slot < bundle->filament_presets.size()) { @@ -165,8 +163,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - // Publish is always allowed: no settings selected means a publish with - // no settings override. + // Publish is always allowed: no settings selected means no settings override. EndModal(wxID_OK); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); @@ -183,12 +180,11 @@ PublishSettingsDialog::~PublishSettingsDialog() {} void PublishSettingsDialog::build_option_model() { - // Structural / non-publishable keys, shared with the published-3MF overlay - // path (see libslic3r/PublishSettings.hpp). + // Structural / non-publishable keys, shared with the published-3MF overlay path. const std::set& denylist = publish_structural_keys(); - // Base keys already added in the print/printer sections. Printer rows share - // this set: a base key appears once (per-extruder "#N" variants collapse to - // the first occurrence - acceptable MVP; the per-extruder context is lost). + // Base keys already added in the print/printer sections. Printer rows share this set: + // per-extruder "#N" variants collapse to the first occurrence (acceptable MVP; the + // per-extruder context is lost in the UI). std::set added; PresetBundle* bundle = wxGetApp().preset_bundle; @@ -198,17 +194,17 @@ void PublishSettingsDialog::build_option_model() m_info_allsel = _L("All items selected..."); m_info_empty = _L("No matching items..."); - // Keep the tab order explicit: Section's enum order is Print, Printer, - // Material, while the dialog presents Printer, Filament, Process. + // Tab order differs from Section's enum order (Print, Printer, Material): the dialog + // presents Printer, Filament, Process. m_sections.reserve(3); const Section tab_order[] = {Section::Printer, Section::Material, Section::Print}; for (Section kind : tab_order) section_group_for(kind); bind_tab_events(); - // Shared per-option label/value computation; returns false when the option - // must be skipped (denylisted / unknown / empty label). value is the pure - // stringified value; unit is the translated sidetext (may be empty). + // Shared per-option label/value computation; returns false when the option must be skipped + // (denylisted / unknown / empty label). value is the stringified value; unit the translated + // sidetext (may be empty). auto option_text = [&denylist, &full](const std::string& opt_id, const std::string& pure_key, wxString& label, wxString& value, wxString& unit) -> bool { if (denylist.count(pure_key) > 0) @@ -224,9 +220,8 @@ void PublishSettingsDialog::build_option_model() return true; }; - // --- Phase 1: printer per-extruder retraction settings (displayed first, - // mirroring the sidebar's Printer group). The printer tab's - // "Extruder"/"Extruder N" pages carry the per-extruder retraction options. + // --- Phase 1: printer per-extruder retraction settings (first, mirroring the sidebar's + // Printer group), from the printer tab's "Extruder"/"Extruder N" pages. { size_t g = section_group_for(Section::Printer); category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); @@ -238,18 +233,17 @@ void PublishSettingsDialog::build_option_model() continue; const wxString page_title = Tab::translate_category(page->title(), tab->m_type); for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { - // Allowlist on the untranslated optgroup title; the "Retraction - // when switching material" group is intentionally skipped. + // Allowlist on the untranslated optgroup title; the "Retraction when + // switching material" group is intentionally skipped. if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") continue; const wxString subcategory = _(optgroup->title); for (const auto& opt : optgroup->opt_map()) { const std::string& opt_id = opt.first; const std::string& pure_key = opt.second.first; - // Per-extruder "#N" variants collapse to the first base key. The row stores - // the BASE key (whole-vector semantics on load: the size-guarded apply - // copies the author's full vector), while the "#0" opt_id is only used to - // display the first extruder's value. + // Per-extruder "#N" variants collapse to the first base key. The row + // stores the base key; GetPublishedKeys() later expands it back to one + // "#N" entry per extruder so the load side can apply per-extruder values. if (!added.insert(pure_key).second) continue; wxString label, value, unit; @@ -264,8 +258,8 @@ void PublishSettingsDialog::build_option_model() } } - // --- Phase 2: per-material sections synthesized from the filament tab's - // "Setting Overrides" page, under the Filament group. + // --- Phase 2: per-material sections synthesized from the filament tab's "Setting + // Overrides" page, under the Filament group. { size_t g = section_group_for(Section::Material); Tab* filament_tab = nullptr; @@ -283,17 +277,16 @@ void PublishSettingsDialog::build_option_model() } if (overrides_page != nullptr) { - // One section per filament slot: a 4-slot printer (e.g. 1 PLA + - // 3 PETG) shows 4 separate pages, each disambiguated internally by - // its colour chip and slot identity while displaying the bare name. + // One section per filament slot (a 4-slot printer shows 4 pages), each + // disambiguated by its colour chip and slot identity while showing the bare name. for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { 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. + // Material requirement rows: an optional filament colour and/or a + // vendor-agnostic material type for this slot, 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; @@ -309,18 +302,16 @@ void PublishSettingsDialog::build_option_model() RowKind::Type); } - // A material section must not repeat a key; the same key may - // appear in other material sections - that is intended. + // A material section must not repeat a key; the same key may appear in other + // material sections - that is intended. std::set material_added; for (const ConfigOptionsGroupShp& optgroup : overrides_page->m_optgroups) { - // Allowlist on the untranslated optgroup title; the - // "Ironing" group is intentionally skipped. + // Allowlist on the untranslated optgroup title; "Ironing" is skipped. if (optgroup->title != "Retraction" && optgroup->title != "Retraction when switching material") continue; for (const auto& opt : optgroup->opt_map()) { - // Row keys are base keys (no "#N"): the load side - // matches the material and uses the author's slot. + // Row keys are base keys; the load side applies them positionally. const std::string& opt_id = opt.first; std::string base = opt_id.substr(0, opt_id.find('#')); if (!material_added.insert(base).second) @@ -381,17 +372,15 @@ void PublishSettingsDialog::build_option_model() } } - // Pre-check the dirty (modified) settings and mark them bold. The base-key - // match covers all sections; collect_dirty_settings_keys already unions the - // prints, printers and filaments of the bundle. + // Pre-check the dirty (modified) settings and mark them bold (base-key match, across all + // sections; collect_dirty_settings_keys unions the prints, printers and filaments). std::set dirty_base; for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) { auto n = key.find('#'); 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. + // The Color/Type requirement rows are not "dirty overrides": never auto-checked. if (row.kind != RowKind::Setting) continue; std::string base = row.key.substr(0, row.key.find('#')); @@ -402,8 +391,8 @@ void PublishSettingsDialog::build_option_model() } } - // 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. + // 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].full_check != nullptr) m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); }); @@ -449,6 +438,7 @@ size_t PublishSettingsDialog::section_group_for(Section kind) section.icon_name = "process"; break; } + section.icon_bmp = ScalableBitmap(this, section.icon_name, 16); constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | wxTR_FULL_ROW_HIGHLIGHT; @@ -466,7 +456,10 @@ size_t PublishSettingsDialog::section_group_for(Section kind) page_sizer->Add(section.page_host, 1, wxEXPAND | wxTOP, FromDIP(4)); section.page->SetSizer(page_sizer); - m_outer_tabs->AppendItem(section.title); + if (section.icon_bmp.bmp().IsOk()) + m_outer_tabs->AppendItem(section.title, section.icon_bmp.bmp()); + else + m_outer_tabs->AppendItem(section.title); m_outer_host_sizer->Add(section.page, 1, wxEXPAND); section.page->Hide(); m_sections.push_back(std::move(section)); @@ -610,7 +603,7 @@ void PublishSettingsDialog::add_row_ui(const std::string& key, 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). + // normalized type, not author-editable). 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"))); @@ -643,8 +636,7 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index) void PublishSettingsDialog::set_row_bold(Row& row, bool bold) { - // Real set/clear: rebase on the dialog's body font so that clearing bold - // restores the exact original font (the old CheckList::SetBold was one-way). + // Rebase on the dialog's body font so clearing bold restores the exact original font. row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13); } @@ -718,12 +710,11 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text) Freeze(); wxString filter = filter_text.Lower(); - // Pseudo filters (menu only): show only checked ("::sel") or only - // unchecked ("::nonsel") rows. + // Pseudo filters (menu only): show only checked ("::sel") or only unchecked ("::nonsel"). const bool pseudo = (filter == "::sel" || filter == "::nonsel"); m_fb_sizer->Show(!pseudo); - // Update row matches first; page and optgroup visibility is applied below. + // Row matches are computed first; page and optgroup visibility is applied below. if (pseudo) { if (m_filter_ctrl->GetValue().Lower() != filter) { m_filter_ctrl->ChangeValue(filter); @@ -808,8 +799,7 @@ void PublishSettingsDialog::apply_visibility() void PublishSettingsDialog::select_all(bool value) { - // "All" does not auto-enable gated material sections; "None" leaves a gated - // row's preserved value untouched. + // "All" skips disabled (gated) rows; "None" leaves a gated row's preserved value. for (Row& row : m_rows) if (row.check->IsEnabled()) row.check->SetValue(value); @@ -829,19 +819,18 @@ bool PublishSettingsDialog::row_is_visible(const Row& row) const void PublishSettingsDialog::select_visible(bool value) { wxString filter = m_filter_ctrl->GetValue().Lower(); - // In a pseudo-filter view the rows being toggled would all disappear; - // drop the filter afterwards so the result stays visible. + // In a pseudo-filter view the rows being toggled would all disappear; drop the filter + // afterwards so the result stays visible. bool clear_pseudo = (!value && filter == "::nonsel") || (value && filter == "::sel"); - // Toggle the rows that are visible under the *current* filter. + // Toggle the rows visible under the *current* filter. for (Row& row : m_rows) if (row_is_visible(row)) row.check->SetValue(value); if (clear_pseudo) { - // Note: SetValue() may fire wxEVT_TEXT on some platforms, which - // re-enters apply_filter() - that is fine, the rows above were already - // toggled and the trailing call below is idempotent. + // Note: SetValue() may fire wxEVT_TEXT on some platforms, re-entering apply_filter() - + // that is fine; the rows above were already toggled and the trailing call is idempotent. m_filter_ctrl->ChangeValue(""); apply_filter(""); // resync visibility and the All/None bar } @@ -895,12 +884,31 @@ void PublishSettingsDialog::show_menu(wxMouseEvent& evt) std::vector PublishSettingsDialog::GetPublishedKeys() const { std::vector out; - // Process and printer sections both travel through published_keys (the load-side - // overlay applies process keys to the prints edited preset and the allowlisted - // printer keys to the printers edited preset). Material keys use a separate API. - for (const Row& row : m_rows) - if ((row.section == Section::Print || row.section == Section::Printer) && row.check->GetValue()) + // Process and printer sections both travel through published_keys (the load-side overlay + // applies process keys to the prints edited preset and the allowlisted printer keys to the + // printers edited preset); material keys use a separate API. + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + for (const Row& row : m_rows) { + if ((row.section != Section::Print && row.section != Section::Printer) || !row.check->GetValue()) + continue; + if (row.section == Section::Printer) { + // Printer rows store the base key (per-extruder "#N" variants collapsed during + // build). Publish every extruder element so the load side can apply per-extruder + // values even when the receiver has a different extruder count; a scalar printer + // key is published as-is. + const std::string base_key = row.key.substr(0, row.key.find('#')); + if (const ConfigOption* opt = full.option(base_key)) { + if (const auto* vec = dynamic_cast(opt)) { + for (size_t i = 0; i < vec->size(); ++i) + out.push_back(base_key + "#" + std::to_string(i)); + } else { + out.push_back(base_key); + } + } + } else { out.push_back(row.key); + } + } return out; } @@ -915,8 +923,15 @@ std::vector PublishSettingsDialog::GetPublishedM entry.filament_vendor = cat.filament_vendor; entry.filament_id = cat.filament_id; entry.slot = static_cast(cat.filament_slot); - // "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. + // The author's preset id distinguishes exact variants that share filament_id + // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely. + PresetBundle *bundle = wxGetApp().preset_bundle; + if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) { + if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) + entry.setting_id = preset->setting_id; + } + // "Full Publish": the whole filament preset is embedded; type and colour are implicitly + // published, and the per-key rows are disabled / their state ignored. if (cat.full_check != nullptr && cat.full_check->GetValue()) { entry.full = true; entry.full_keys = full_keys_for_slot(); @@ -946,8 +961,7 @@ std::vector PublishSettingsDialog::GetPublishedM 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. + // Nothing checked at all -> nothing to write. if (!entry.keys.empty() || entry.publish_type || entry.publish_color) out.push_back(std::move(entry)); } @@ -956,9 +970,9 @@ std::vector PublishSettingsDialog::GetPublishedM std::vector PublishSettingsDialog::full_keys_for_slot() const { - // The canonical filament preset keys, minus the structural keys the published overlay must + // 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 + // member of Preset::filament_options). Values travel in the exported config, masked to // this slot, and are applied on load onto the receiver's slot. const std::set& denylist = publish_structural_keys(); std::vector keys; @@ -971,7 +985,7 @@ std::vector PublishSettingsDialog::full_keys_for_slot() const void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) { - // Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint themselves. + // Rescale toolbar bitmaps and icons; collapse chevrons are vector-drawn and repaint. m_search.msw_rescale(); m_menu.msw_rescale(); m_filter_box->SetIcon(m_search.bmp()); @@ -1000,8 +1014,13 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) cat.list_sizer->Layout(); } - for (SectionGroup& section : m_sections) + for (size_t s = 0; s < m_sections.size(); ++s) { + SectionGroup& section = m_sections[s]; + section.icon_bmp.msw_rescale(); + if (section.icon_bmp.bmp().IsOk()) + m_outer_tabs->SetItemBitmap(s, section.icon_bmp.bmp()); section.tabs->Rescale(); + } // Refresh the per-row Color chips at the new DPI. for (Row& row : m_rows) { diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 70b1dc8a2d..74fc6a7077 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -28,25 +28,21 @@ struct PublishMaterialIdentity std::string id; }; -// Dialog that lets a model author select which settings get embedded in a 3MF. -// Settings are grouped into the same nested custom tab layout used by the -// Process settings: Printer, Filament, and Process outer tabs, with category or -// material tabs inside each section. Optgroups are ordinary grouped headers. -// Modified (dirty) settings are pre-checked and shown bold. On OK, the print -// rows become the "published_keys" list and the material rows become the -// per-material "published_material_keys". +// Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout +// mirroring the Process settings (Printer / Filament / Process outer tabs, category or material +// tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become +// "published_keys" and the material rows become "published_material_keys". class PublishSettingsDialog : public DPIDialog { public: PublishSettingsDialog(wxWindow* parent = nullptr); ~PublishSettingsDialog(); - // The selected print-section setting keys (in display order). Keys may - // contain '#'. + // The selected print/printer setting keys (in display order); printer keys carry a '#N' + // per-extruder suffix. std::vector GetPublishedKeys() const; - // The selected keys grouped per material: one entry per material section - // with at least one checked key. Keys are base keys (no "#N" suffix). + // The selected keys grouped per material section (base keys, no '#N' suffix). std::vector GetPublishedMaterialKeys() const; protected: @@ -56,9 +52,9 @@ private: // Which part of the settings the row/category came from. enum class Section { Print, Printer, Material }; - // 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. + // One selectable setting row: a checkbox (setting name) plus a value label and an optional + // grey unit label. key is the full config key, possibly with 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) @@ -113,9 +109,9 @@ private: ScalableBitmap icon_bmp; // scalable bitmap for DPI changes wxStaticBitmap* icon{nullptr}; wxStaticBitmap* filament_color_chip{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. + wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere) + // "Full Publish": while checked, the whole slot preset is serialized and its rows + // (incl. Color/Type) are disabled. bool full{false}; wxCheckBox* full_check{nullptr}; // Material identity, only for Section::Material categories. @@ -134,6 +130,7 @@ private: wxString title; // _L("Printer") / _L("Filament") / _L("Process") Section kind{Section::Print}; // maps 1:1 to the display group std::string icon_name; // "printer" / "filament" / "process" + ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change wxPanel* page{nullptr}; TabCtrl* tabs{nullptr}; wxPanel* page_host{nullptr}; diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index d9d6833a5a..34ff31183f 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -501,12 +501,10 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { } } -// The "Publish" feature stores a published flag plus a JSON array of author-selected setting keys -// in model.model_info->metadata_items. This locks the serialization contract: both keys must survive -// a store_bbs_3mf -> load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior — -// keeping the user's currently-selected presets and overlaying only the published keys onto them — -// is exercised headlessly in "Published 3MF overlays only the author-selected process keys onto the -// edited preset" in test_preset_bundle_loading.cpp.) +// Locks the serialization contract of the "Publish" metadata: the published flag and the +// published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf -> +// load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised +// headlessly in test_preset_bundle_loading.cpp.) SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") { GIVEN("a model carrying published metadata") { Model model; @@ -518,8 +516,8 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada model.model_info->metadata_items["published"] = "1"; model.model_info->metadata_items["published_keys"] = R"(["layer_height","wall_thickness"])"; - // store_bbs_3mf stages Metadata/project_settings.config through the model's backup path; - // point it at a writable temp dir (the default lives under a read-only root in CI). + // store_bbs_3mf stages project_settings.config through the model's backup path; point + // it at a writable temp dir (the default lives under a read-only root in CI). ScopedTemporaryDir backup_dir("orca_pub"); model.set_backup_path(backup_dir.string()); @@ -564,8 +562,8 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada } } -// A project saved without the Publish metadata (i.e. a normal 3MF) must load identically: the -// loader must not fabricate a "published" flag or published_keys for files that never carried them. +// A normal 3MF (no Publish metadata) must load identically: the loader must not fabricate a +// "published" flag or published_keys for files that never carried them. SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { GIVEN("a model without any published metadata") { Model model; @@ -610,10 +608,8 @@ SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { } } -// The "Publish" feature can also store material-qualified setting keys, one entry per material -// the author uses, in model.model_info->metadata_items. This locks the serialization contract -// for that entry list: the JSON must survive a store_bbs_3mf -> load_bbs_3mf round-trip -// verbatim, exactly like the plain published_keys array. +// Locks the serialization contract of the published_material_keys metadata: the per-entry JSON +// must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like published_keys. SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") { GIVEN("a model carrying published material keys metadata") { Model model; @@ -658,8 +654,7 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf REQUIRE(dst_model.model_info->metadata_items["published_material_keys"] == material_keys_json); // The value must parse back to one material entry carrying the nested identity - // object, the author slot ordinal and the key list, so the loader can match it - // to the receiver's filaments. + // object, the author slot ordinal and the key list. nlohmann::json entries = nlohmann::json::parse(material_keys_json); REQUIRE(entries.is_array()); REQUIRE(entries.size() == 1); @@ -689,19 +684,19 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded const std::vector published_keys = { "layer_height", "retraction_length" }; const std::vector material_keys = { - { "PLA", "Generic", "GFL99", 0, { "filament_retraction_length" } } + { "PLA", "Generic", "GFL99", "", 0, { "filament_retraction_length" } } }; DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); - // Filtered config must contain the published keys and identity keys + // Filtered config keeps the published and identity keys... REQUIRE(filtered_cfg.option("layer_height") != nullptr); REQUIRE(filtered_cfg.option("retraction_length") != nullptr); REQUIRE(filtered_cfg.option("filament_colour") != nullptr); REQUIRE(filtered_cfg.option("filament_type") != nullptr); REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); - // Non-published settings should NOT be in filtered_cfg + // ...and drops everything else. REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); @@ -716,7 +711,7 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); - // Create a fake project preset to verify it gets omitted with MinimalPublished + // Create a fake project preset to verify MinimalPublished omits it. Preset preset(Preset::TYPE_PRINT, "TestPrintPreset"); preset.config = full_cfg; std::vector project_presets = { &preset }; @@ -753,9 +748,9 @@ 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. +// 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(); @@ -787,9 +782,9 @@ SCENARIO("Full-publish entries filter the whole slot and mask the other slots", } } -// 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]") { +// 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 extended 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"; @@ -797,7 +792,7 @@ SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata 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":""}])"; + R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf"},"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(); model.model_info->metadata_items["published_material_keys"] = material_keys_json; @@ -832,7 +827,7 @@ SCENARIO("Published 3MF round-trips the filament-publishing-v2 material metadata 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. + // The value must parse back with every extended field intact. nlohmann::json entries = nlohmann::json::parse(material_keys_json); REQUIRE(entries.is_array()); REQUIRE(entries.size() == 1); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 8716af417d..3a9ff77be9 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -549,36 +549,26 @@ TEST_CASE("A printer specific filament supersedes the generic library filament w } // A "published" 3MF keeps the user's currently-selected presets and overlays only the -// author-selected process keys onto the edited preset. Mirrors the GUI load path -// (src/slic3r/GUI/Plater.cpp): Preset::normalize before load_config_model, then the -// published overlay in PresetBundle::load_config_file_config. +// author-selected process keys onto the edited preset (mirrors the GUI load path: normalize +// before load_config_model, then the overlay in load_config_file_config). TEST_CASE("Published 3MF overlays only the author-selected process keys onto the edited preset", "[Preset][Bundle][Published]") { // The file config the GUI builds from a .3mf's project settings. auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); // The loader derives the filament count from filament_colour and throws when it is - // empty ("Invalid configuration file"); a 3mf always carries it. + // empty; a 3mf always carries it. config.opt("filament_colour")->values = { "#FF0000" }; - // Process scalar key. - config.opt_float("layer_height") = 0.28; - // Process vector key, size 2 to match the edited preset's resized vector. - config.opt("wiping_volumes_extruders")->values = { 140., 150. }; - // Process vector key, size 2: deliberately mismatched against the edited preset. - config.opt("post_process")->values = { "script-a", "script-b" }; - // A filament key: published files may still carry legacy filament keys. - config.opt("nozzle_temperature")->values = { 220 }; - // A structural (denylisted) key: must be silently ignored even if a hand-crafted - // file lists it as published. full_print_config() omits the *_settings_id keys (they - // have no static counterpart), while a real 3mf project config carries it, so create - // it explicitly. + config.opt_float("layer_height") = 0.28; // process scalar + config.opt("wiping_volumes_extruders")->values = { 140., 150. }; // matching-size vector + config.opt("post_process")->values = { "script-a", "script-b" }; // mismatched vector + config.opt("nozzle_temperature")->values = { 220 }; // filament key (not applied anywhere) + // Structural (denylisted) key: must be silently ignored. full_print_config() omits the + // *_settings_id keys, so create one explicitly. config.opt_string("print_settings_id", true) = "file process"; - // Project-level filament/purge data: must NOT cross over in published mode. - config.opt("flush_multiplier")->values = { 2., 2. }; - // A project-level option, to pin the project_config.apply_only() invariant. - config.opt("wipe_tower_x")->values = { 100. }; - // The author's bed type must NOT cross over either: the receiver keeps its own. - config.option("curr_bed_type")->setInt(BedType::btPC); + config.opt("flush_multiplier")->values = { 2., 2. }; // must NOT cross over + config.opt("wipe_tower_x")->values = { 100. }; // plate geometry, does cross over + config.option("curr_bed_type")->setInt(BedType::btPC); // must NOT cross over return config; }; @@ -595,14 +585,14 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values = { 10., 20. }; bundle.prints.get_edited_preset().config.opt("post_process")->values = { "existing-script" }; bundle.prints.get_edited_preset().config.opt_string("print_settings_id") = "user process"; - // Capture the ctor-seeded project_config values; the assertions below check that the - // published load leaves them untouched rather than hardcoding the defaults. + // Capture the ctor-seeded project_config values so the assertions below check the load + // leaves them untouched rather than hardcoding the defaults. const std::vector seed_filament_colour = bundle.project_config.opt("filament_colour")->values; const std::vector seed_flush_multiplier = bundle.project_config.opt("flush_multiplier")->values; const int seed_bed_type = bundle.project_config.option("curr_bed_type")->getInt(); DynamicPrintConfig config = make_file_config(); - // The GUI normalizes the config before load; do the same so only the production path is exercised. + // The GUI normalizes the config before load; mirror that so only the production path runs. Preset::normalize(config); PublishedConfig pub; @@ -610,48 +600,36 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the pub.published_keys = published_keys; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // a) The process scalar is overlaid onto the edited preset. + // a) Process scalar overlaid; matching-size vector applied, mismatched one lands in + // skipped_keys; applied keys are not reported. CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); - - // b) A matching-size process vector is applied; a size-mismatched one is neither applied - // nor reported as skipped by accident — it lands in skipped_keys. CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 140., 150. }); CHECK(bundle.prints.get_edited_preset().config.opt("post_process")->values == std::vector{ "existing-script" }); CHECK(contains_key(pub.skipped_keys, "post_process")); - - // c) A filament key is never applied anywhere and is reported as skipped (warning). - CHECK(bundle.prints.get_edited_preset().config.option("nozzle_temperature") == nullptr); - CHECK(contains_key(pub.skipped_keys, "nozzle_temperature")); - - // d) A structural key is silently ignored: neither applied nor reported as skipped. - CHECK(bundle.prints.get_edited_preset().config.opt_string("print_settings_id") == "user process"); - CHECK_FALSE(contains_key(pub.skipped_keys, "print_settings_id")); - - // Applied keys are not reported as skipped. CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); CHECK_FALSE(contains_key(pub.skipped_keys, "wiping_volumes_extruders")); - // e) project_config.apply_only() still runs, but in published mode only the plate/bed - // geometry crosses: the file's filament/purge data must NOT port to the receiver. - // Filament colors do not port; project_config keeps its ctor-seeded values. - CHECK(bundle.project_config.opt("filament_colour")->values != std::vector{ "#FF0000" }); + // b) A filament key is never applied anywhere and is reported as skipped. + CHECK(bundle.prints.get_edited_preset().config.option("nozzle_temperature") == nullptr); + CHECK(contains_key(pub.skipped_keys, "nozzle_temperature")); + + // c) A structural key is silently ignored: neither applied nor reported as skipped. + CHECK(bundle.prints.get_edited_preset().config.opt_string("print_settings_id") == "user process"); + CHECK_FALSE(contains_key(pub.skipped_keys, "print_settings_id")); + + // d) Only plate/bed geometry crosses in published mode: filament/purge data and bed type + // stay at the ctor seeds. CHECK(bundle.project_config.opt("filament_colour")->values == seed_filament_colour); - // Purge data does not port either, and update_multi_material_filament_presets() cannot - // resurrect it (flush_multiplier stays at the ctor seed). CHECK(bundle.project_config.opt("flush_multiplier")->values == seed_flush_multiplier); - // The author's bed type does not cross over: the receiver keeps its own. CHECK(bundle.project_config.option("curr_bed_type")->getInt() == seed_bed_type); - // Plate/bed geometry still crosses. CHECK(bundle.project_config.opt("wipe_tower_x")->values == std::vector{ 100. }); - // The published path keeps the user's currently-selected presets: the edited process - // preset is the same preset as before the load. + // e) The published path keeps the user's currently-selected presets: same preset, same size. CHECK(bundle.prints.get_edited_preset().name == pre_load_name); CHECK(bundle.prints.size() == pre_load_size); - // f) Non-published control: with published=false the overlay is disabled. The file's - // presets are loaded and selected instead (the user's preset is not kept) and no - // skipped_keys are produced. + // f) Non-published control: the overlay is disabled, the file's presets are imported + // instead, and no skipped_keys are produced. PresetBundle control_bundle; const size_t control_pre_size = control_bundle.prints.size(); PublishedConfig control_pub; @@ -663,26 +641,22 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the CHECK(control_pub.skipped_keys.empty()); CHECK(control_bundle.prints.size() > control_pre_size); - // The file's layer_height reached the edited preset through the normal preset import, - // not through the published overlay. + // The file's layer_height reached the edited preset via the normal import, not the overlay. CHECK_THAT(control_bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); } -// The published printer overlay is restricted to the publishable retraction/z-hop allowlist -// (publishable_printer_keys). Matching-size retraction vectors apply; mismatched vectors are -// reported as skipped; any other printer-class key (e.g. machine_start_gcode) is -// contract-excluded: never applied and never reported. +// The published printer overlay is restricted to the publishable retraction/z-hop allowlist: +// matching-size vectors apply, mismatched vectors are reported as skipped, and any other +// printer-class key (e.g. machine_start_gcode) is contract-excluded (never applied, never +// reported). TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys onto the edited printer preset", "[Preset][Bundle][Published]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.opt("filament_colour")->values = { "#FF0000" }; Preset::normalize(config); - // Matching size (the receiver's default printer has one extruder). - config.opt("retraction_length")->values = { 1.4 }; - // Size 2: mismatched against the single-extruder receiver. - config.opt("retraction_speed")->values = { 45., 55. }; - // Printer-class but outside the allowlist: must be silently contract-excluded. - config.opt_string("machine_start_gcode") = "G28 ; from file"; + config.opt("retraction_length")->values = { 1.4 }; // matching size (1 extruder) + config.opt("retraction_speed")->values = { 45., 55. }; // size 2: mismatched + config.opt_string("machine_start_gcode") = "G28 ; from file"; // outside the allowlist PresetBundle bundle; bundle.printers.get_edited_preset().config.opt("retraction_length")->values = { 0.8 }; @@ -693,9 +667,8 @@ TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys pub.published_keys = { "retraction_length", "retraction_speed", "machine_start_gcode" }; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // Matching-size retraction vector applied to the edited printer preset. + // Matching-size retraction vector applied; mismatched vector reported as skipped. CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 1.4 }); - // Mismatched vector not applied and reported as skipped. CHECK(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values == std::vector{ 30. }); CHECK(contains_key(pub.skipped_keys, "retraction_speed")); // Contract-excluded printer key: silently ignored, absent from skipped_keys. @@ -703,10 +676,10 @@ TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys CHECK_FALSE(contains_key(pub.skipped_keys, "machine_start_gcode")); } -// A published 3MF can carry material-qualified keys; on load they are applied to the -// receiver's filament presets whose material identity matches the author's (filament_id when -// both sides have one, filament_type + vendor fallback otherwise). -TEST_CASE("Published 3MF applies material retraction keys onto the receiver's matching filament presets", "[Preset][Bundle][Published]") +// A published 3MF carries per-slot material keys; on load they are applied positionally to the +// receiver's slot N (a key-only entry has no type gate), written onto the slot's stored preset +// in place. +TEST_CASE("Published 3MF applies positional material keys onto the receiver's material presets", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -715,7 +688,6 @@ TEST_CASE("Published 3MF applies material retraction keys onto the receiver's ma // Keep the multi-extruder consistency validation happy for a 2-slot config. config.opt("filament_self_index")->values = { 1, 2 }; config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; - // Author per-slot material identity (filament_ids feeds the loader's local copy). config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; config.opt("filament_type")->values = { "PLA", "PETG" }; config.opt("filament_vendor")->values = { "Generic", "Generic" }; @@ -729,13 +701,10 @@ TEST_CASE("Published 3MF applies material retraction keys onto the receiver's ma }; PresetBundle bundle; - // Receiver materials with matching stable ids. Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.filament_id = "GFL99"; pla.config.opt_string("filament_type", 0u) = "PLA"; pla.config.opt_string("filament_vendor", 0u) = "Generic"; - // In-memory preset configs carry the per-filament retraction keys as nullable options - // (the type real filament presets hold), so access them through the nullable type. pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; pla.config.opt("filament_settings_id")->values = { "receiver-pla" }; Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); @@ -743,52 +712,46 @@ TEST_CASE("Published 3MF applies material retraction keys onto the receiver's ma petg.config.opt_string("filament_type", 0u) = "PETG"; petg.config.opt_string("filament_vendor", 0u) = "Generic"; petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // The z-hop key must exist on the receiver preset for the overlay to apply into it. petg.config.opt("filament_z_hop", true)->values = { 0.1 }; bundle.filament_presets = { "My PLA", "My PETG" }; PublishedMaterialEntry pla_entry; - pla_entry.filament_type = "PLA"; - pla_entry.filament_vendor = "Generic"; - pla_entry.filament_id = "GFL99"; - pla_entry.slot = 0; // the author's PLA slot - pla_entry.keys = { "filament_retraction_length", "filament_settings_id" }; + pla_entry.filament_id = "GFL99"; + pla_entry.slot = 0; // the author's PLA slot + pla_entry.keys = { "filament_retraction_length", "filament_settings_id" }; PublishedMaterialEntry petg_entry; - petg_entry.filament_type = "PETG"; - petg_entry.filament_vendor = "Generic"; - petg_entry.filament_id = "GFT99"; - petg_entry.slot = 1; // the author's PETG slot - petg_entry.keys = { "filament_retraction_length", "filament_z_hop" }; - // A material that does not exist on the author's side: whole entry skipped, no reporting. - PublishedMaterialEntry abs_entry; - abs_entry.filament_type = "ABS"; - abs_entry.filament_id = "GFX99"; - abs_entry.keys = { "filament_retraction_length" }; + petg_entry.filament_id = "GFT99"; + petg_entry.slot = 1; // the author's PETG slot + petg_entry.keys = { "filament_retraction_length", "filament_z_hop" }; + // A slot-less entry (no slot field, only possible in hand-crafted files): silently skipped. + PublishedMaterialEntry noslot_entry; + noslot_entry.filament_type = "ABS"; + noslot_entry.keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; - pub.material_keys = { pla_entry, petg_entry, abs_entry }; + pub.material_keys = { pla_entry, petg_entry, noslot_entry }; DynamicPrintConfig config = make_file_config(); Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // Per-slot scalar copy: the author's slot value lands in the matching receiver preset. + // The author's slot values are written onto the receiver's stored presets in place. CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_z_hop")->values == std::vector{ 0.3 }); // Structural keys inside a material entry are silently ignored: the receiver's own - // filament_settings_id is left untouched and nothing is reported for it. + // filament_settings_id is untouched and nothing is reported for it. CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_settings_id")->values == std::vector{ "receiver-pla" }); CHECK_FALSE(contains_key(pub.skipped_keys, "material:GFL99 (filament_settings_id)")); - // Everything applied; the unknown material entry produced no skipped entry. + // Everything applied; the slot-less entry produced no skipped entry. 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. +// A "full publish" slot serializes the whole filament. On load the slot is matched positionally +// against the published type: a matching receiver type still receives the author's full values +// (like a normal save/load of the filament), a mismatched type replaces it with the first +// same-type visible preset (applying the author's full values on top), and a type not in the +// receiver's library falls back to the first available visible preset. TEST_CASE("Published 3MF full-published slots replace or ignore the receiver material by type", "[Preset][Bundle][Published]") { auto make_file_config = [] { @@ -815,7 +778,7 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat return entry; }; - SECTION("type match leaves a full-published slot untouched") { + SECTION("type match applies the full dump onto the receiver's material") { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.config.opt_string("filament_type", 0u) = "PLA"; @@ -832,8 +795,9 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat 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("filament_retraction_length")->values == std::vector{ 0.5 }); + // The type matches, but a full publish behaves like a normal save: the author's values + // are written onto the slot's preset wholesale. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); CHECK(pub.skipped_keys.empty()); CHECK(pub.material_replacements.empty()); } @@ -855,47 +819,306 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - CHECK(bundle.filament_presets.size() == 1); + REQUIRE(bundle.filament_presets.size() == 1); + // The slot is re-pointed at the library's ABS preset and the author's full values are + // written onto it in place (the original 0.3 is overwritten); the receiver's own + // material is untouched. 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("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My ABS", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); + // The entry carries no identity fields, so the pick cannot be judged as a substitute. + CHECK(pub.material_replacements[0] == "slot 0: My PLA -> My ABS"); } - SECTION("no same-type match creates a temporary project-embedded custom preset") { + SECTION("no same-type match falls back to the first available visible preset") { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.config.opt_string("filament_type", 0u) = "PLA"; pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + Preset &other = add_inmemory_preset(bundle.filaments, "Other PLA"); + other.config.opt_string("filament_type", 0u) = "PLA"; + other.config.opt("filament_retraction_length", true)->values = { 0.7 }; bundle.filament_presets = { "My PLA" }; + PublishedMaterialEntry full = make_full_abs_entry(); + // The dump carries the identity too, so the fallback preset must take the author's type + // and vendor. + full.full_keys = { "filament_retraction_length", "filament_type", "filament_vendor" }; + PublishedConfig pub; pub.published = true; - pub.material_keys = { make_full_abs_entry() }; + pub.material_keys = { full }; DynamicPrintConfig config = make_file_config(); + // The author's slot 0 really is ABS. + config.opt("filament_type")->values = { "ABS", "PETG" }; 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("filament_retraction_length")->values == std::vector{ 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. + // No ABS in the library: the slot falls back to the first available visible preset (the + // unused "Other PLA") and the author's full values are written onto it, type and vendor + // included. The receiver's own material is untouched. + CHECK(bundle.filament_presets[0] == "Other PLA"); + Preset *fallback = bundle.filaments.find_preset("Other PLA", false, true); + REQUIRE(fallback != nullptr); + CHECK(fallback->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(fallback->config.opt_string("filament_type", 0u) == "ABS"); + CHECK(fallback->config.opt_string("filament_vendor", 0u) == "Generic"); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt_string("filament_type", 0u) == "PLA"); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PLA -> Other PLA (substitute: no ABS available)"); } } -// 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. +// Regression for the author's published material being skipped by a type-only replacement +// search: the slot must prefer the exact published material (filament_id) over the first other +// same-type preset, even when the exact preset is already referenced by another slot. +TEST_CASE("Published 3MF replaces a mismatched slot with the exact published material when available", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + // The receiver's second slot already uses the exact published material. + Preset &generic_pla = add_inmemory_preset(bundle.filaments, "Generic PLA"); + generic_pla.filament_id = "GFL99"; + generic_pla.config.opt_string("filament_type", 0u) = "PLA"; + generic_pla.config.opt_string("filament_vendor", 0u) = "Generic"; + generic_pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + // An unrelated PLA, unreferenced: a type-only search picks it because Generic PLA is + // referenced by slot 1. + Preset &bambu = add_inmemory_preset(bundle.filaments, "Bambu PLA Basic"); + bambu.filament_id = "GFB00"; + bambu.config.opt_string("filament_type", 0u) = "PLA"; + bambu.config.opt_string("filament_vendor", 0u) = "Bambu Lab"; + bambu.config.opt("filament_retraction_length", true)->values = { 0.4 }; + bundle.filament_presets = { "My PETG", "Generic PLA" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "GFL99"; + entry.filament_vendor = "Generic"; + entry.full_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 exact published material (id GFL99) wins over the unreferenced type-only preset. + CHECK(bundle.filament_presets[0] == "Generic PLA"); + CHECK(bundle.filament_presets[1] == "Generic PLA"); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // The receiver's own material is untouched; the unrelated PLA too. + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + CHECK(bundle.filaments.find_preset("Bambu PLA Basic", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.4 }); + // Accepted mutate tradeoff: the shared exact-material preset was mutated, so slot 1 also + // carries the author's values (the leak is documented, not accidental). + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // An exact-material pick is reported without a substitute qualifier. + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA"); + CHECK(pub.skipped_keys.empty()); +} + +// The replacement search prefers the published identity: exact filament_id, then vendor+type, +// then type only (collection order decides equal scores; the pick is reported as a substitute +// when it is not the exact published material). +TEST_CASE("Published 3MF prefers the published material identity when replacing a slot", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + auto make_entry = [] { + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "GFL99"; + entry.filament_vendor = "Generic"; + entry.full_keys = { "filament_retraction_length" }; + return entry; + }; + auto add_pla = [](PresetBundle &bundle, const char *name, const char *id, const char *vendor) { + Preset &preset = add_inmemory_preset(bundle.filaments, name); + preset.filament_id = id; + preset.config.opt_string("filament_type", 0u) = "PLA"; + preset.config.opt_string("filament_vendor", 0u) = vendor; + preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; + return &preset; + }; + auto load = [&](PresetBundle &bundle, PublishedConfig &pub) { + PublishedMaterialEntry entry = make_entry(); + 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); + }; + + SECTION("exact filament_id beats collection order") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + // "Bambu PLA Basic" sorts before "Zebra PLA"; only the latter carries the published id. + add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); + add_pla(bundle, "Zebra PLA", "GFL99", "Generic"); + bundle.filament_presets = { "My PETG" }; + + PublishedConfig pub; + load(bundle, pub); + CHECK(bundle.filament_presets[0] == "Zebra PLA"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Zebra PLA"); + } + + SECTION("vendor and type beat a type-only preset, reported as a substitute") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); + add_pla(bundle, "Zebra PLA", "ZZZ99", "Generic"); // same vendor+type, different id + bundle.filament_presets = { "My PETG" }; + + PublishedConfig pub; + load(bundle, pub); + CHECK(bundle.filament_presets[0] == "Zebra PLA"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Zebra PLA (substitute: no exact material match)"); + } + + SECTION("type-only candidates keep collection order and are reported as substitutes") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); + add_pla(bundle, "Zebra PLA", "ZZZ99", "Acme"); // no identity match at all + bundle.filament_presets = { "My PETG" }; + + PublishedConfig pub; + load(bundle, pub); + CHECK(bundle.filament_presets[0] == "Bambu PLA Basic"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Bambu PLA Basic (substitute: no exact material match)"); + } +} + +// "Generic PLA" and "Generic PLA Matte" share their inherited filament_id (OGFL99), so the +// exact variant can only be matched via the preset setting_id carried in the published file. +TEST_CASE("Published 3MF matches the exact published variant via setting_id", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "OGFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + auto add_pla = [](PresetBundle &bundle, const char *name, const char *setting_id) { + Preset &preset = add_inmemory_preset(bundle.filaments, name); + preset.setting_id = setting_id; + preset.filament_id = "OGFL99"; // shared by all Generic PLA variants + preset.config.opt_string("filament_type", 0u) = "PLA"; + preset.config.opt_string("filament_vendor", 0u) = "Generic"; + preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; + return &preset; + }; + auto load = [&](PresetBundle &bundle, PublishedConfig &pub, const std::string &setting_id) { + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "OGFL99"; + entry.filament_vendor = "Generic"; + entry.setting_id = setting_id; + entry.full_keys = { "filament_retraction_length" }; + 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); + }; + + SECTION("the published variant wins over its same-id sibling") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Generic PLA Matte", "RFs9eCKYOMUSmvZf"); + bundle.filament_presets = { "My PETG" }; + + PublishedConfig pub; + load(bundle, pub, "RFs9eCKYOMUSmvZf"); // the author published "Generic PLA Matte" + CHECK(bundle.filament_presets[0] == "Generic PLA Matte"); + CHECK(bundle.filaments.find_preset("Generic PLA Matte", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA Matte"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("without a setting_id the same-id siblings fall back to collection order") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Generic PLA Matte", "RFs9eCKYOMUSmvZf"); + bundle.filament_presets = { "My PETG" }; + + PublishedConfig pub; + load(bundle, pub, ""); // legacy file without the field + CHECK(bundle.filament_presets[0] == "Generic PLA"); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(pub.skipped_keys.empty()); + } +} + +// 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 = [] { @@ -934,9 +1157,11 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish 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("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // Type matched: keys and colour applied onto the receiver's preset in place. + Preset *pla_preset = bundle.filaments.find_preset("My PLA", false, true); + REQUIRE(pla_preset != nullptr); + CHECK(pla_preset->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(pla_preset->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(pub.skipped_keys.empty()); CHECK(pub.material_replacements.empty()); } @@ -964,10 +1189,13 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish 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. + // The slot keeps the receiver's material: the colour applies in place, the keys are + // skipped. CHECK(bundle.filament_presets[0] == "My PLA"); - CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + Preset *pla_preset = bundle.filaments.find_preset("My PLA", false, true); + REQUIRE(pla_preset != nullptr); + CHECK(pla_preset->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(pla_preset->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)")); CHECK(pub.material_replacements.empty()); } @@ -997,15 +1225,18 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish 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. + // The slot list was grown so author slot 1 has a material (the PETG preset), which + // then receives the author's values in place. REQUIRE(bundle.filament_presets.size() == 2); CHECK(bundle.filament_presets[1] == "My PETG"); + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); } } -// 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. +// The receiver's slot list grows only as far as the highest published slot: a 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 = [] { @@ -1066,18 +1297,16 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published 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("filament_colour")->values == std::vector{ "#ABCDEF" }); + // The published colour lands on the slot's preset in place. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); } - // Slot 3 published: the receiver grows to 4 so the published slot exists. The unpublished - // filler slots repeat the receiver's last preset ("Add one filament" behaviour); the - // published slot gets a visible preset not used by another slot (with a single-preset - // library it falls back to the receiver's last preset, aliasing being unavoidable). + // Slot 3 published: the receiver grows to 4 so the published slot exists. Unpublished + // filler slots repeat the receiver's last preset ("Add one filament" behaviour). { 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; @@ -1089,9 +1318,12 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published REQUIRE(bundle.filament_presets.size() == 4); CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "My PLA"); - CHECK(bundle.filament_presets[3] == filler); - // The project-level per-slot vectors were grown and seeded like "Add one filament": - // fillers take their preset's colour, the published slot its published colour. + // Only "My PLA" exists in the library, so the published slot keeps the aliasing and the + // colour is written onto the shared preset (every slot references it). + CHECK(bundle.filament_presets[3] == "My PLA"); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // The project-level per-slot vectors were grown and seeded: fillers take their preset's + // colour, the published slot its published colour. CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); CHECK(bundle.project_config.opt("filament_colour")->values[3] == "#ABCDEF"); @@ -1119,11 +1351,9 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published } } -// The published overlay mutates stored filament presets in place per slot, so a slot carrying -// published content must never share its stored preset with another slot: its colour/keys -// would leak into the sibling slot - and, with "repeat the last preset" growth, into the -// receiver's own first slot. Regression for the slot-aliasing hazard. -TEST_CASE("Published 3MF gives grown published slots a distinct preset so values never leak", "[Preset][Bundle][Published]") +// A published slot is seeded from an unused library preset and the values are written onto it +// in place, so the receiver's own material (slot 0) is never overwritten. +TEST_CASE("Published 3MF seeds published slots from unused presets and mutates them in place", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -1137,10 +1367,9 @@ TEST_CASE("Published 3MF gives grown published slots a distinct preset so values return config; }; - // The receiver has one slot of its own material plus one more preset in the library; the - // author publishes only slot 4 (Red). With naive repeat-last growth the new slot would - // reference the receiver's own preset and the published red would recolor it; the grown - // slot must point at a distinct preset. + // Receiver with its own material plus one more library preset; author publishes only slot 4 + // (Red). The grown slot is seeded from the unused library preset, so the published red + // recolors that preset in place and never the receiver's own material. PresetBundle bundle; Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); mine.config.opt_string("filament_type", 0u) = "PLA"; @@ -1165,9 +1394,9 @@ TEST_CASE("Published 3MF gives grown published slots a distinct preset so values // Unpublished filler slots repeat the receiver's last preset ("Add one filament"). CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "My PLA"); - // The published slot references the unused library preset, not the receiver's own... + // The published slot was seeded from the unused library preset; the published colour was + // written onto it in place, never onto the receiver's own material. CHECK(bundle.filament_presets[3] == "Other PLA"); - // ...so the published colour landed there and never recoloured the receiver's material. CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); // The project-level colours are sized and seeded for every grown slot. @@ -1180,9 +1409,9 @@ TEST_CASE("Published 3MF gives grown published slots a distinct preset so values } // 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. +// selection time. Since the overlay mutates the collection presets in place, the load must +// re-select the first slot's filament so the applied values - and slot replacements - 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 = [] { @@ -1226,8 +1455,11 @@ TEST_CASE("Published 3MF refreshes the edited preset so the applied material val bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); const Preset &edited = bundle.filaments.get_edited_preset(); + // The slot's preset was mutated in place; the edited preset displays the applied values. + CHECK(edited.name == "My PLA"); CHECK(edited.config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(pub.skipped_keys.empty()); } @@ -1261,203 +1493,15 @@ TEST_CASE("Published 3MF refreshes the edited preset so the applied material val } } -// 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. -TEST_CASE("Published 3MF reports material keys with no unique receiver match as skipped", "[Preset][Bundle][Published]") -{ - auto make_file_config = [] { - DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - // Three author slots: PLA, PETG, ABS. - config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; - config.opt("filament_self_index")->values = { 1, 2, 3 }; - config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; - config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF" }; - config.opt("filament_type")->values = { "PLA", "PETG", "ABS" }; - config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; - config.opt("filament_ids")->values = { "GFL99", "GFT99", "GFA99" }; - config.option("filament_retraction_length", true)->values = { 0.9, 1.2, 1.5 }; - return config; - }; - - PresetBundle bundle; - // Two receiver presets of the SAME type with no filament_id: the type fallback is ambiguous. - Preset &pla_a = add_inmemory_preset(bundle.filaments, "My PLA A"); - pla_a.config.opt_string("filament_type", 0u) = "PLA"; - pla_a.config.opt_string("filament_vendor", 0u) = "Generic"; - pla_a.config.opt("filament_retraction_length", true)->values = { 0.5 }; - Preset &pla_b = add_inmemory_preset(bundle.filaments, "My PLA B"); - pla_b.config.opt_string("filament_type", 0u) = "PLA"; - pla_b.config.opt_string("filament_vendor", 0u) = "Generic"; - pla_b.config.opt("filament_retraction_length", true)->values = { 0.5 }; - // A unique PETG receiver preset: the single type fallback is unambiguous. - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt_string("filament_vendor", 0u) = "Generic"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - bundle.filament_presets = { "My PLA A", "My PLA B", "My PETG" }; - - auto make_entry = [](const std::string &type, const std::string &key) { - PublishedMaterialEntry entry; - entry.filament_type = type; - entry.filament_vendor = "Generic"; - entry.keys = { key }; - return entry; - }; - - PublishedMaterialEntry pla_entry = make_entry("PLA", "filament_retraction_length"); - pla_entry.slot = 0; // the author's PLA slot - PublishedMaterialEntry petg_entry = make_entry("PETG", "filament_retraction_length"); - petg_entry.slot = 1; // the author's PETG slot - PublishedMaterialEntry abs_entry = make_entry("ABS", "filament_retraction_length"); // author slot exists, no receiver match - abs_entry.slot = 2; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { pla_entry, petg_entry, abs_entry }; - DynamicPrintConfig config = make_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - // Ambiguous type fallback: neither PLA preset is touched, reported as skipped. - CHECK(bundle.filaments.find_preset("My PLA A")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(bundle.filaments.find_preset("My PLA B")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(contains_key(pub.skipped_keys, "material:PLA (filament_retraction_length)")); - // Unambiguous single fallback: applied. - CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); - CHECK_FALSE(contains_key(pub.skipped_keys, "material:PETG (filament_retraction_length)")); - // Receiver-side miss: the author slot exists but no receiver preset matches. - CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)")); -} - -// A slotted material entry carries the author's per-slot overrides: on load it applies to the -// receiver's matching preset at the author's slot ordinal (first matching author slot -> first -// matching receiver preset, second -> second, ...). Legacy entries without a slot keep applying -// to every matching receiver preset. -TEST_CASE("Published material keys apply to the receiver's matching filament preset by author slot ordinal", "[Preset][Bundle][Published]") -{ - auto make_file_config = [] { - DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - // Three author slots of the SAME material (PETG) with distinct per-slot retraction. - config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; - config.opt("filament_self_index")->values = { 1, 2, 3 }; - config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; - config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF" }; - config.opt("filament_type")->values = { "PETG", "PETG", "PETG" }; - config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; - config.opt("filament_ids")->values = { "GFT99", "GFT99", "GFT99" }; - config.option("filament_retraction_length", true)->values = { 0.7, 0.8, 0.9 }; - return config; - }; - auto make_slotted_entry = [](int slot) { - PublishedMaterialEntry entry; - entry.filament_type = "PETG"; - entry.filament_vendor = "Generic"; - entry.filament_id = "GFT99"; - entry.slot = slot; - entry.keys = { "filament_retraction_length" }; - return entry; - }; - auto add_petg_preset = [](PresetBundle &bundle, const std::string &name) { - Preset &preset = add_inmemory_preset(bundle.filaments, name); - preset.filament_id = "GFT99"; - preset.config.opt_string("filament_type", 0u) = "PETG"; - preset.config.opt_string("filament_vendor", 0u) = "Generic"; - preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; - return &preset; - }; - - // Three receiver presets, one per author slot: each gets its ordinal's value. - { - PresetBundle bundle; - add_petg_preset(bundle, "My PETG 1"); - add_petg_preset(bundle, "My PETG 2"); - add_petg_preset(bundle, "My PETG 3"); - bundle.filament_presets = { "My PETG 1", "My PETG 2", "My PETG 3" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { make_slotted_entry(0), make_slotted_entry(1), make_slotted_entry(2) }; - DynamicPrintConfig config = make_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - // Each author slot's value lands in the receiver preset at the same ordinal. - CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); - CHECK(bundle.filaments.find_preset("My PETG 2")->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); - CHECK(bundle.filaments.find_preset("My PETG 3")->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(pub.skipped_keys.empty()); - } - - // A single receiver preset: only the first ordinal fits; the later slots are reported - // with a slot-qualified label. - { - PresetBundle bundle; - add_petg_preset(bundle, "My PETG 1"); - bundle.filament_presets = { "My PETG 1" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { make_slotted_entry(0), make_slotted_entry(1), make_slotted_entry(2) }; - DynamicPrintConfig config = make_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); - CHECK(contains_key(pub.skipped_keys, "material:GFT99 slot 1 (filament_retraction_length)")); - CHECK(contains_key(pub.skipped_keys, "material:GFT99 slot 2 (filament_retraction_length)")); - CHECK_FALSE(contains_key(pub.skipped_keys, "material:GFT99 slot 0 (filament_retraction_length)")); - } - - // A legacy entry (no slot) applies to every matching receiver preset, from the first - // author slot. - { - PresetBundle bundle; - add_petg_preset(bundle, "My PETG 1"); - add_petg_preset(bundle, "My PETG 2"); - bundle.filament_presets = { "My PETG 1", "My PETG 2" }; - - PublishedMaterialEntry legacy = make_slotted_entry(0); - legacy.slot = -1; // legacy: no slot field - PublishedConfig pub; - pub.published = true; - pub.material_keys = { legacy }; - DynamicPrintConfig config = make_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); - CHECK(bundle.filaments.find_preset("My PETG 2")->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); - CHECK(pub.skipped_keys.empty()); - } - - // An out-of-range author slot is silently skipped: nothing applied, nothing reported. - { - PresetBundle bundle; - add_petg_preset(bundle, "My PETG 1"); - bundle.filament_presets = { "My PETG 1" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { make_slotted_entry(5) }; - DynamicPrintConfig config = make_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filaments.find_preset("My PETG 1")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(pub.skipped_keys.empty()); - } -} - -// The published overlay must validate '#' variant indices: an out-of-range index must be -// reported as skipped and must NOT resize/corrupt the receiver's vector, and a variant suffix -// on a scalar key must be rejected instead of silently no-op'd. +// The published overlay must validate '#' variant indices: an out-of-range index is reported as +// skipped and must NOT resize/corrupt the receiver's vector, and a variant suffix on a scalar +// key is rejected instead of silently no-op'd. TEST_CASE("Published 3MF rejects out-of-range vector variants and variant-suffixed scalar keys", "[Preset][Bundle][Published]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.opt("filament_colour")->values = { "#FF0000" }; - // Vector key, size 2 (matches the receiver's resized vector); distinct values so the - // applied element is observable. + // Vector key, size 2 (matches the receiver's resized vector); distinct values make the + // applied element observable. config.opt("wiping_volumes_extruders")->values = { 140., 150. }; config.opt_float("layer_height") = 0.28; Preset::normalize(config); @@ -1482,9 +1526,10 @@ TEST_CASE("Published 3MF rejects out-of-range vector variants and variant-suffix CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.1, 0.000001)); } -// A receiver filament preset whose material identity fields are missing (hand-edited preset -// file) must not crash the material pass: the entry simply cannot match and is reported skipped. -TEST_CASE("Published 3MF survives a receiver filament preset missing its material identity", "[Preset][Bundle][Published]") +// A receiver filament preset missing its material identity (hand-edited file) must not crash +// the type gate: the gate reads it as a type mismatch, and the slot falls back to the +// "no replacement" path (keys skipped, colour still applied to the slot's preset). +TEST_CASE("Published 3MF survives a receiver preset missing its material identity", "[Preset][Bundle][Published]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.opt("filament_diameter")->values = { 1.75 }; @@ -1502,28 +1547,40 @@ TEST_CASE("Published 3MF survives a receiver filament preset missing its materia // Malformed receiver preset: the identity options are missing entirely. pla.config.erase("filament_type"); pla.config.erase("filament_vendor"); + pla.config.opt("filament_colour", true)->values = { "#123456" }; pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; bundle.filament_presets = { "My PLA" }; PublishedMaterialEntry entry; - entry.filament_type = "PLA"; - entry.filament_vendor = "Generic"; - entry.filament_id = "GFL99"; - entry.slot = 0; - entry.keys = { "filament_retraction_length" }; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.filament_id = "GFL99"; + entry.slot = 0; + entry.publish_type = true; // exercises the type gate against the missing identity + // Require ABS: the receiver library (PLA-typed default preset, typeless slot preset) has no + // ABS candidate, so the gate falls into the "no replacement" path (a PLA requirement would + // legitimately replace the slot with the visible PLA default). + entry.publish_type_value = "ABS"; + entry.publish_color = true; + entry.color = "#ABCDEF"; + entry.keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; pub.material_keys = { entry }; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // No match possible without the identity fields: reported skipped, preset untouched. + // The gate reads the missing identity as a type mismatch; no same-type replacement exists, + // so the keys are skipped while the colour applies to the slot's preset in place. No crash. CHECK(contains_key(pub.skipped_keys, "material:GFL99 (filament_retraction_length)")); - CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filament_presets[0] == "My PLA"); + Preset *mine_preset = bundle.filaments.find_preset("My PLA", false, true); + REQUIRE(mine_preset != nullptr); + CHECK(mine_preset->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); } -// The printer publishable allowlist is the union of the printer tab's "Retraction" and -// "Z-Hop" optgroup option lists; lock the exact contents and order (Tab.cpp). +// Lock the exact contents and order of the printer allowlist (the union of the tab's +// "Retraction" and "Z-Hop" optgroup lists, Tab.cpp). TEST_CASE("Printer publishable allowlist matches the printer tab's Retraction and Z-Hop optgroups", "[Preset][Bundle][Published]") { auto keys_of = [](const std::vector& opts) { @@ -1552,3 +1609,166 @@ TEST_CASE("Printer publishable allowlist matches the printer tab's Retraction an CHECK(publishable_printer_keys() == expected_union); } +// Loading the same published file twice must not compound values on the receiver's presets: +// each load re-applies the same absolute values, so the result is idempotent. +TEST_CASE("Published 3MF reloading does not compound values on the receiver's presets", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + auto make_entry = [] { + PublishedMaterialEntry entry; + entry.slot = 0; + entry.publish_color = true; + entry.color = "#ABCDEF"; + entry.keys = { "filament_retraction_length" }; + return entry; + }; + + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt("filament_colour", true)->values = { "#123456" }; + pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + bundle.filament_presets = { "My PLA" }; + + auto load = [&] { + 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); + }; + load(); + // First load: the receiver's preset carries the published values (mutated in place). + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + load(); + // Each load re-applies the same values onto the (already mutated) preset: no accumulation. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); +} + +// A receiver with several slots aliasing the same preset (multi-extruder profile with one +// filament) and an author publishing keys on several slots: each published slot is re-pointed +// at its own distinct preset so values never leak between slots. +TEST_CASE("Published 3MF gives each published slot its own preset on an aliased receiver", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.6, 0.9, 1.2, 1.5 }; + return config; + }; + auto make_key_entry = [](int slot) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.keys = { "filament_retraction_length" }; + return entry; + }; + + // A 4-extruder receiver with a single filament preset: the slots alias [A, A, A, A] before + // the published pass. + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + mine.config.opt("filament_retraction_length", true)->values = { 0.5 }; + // Spare library presets for the re-pointing to fall back on. + for (const char *name : { "Extra PLA A", "Extra PLA B", "Extra PLA C" }) { + Preset &extra = add_inmemory_preset(bundle.filaments, name); + extra.config.opt_string("filament_type", 0u) = "PLA"; + extra.config.opt("filament_retraction_length", true)->values = { 0.5 }; + } + bundle.filament_presets = { "My PLA", "My PLA", "My PLA", "My PLA" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_key_entry(0), make_key_entry(1), make_key_entry(2), make_key_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); + // Every published slot references its own distinct preset: slot 0 keeps the receiver's + // material, slots 1-3 are re-pointed at the spare library presets. + CHECK(bundle.filament_presets[0] == "My PLA"); + CHECK(bundle.filament_presets[1] != bundle.filament_presets[0]); + CHECK(bundle.filament_presets[2] != bundle.filament_presets[0]); + CHECK(bundle.filament_presets[2] != bundle.filament_presets[1]); + CHECK(bundle.filament_presets[3] != bundle.filament_presets[0]); + CHECK(bundle.filament_presets[3] != bundle.filament_presets[1]); + CHECK(bundle.filament_presets[3] != bundle.filament_presets[2]); + // Each slot's stored preset carries its own slot's retraction (mutated in place). + const std::vector expected = { 0.6, 0.9, 1.2, 1.5 }; + for (size_t slot = 0; slot < 4; ++slot) { + Preset *preset = bundle.filaments.find_preset(bundle.filament_presets[slot], false, true); + REQUIRE(preset != nullptr); + CHECK(preset->config.opt("filament_retraction_length")->values == std::vector{ expected[slot] }); + } + CHECK(pub.skipped_keys.empty()); +} + +// Printer retraction keys are published per-extruder ("#N"): a receiver with a different +// extruder count still receives the in-range elements; out-of-range variants are reported as +// skipped instead of corrupting the receiver's vector. +TEST_CASE("Published 3MF applies per-extruder printer keys across extruder-count mismatches", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + // Author has 4 extruders. + config.opt("retraction_length")->values = { 0.6, 0.9, 1.2, 1.5 }; + Preset::normalize(config); + return config; + }; + + // Receiver with a single extruder: only "#0" is in range; "#1..#3" are skipped. + { + PresetBundle bundle; + bundle.printers.get_edited_preset().config.opt("retraction_length")->values = { 0.8 }; + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "retraction_length#0", "retraction_length#1", "retraction_length#2", "retraction_length#3" }; + DynamicPrintConfig config = make_file_config(); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 0.6 }); + CHECK(contains_key(pub.skipped_keys, "retraction_length#1")); + CHECK(contains_key(pub.skipped_keys, "retraction_length#2")); + CHECK(contains_key(pub.skipped_keys, "retraction_length#3")); + CHECK_FALSE(contains_key(pub.skipped_keys, "retraction_length#0")); + } + + // Receiver with four extruders and a 1-extruder author: only "#0" is published; the + // receiver's other extruders keep their own values. + { + PresetBundle bundle; + bundle.printers.get_edited_preset().config.opt("retraction_length")->values = { 0.8, 0.8, 0.8, 0.8 }; + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "retraction_length#0" }; + DynamicPrintConfig config = make_file_config(); + // The author's file carries a single-extruder value. + config.opt("retraction_length")->values = { 0.7 }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 0.7, 0.8, 0.8, 0.8 }); + CHECK(pub.skipped_keys.empty()); + } +} + From aeaa3c5d66c78bfc3c9d3ca956cabea453383626 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 19 Aug 2026 16:57:45 +0800 Subject: [PATCH 12/60] Bug fix for perfect name matching --- src/libslic3r/PresetBundle.cpp | 38 +++- src/libslic3r/PublishSettings.hpp | 4 + src/slic3r/GUI/Plater.cpp | 4 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 9 +- tests/libslic3r/test_3mf.cpp | 4 +- .../libslic3r/test_preset_bundle_loading.cpp | 199 ++++++++++++++++++ 6 files changed, 242 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 7a90036fe2..bc6df4c3a9 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4894,11 +4894,19 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Mirror first_visible_idx()'s start index so suppressed default presets are // never picked as a slot material. const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; - // Candidate preference for a published entry: exact setting_id (variant-level, - // since "Generic PLA" and "Generic PLA Matte" share filament_id), then exact - // filament_id, then vendor+type, then type only (a type-only pick may surface an - // unrelated preset, e.g. a different vendor's PLA). + // Candidate preference for a published entry: exact preset name (unambiguous + // even when ids are shared between variants or missing from older files), then + // exact setting_id (variant-level, since "Generic PLA" and "Generic PLA Matte" + // share filament_id), then exact filament_id, then vendor+type, then type only + // (a type-only pick may surface an unrelated preset, e.g. a different vendor's + // PLA). auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry) -> int { + if (!entry.preset_name.empty()) { + const std::string bare_name = entry.preset_name.substr(0, entry.preset_name.find('@')); + if (candidate.name == entry.preset_name || candidate.alias == entry.preset_name || + candidate.name == bare_name || candidate.alias == bare_name) + return 4; + } if (!entry.setting_id.empty() && candidate.setting_id == entry.setting_id) return 3; const ConfigOptionStrings *types = candidate.config.opt("filament_type"); @@ -4926,10 +4934,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool int best_score = -1; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) - continue; const int score = candidate_score(candidate, entry); if (score > best_score) { + // Exact identity tiers (name / setting_id) win even when the + // preset is hidden or already referenced by another slot; the + // alias re-pointing pass below still de-aliases afterwards. + if (score < 3 && (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)) + continue; best_score = score; initial_preset = candidate.name; } @@ -4979,9 +4990,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool continue; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1))) - continue; const int score = candidate_score(candidate, entry); + // Exact identity tiers (name / setting_id) may use a hidden preset; + // the referenced check stays: re-pointing exists to de-alias. + if (score < 3 && !candidate.is_visible) + continue; + if (referenced_elsewhere(candidate.name, size_t(-1))) + continue; if (score > best_score) { best_score = score; replacement = candidate.name; @@ -5106,11 +5121,14 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::string best_name; for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) - continue; const int score = candidate_score(candidate, entry); if (score <= best_score) continue; + // Lower tiers (vendor+type, type only) need a visible preset; + // an exact identity match (name / setting_id) wins even when + // the preset is hidden in the library. + if (score < 3 && !candidate.is_visible) + continue; if (unreferenced_only) { bool used = false; for (size_t s = 0; s < this->filament_presets.size(); ++s) diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index ecb51523f5..0dc579c2a9 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -40,6 +40,10 @@ struct PublishedMaterialEntry { // used on load to match the exact published variant, which filament_id alone cannot // distinguish ("Generic PLA" and "Generic PLA Matte" share their inherited id). std::string setting_id; + // Canonical name of the author's slot preset (e.g. "Generic PLA @System"). The receiver + // prefers an exact name/alias match over id matching: ids can be shared across variants + // or missing from older files, the name is what the author actually selected. + std::string preset_name; // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector keys; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index be56cc1f66..50d613de33 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7252,6 +7252,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ entry.filament_id = mat["filament_id"].get(); if (mat.contains("setting_id") && mat["setting_id"].is_string()) entry.setting_id = mat["setting_id"].get(); + if (mat.contains("name") && mat["name"].is_string()) + entry.preset_name = mat["name"].get(); } if (m.contains("slot") && m["slot"].is_number_integer()) entry.slot = m["slot"].get(); @@ -16251,7 +16253,7 @@ int Plater::export_published_3mf(const std::vector& 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}, {"setting_id", e.setting_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}, {"setting_id", e.setting_id}, {"name", e.preset_name}}}, {"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} }); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index f62d7af3a4..881560f93c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -924,11 +924,14 @@ std::vector PublishSettingsDialog::GetPublishedM entry.filament_id = cat.filament_id; entry.slot = static_cast(cat.filament_slot); // The author's preset id distinguishes exact variants that share filament_id - // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely. + // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely; the + // preset name is the most direct identity and is matched first on load. PresetBundle *bundle = wxGetApp().preset_bundle; if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) { - if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) - entry.setting_id = preset->setting_id; + if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) { + entry.setting_id = preset->setting_id; + entry.preset_name = preset->name; + } } // "Full Publish": the whole filament preset is embedded; type and colour are implicitly // published, and the per-key rows are disabled / their state ignored. diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 34ff31183f..818c354628 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -684,7 +684,7 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded const std::vector published_keys = { "layer_height", "retraction_length" }; const std::vector material_keys = { - { "PLA", "Generic", "GFL99", "", 0, { "filament_retraction_length" } } + { "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } } }; DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); @@ -792,7 +792,7 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { model.add_default_instances(); const std::string material_keys_json = - R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf"},"slot":1,"keys":[],"full":true,"full_keys":["filament_retraction_length","filament_colour"],"publish_type":true,"type":"PLA","publish_color":false,"color":""}])"; + R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf","name":"Generic PLA Matte @System"},"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(); model.model_info->metadata_items["published_material_keys"] = material_keys_json; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 3a9ff77be9..11162922fc 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -942,6 +942,205 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact published mat CHECK(pub.skipped_keys.empty()); } +// The author's preset name travels in the file, so a type mismatch resolves to the exact +// published material even when the identity fields are absent or stale (older files): a +// "Generic PLA" author must land on the receiver's "Generic PLA", never on a same-type +// substitute like "Bambu PLA Basic". +TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by name", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "OGFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9 }; + return config; + }; + + auto add_pla = [](PresetBundle &bundle, const char *name, const char *id, const char *vendor, const char *setting_id) { + Preset &preset = add_inmemory_preset(bundle.filaments, name); + preset.filament_id = id; + preset.setting_id = setting_id; + preset.config.opt_string("filament_type", 0u) = "PLA"; + preset.config.opt_string("filament_vendor", 0u) = vendor; + preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; + return &preset; + }; + + SECTION("full identity (name, setting_id, filament_id): the name match wins") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "OGFL99"; + entry.filament_vendor = "Generic"; + entry.setting_id = "RcBNzytWgwRrwXXz"; + entry.preset_name = "Generic PLA @System"; + entry.full_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); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("only the name is present (broken/stale ids): still the exact preset") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_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); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(bundle.filaments.find_preset("Bambu PLA Basic @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("the exact preset is hidden in the library: the exact match still wins") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + Preset &generic_pla = *add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + generic_pla.is_visible = false; + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_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); + + CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("grown slot (author slot 1) is seeded with the exact preset by name") { + auto two_slot_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "OGFL99", "OGFL99" }; + config.option("filament_retraction_length", true)->values = { 0.9, 0.8 }; + return config; + }; + + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 1; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = two_slot_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 2); + CHECK(bundle.filament_presets[0] == "My PETG"); + CHECK(bundle.filament_presets[1] == "Generic PLA @System"); + // The full dump applies the author's slot-1 value onto the grown slot's preset. + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); + CHECK(pub.skipped_keys.empty()); + } + + SECTION("no exact preset in the library: falls back to the substitute") { + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @System"; + entry.filament_id = "OGFL99"; + entry.full_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); + + CHECK(bundle.filament_presets[0] == "Bambu PLA Basic @System"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Bambu PLA Basic @System (substitute: no exact material match)"); + } +} + // The replacement search prefers the published identity: exact filament_id, then vendor+type, // then type only (collection order decides equal scores; the pick is reported as a substitute // when it is not the exact published material). From aa3ce35683c2fdddc55df3d5db80f0def3a42122 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 21 Aug 2026 13:00:32 +0800 Subject: [PATCH 13/60] Published 3MF: silent geometry-only fallback in old versions via tag/config-less export --- src/libslic3r/Format/bbs_3mf.cpp | 48 ++++++-- src/slic3r/GUI/Plater.cpp | 205 ++++++++++++++++++------------- tests/libslic3r/test_3mf.cpp | 51 ++++++-- 3 files changed, 202 insertions(+), 102 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index a5d20807b7..06cc89de99 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -1214,6 +1214,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // add backup & restore logic bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr, BBLProject* project = nullptr, int plate_id = 0); + + // A minimal published 3MF carries no slicer tags (any tag would make old receivers show + // a baked-in, wrong "old version" popup on their geometry-only fallback), so it + // classifies as From_Other. It is still a fully structured OrcaSlicer file though: + // identified by its own metadata, it keeps BBS-grade geometry handling (no instance + // splitting, no transform baking, no renaming) in this build. Old receivers without the + // publish feature don't know the metadata and take their third-party geometry path. + // Reads the parse-time metadata: the model XML carries it before its resources, while + // m_model->model_info is only filled in after the whole XML has been parsed. + bool _is_published_3mf() const { + return this->model_info.metadata_items.find("published") != this->model_info.metadata_items.end(); + } + bool _is_svg_shape_file(const std::string &filename) const; bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function, bool restore = false); bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler); @@ -2037,7 +2050,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) lock.close(); - if (!m_is_bbl_3mf) { + if (!m_is_bbl_3mf && !_is_published_3mf()) { // if the 3mf was not produced by OrcaSlicer and there is more than one instance, // split the object in as many objects as instances BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance"); @@ -3591,7 +3604,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_index_paths.insert({ object.first.second, object.first.first}); } - if (!m_is_bbl_3mf) { + if (!m_is_bbl_3mf && !_is_published_3mf()) { // if the 3mf was not produced by OrcaSlicer and there is only one object, // set the object name to match the filename if (m_model->objects.size() == 1) @@ -5297,7 +5310,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats); - if (!m_is_bbl_3mf) { + if (!m_is_bbl_3mf && !_is_published_3mf()) { // if the 3mf was not produced by OrcaSlicer and there is only one instance, // bake the transformation into the geometry to allow the reload from disk command // to work properly @@ -5943,7 +5956,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) bool m_save_gcode { false }; // whether to save gcode for normal save bool m_skip_model { false }; // skip model when exporting .gcode.3mf bool m_skip_auxiliary { false }; // skip normal axuiliary files - bool m_minimal_published { false }; // published 3MF: omit embedded preset files + bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags bool m_use_loaded_id { false }; // whether to use loaded id for identify_id bool m_share_mesh { false }; // whether to share mesh between objects std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE; @@ -6453,7 +6466,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Adds slic3r print config file ("Metadata/Slic3r_PE.config"). // This file contains the content of FullPrintConfig / SLAFullPrintConfig. - if (config != nullptr) { + // Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature + // then fall back to importing the geometry only, and new versions read the published + // payload from the model metadata instead. + if (config != nullptr && !m_minimal_published) { // BBS: change to json format // if (!_add_print_config_file_to_archive(archive, *config)) { if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; } @@ -6939,8 +6955,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Orca: PRIVACY: do not store creation & modification date in 3mf metadata_item_map[BBL_CREATION_DATE_TAG] = ""; metadata_item_map[BBL_MODIFICATION_TAG] = ""; - // Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION - metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str(); + // Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION. + // A minimal published 3MF writes no slicer tags at all: any tag would route old + // receivers onto a geometry-only fallback whose baked-in popup misreports the + // file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify + // as From_Other and import the geometry silently. + if (!m_minimal_published) + metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str(); } metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF); @@ -6966,8 +6987,17 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">" << xml_escape(item.second) << "\n"; if (item.first == BBL_APPLICATION_TAG) { - stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" - << xml_escape(SoftFever_VERSION) << "\n"; + // The OrcaSlicer tag is the version receivers compare against their own to + // pick the import branch, and every graceful config-less branch of an + // Orca-classified file shows a baked-in "old OrcaSlicer version" popup. A + // minimal published 3MF omits the tag (together with the Application tag + // above): old receivers then classify it From_Other and import the geometry + // silently, while this build rebuilds the config from the published metadata + // payload before the branch runs. + if (!m_minimal_published) { + stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" + << xml_escape(SoftFever_VERSION) << "\n"; + } } } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 50d613de33..908e4dc222 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6881,6 +6881,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicPrintConfig config; Semver file_version; En3mfType en_3mf_file_type = En3mfType::From_BBS; + // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; + // on load keep the user's current presets and overlay only those keys. Declared + // here (outside the config block below) so it stays alive for the embedded-preset + // gate, the metadata strip and the preset overlay after the block closes. + PublishedConfig published_config; { DynamicPrintConfig config_loaded; @@ -6908,6 +6913,102 @@ std::vector Plater::priv::load_files(const std::vector& input_ << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() % project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string(); + // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; + // on load keep the user's current presets and overlay only those keys. Parsed + // here (before the version/fallback chain below) because a published file has + // no project_settings.config: its values travel in the published_config + // metadata payload, which must fill config_loaded before the chain decides + // whether to import geometry only. + if (model.model_info != nullptr) { + auto published_it = model.model_info->metadata_items.find("published"); + if (published_it != model.model_info->metadata_items.end() && + (published_it->second == "true" || published_it->second == "1")) { + published_config.published = true; + auto keys_it = model.model_info->metadata_items.find("published_keys"); + if (keys_it != model.model_info->metadata_items.end()) { + try { + auto j = nlohmann::json::parse(keys_it->second); + if (j.is_array()) + for (const auto &k : j) + if (k.is_string()) + published_config.published_keys.emplace_back(k.get()); + } catch (...) { + // Ignore malformed published_keys; the project still loads normally. + } + } + + auto material_keys_it = model.model_info->metadata_items.find("published_material_keys"); + if (material_keys_it != model.model_info->metadata_items.end()) { + try { + auto jm = nlohmann::json::parse(material_keys_it->second); + if (jm.is_array()) + for (const auto &m : jm) { + // Malformed entries are skipped individually. + if (!m.is_object()) + continue; + PublishedMaterialEntry entry; + const auto mat_it = m.find("material"); + if (mat_it != m.end() && mat_it->is_object()) { + const auto &mat = *mat_it; + if (mat.contains("filament_type") && mat["filament_type"].is_string()) + entry.filament_type = mat["filament_type"].get(); + if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) + entry.filament_vendor = mat["filament_vendor"].get(); + if (mat.contains("filament_id") && mat["filament_id"].is_string()) + entry.filament_id = mat["filament_id"].get(); + if (mat.contains("setting_id") && mat["setting_id"].is_string()) + entry.setting_id = mat["setting_id"].get(); + if (mat.contains("name") && mat["name"].is_string()) + entry.preset_name = mat["name"].get(); + } + if (m.contains("slot") && m["slot"].is_number_integer()) + entry.slot = m["slot"].get(); + const auto entry_keys_it = m.find("keys"); + if (entry_keys_it != m.end() && entry_keys_it->is_array()) + for (const auto &k : *entry_keys_it) + if (k.is_string()) + entry.keys.emplace_back(k.get()); + // Fields always written by the current exporter. + if (m.contains("full") && m["full"].is_boolean()) + entry.full = m["full"].get(); + 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()); + if (m.contains("publish_type") && m["publish_type"].is_boolean()) + entry.publish_type = m["publish_type"].get(); + if (m.contains("type") && m["type"].is_string()) + entry.publish_type_value = m["type"].get(); + if (m.contains("publish_color") && m["publish_color"].is_boolean()) + entry.publish_color = m["publish_color"].get(); + if (m.contains("color") && m["color"].is_string()) + entry.color = m["color"].get(); + published_config.material_keys.emplace_back(std::move(entry)); + } + } catch (...) { + // Ignore malformed published_material_keys; the project still loads normally. + } + } + + // Rebuild the published values from the metadata payload: a published + // file carries no project_settings.config, so config_loaded is filled + // from here; a missing or malformed payload leaves it empty and the + // fallback chain below imports the geometry only. + auto payload_it = model.model_info->metadata_items.find("published_config"); + if (payload_it != model.model_info->metadata_items.end()) { + try { + ConfigSubstitutions payload_substitutions = config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable); + config_substitutions.substitutions.insert(config_substitutions.substitutions.end(), + std::make_move_iterator(payload_substitutions.begin()), + std::make_move_iterator(payload_substitutions.end())); + } catch (...) { + // Ignore malformed published_config; the project still loads normally. + } + } + } + } + // 1. add extruder for prusa model if the number of existing extruders is not enough // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { @@ -7071,7 +7172,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ text += "\n"; log_and_show_3mf_info(text, bambu_project_title); } - } else if (load_config) { + } else if (load_config && !published_config.published) { // BambuStudio version is older or same as our SLIC3R_VERSION wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer."); log_and_show_3mf_info(text, bambu_project_title); @@ -7153,8 +7254,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - // BBS:: project embedded presets - if ((project_presets.size() > 0) && load_config) { + // BBS:: project embedded presets (skipped for published projects: the author's + // embedded presets must not pollute the receiver's library, the overlay applies + // the published keys to the receiver's own presets instead). + if ((project_presets.size() > 0) && load_config && !published_config.published) { // load project embedded presets PresetsConfigSubstitutions preset_substitutions; PresetBundle & preset_bundle = *wxGetApp().preset_bundle; @@ -7210,83 +7313,6 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; - // on load keep the user's current presets and overlay only those keys. - PublishedConfig published_config; - if (model.model_info != nullptr) { - auto published_it = model.model_info->metadata_items.find("published"); - if (published_it != model.model_info->metadata_items.end() && - (published_it->second == "true" || published_it->second == "1")) { - published_config.published = true; - auto keys_it = model.model_info->metadata_items.find("published_keys"); - if (keys_it != model.model_info->metadata_items.end()) { - try { - auto j = nlohmann::json::parse(keys_it->second); - if (j.is_array()) - for (const auto &k : j) - if (k.is_string()) - published_config.published_keys.emplace_back(k.get()); - } catch (...) { - // Ignore malformed published_keys; the project still loads normally. - } - } - - auto material_keys_it = model.model_info->metadata_items.find("published_material_keys"); - if (material_keys_it != model.model_info->metadata_items.end()) { - try { - auto jm = nlohmann::json::parse(material_keys_it->second); - if (jm.is_array()) - for (const auto &m : jm) { - // Malformed entries are skipped individually. - if (!m.is_object()) - continue; - PublishedMaterialEntry entry; - const auto mat_it = m.find("material"); - if (mat_it != m.end() && mat_it->is_object()) { - const auto &mat = *mat_it; - if (mat.contains("filament_type") && mat["filament_type"].is_string()) - entry.filament_type = mat["filament_type"].get(); - if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) - entry.filament_vendor = mat["filament_vendor"].get(); - if (mat.contains("filament_id") && mat["filament_id"].is_string()) - entry.filament_id = mat["filament_id"].get(); - if (mat.contains("setting_id") && mat["setting_id"].is_string()) - entry.setting_id = mat["setting_id"].get(); - if (mat.contains("name") && mat["name"].is_string()) - entry.preset_name = mat["name"].get(); - } - if (m.contains("slot") && m["slot"].is_number_integer()) - entry.slot = m["slot"].get(); - const auto entry_keys_it = m.find("keys"); - if (entry_keys_it != m.end() && entry_keys_it->is_array()) - for (const auto &k : *entry_keys_it) - if (k.is_string()) - entry.keys.emplace_back(k.get()); - // Fields always written by the current exporter. - if (m.contains("full") && m["full"].is_boolean()) - entry.full = m["full"].get(); - 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()); - if (m.contains("publish_type") && m["publish_type"].is_boolean()) - entry.publish_type = m["publish_type"].get(); - if (m.contains("type") && m["type"].is_string()) - entry.publish_type_value = m["type"].get(); - if (m.contains("publish_color") && m["publish_color"].is_boolean()) - entry.publish_color = m["publish_color"].get(); - if (m.contains("color") && m["color"].is_string()) - entry.color = m["color"].get(); - published_config.material_keys.emplace_back(std::move(entry)); - } - } catch (...) { - // Ignore malformed published_material_keys; the project still loads normally. - } - } - } - } - // BBS: a "published" 3MF loads as a new project: its path must not become the // project filename (Save/Ctrl-S would overwrite the shared file), and the // published metadata is consumed above and stripped so a later save is a normal @@ -7297,6 +7323,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ 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"); + this->model.model_info->metadata_items.erase("published_config"); } if (load_config) { @@ -16265,9 +16292,11 @@ int Plater::export_published_3mf(const std::vector& published_keys, const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end()); const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end()); const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find("published_material_keys") != model.model_info->metadata_items.end()); + const bool had_payload = had_model_info && (model.model_info->metadata_items.find("published_config") != model.model_info->metadata_items.end()); const std::string prev_published = had_published ? model.model_info->metadata_items.at("published") : std::string(); const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at("published_keys") : std::string(); const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at("published_material_keys") : std::string(); + const std::string prev_payload = had_payload ? model.model_info->metadata_items.at("published_config") : std::string(); if (model.model_info == nullptr) model.model_info = std::make_shared(); model.model_info->metadata_items["published"] = "1"; @@ -16275,9 +16304,17 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items["published_material_keys"] = jm.dump(); // Minimal published export: filter full_config to the published keys, material keys, - // identity fields and plate geometry keys, and omit project-embedded preset dumps. + // identity fields and plate geometry keys, and omit the project config file, the + // project-embedded preset dumps and the OrcaSlicer version tag from the archive. The + // filtered values are serialized into the published_config metadata payload instead, so + // OrcaSlicer versions without the publish feature fall back to importing the geometry only + // (keeping the receiver's presets) while new versions rebuild the config from the payload. DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + std::string payload; + for (const std::string &key : filtered_cfg.keys()) + payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; + model.model_info->metadata_items["published_config"] = std::move(payload); // Same file layout as save_project(), plus Silence (so export_3mf does not set the project // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. @@ -16286,7 +16323,7 @@ int Plater::export_published_3mf(const std::vector& published_keys, if (full_pathnames) save_strategy = save_strategy | SaveStrategy::FullPathSources; - const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr, &filtered_cfg); + const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr); // Restore the previous metadata state (both on success and on failure). if (!had_model_info) { @@ -16304,6 +16341,10 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items["published_material_keys"] = prev_material_keys; else model.model_info->metadata_items.erase("published_material_keys"); + if (had_payload) + model.model_info->metadata_items["published_config"] = prev_payload; + else + model.model_info->metadata_items.erase("published_config"); } if (ret < 0) { diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 5fdad8b7ce..6a20613a79 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -671,12 +671,15 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf } } -SCENARIO("Minimal published 3MF serialization filters config and omits embedded presets", "[3mf]") { - GIVEN("a full print configuration and published keys") { +SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer tags", "[3mf]") { + GIVEN("a multi-instance model carrying published metadata and a published_config payload") { 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(); + // A second instance: tag-less third-party files get their multi-instance objects split, + // published files must not (the loader recognizes them by their metadata). + model.objects.front()->add_instance(); DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.set_key_value("layer_height", new ConfigOptionFloat(0.24)); @@ -687,27 +690,30 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded { "PLA", "Generic", "GFL99", "", "Generic PLA", 0, { "filament_retraction_length" } } }; + // The payload builder keeps the published and identity keys and drops everything else. DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); - - // Filtered config keeps the published and identity keys... REQUIRE(filtered_cfg.option("layer_height") != nullptr); REQUIRE(filtered_cfg.option("retraction_length") != nullptr); REQUIRE(filtered_cfg.option("filament_colour") != nullptr); REQUIRE(filtered_cfg.option("filament_type") != nullptr); REQUIRE(filtered_cfg.option("wipe_tower_x") != nullptr); - - // ...and drops everything else. REQUIRE(filtered_cfg.option("sparse_infill_density") == nullptr); REQUIRE(filtered_cfg.option("machine_start_gcode") == nullptr); + // Serialize the payload exactly like export_published_3mf does. + std::string payload; + for (const std::string &key : filtered_cfg.keys()) + payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; + model.model_info = std::make_shared(); model.model_info->metadata_items["published"] = "1"; model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])"; + model.model_info->metadata_items["published_config"] = payload; ScopedTemporaryDir backup_dir("orca_min_pub"); model.set_backup_path(backup_dir.string()); - WHEN("stored using SaveStrategy::MinimalPublished") { + WHEN("stored using SaveStrategy::MinimalPublished and reloaded") { ScopedTemporaryFile temp(".3mf"); const std::string test_file = temp.string(); @@ -736,12 +742,35 @@ SCENARIO("Minimal published 3MF serialization filters config and omits embedded bool loaded = load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, &loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); - THEN("the 3MF loads successfully without project embedded presets") { + THEN("the 3MF loads without project config or embedded presets") { REQUIRE(loaded); + REQUIRE(dst_config.empty()); REQUIRE(loaded_presets.empty()); - REQUIRE(dst_config.option("layer_height") != nullptr); - REQUIRE_THAT(dst_config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6)); - REQUIRE(dst_config.option("sparse_infill_density") == nullptr); + } + THEN("the file carries no slicer tags and classifies as a generic 3MF") { + REQUIRE_FALSE(is_bbl_3mf); + REQUIRE_FALSE(is_orca_3mf); + // No Application / OrcaSlicer tag: old receivers import the geometry silently + // instead of showing a baked-in, wrong "old version" popup. + REQUIRE_FALSE(file_version.valid()); + } + THEN("the geometry keeps BBS-grade handling: instances are not split") { + REQUIRE(dst_model.objects.size() == 1); + REQUIRE(dst_model.objects.front()->instances.size() == 2); + } + THEN("the published metadata and payload round-trip unchanged") { + REQUIRE(dst_model.model_info != nullptr); + REQUIRE(dst_model.model_info->metadata_items["published"] == "1"); + REQUIRE(dst_model.model_info->metadata_items["published_keys"] == R"(["layer_height","retraction_length"])"); + REQUIRE(dst_model.model_info->metadata_items["published_config"] == payload); + } + THEN("the payload parses back to the published values") { + DynamicPrintConfig parsed_payload; + parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items["published_config"], ForwardCompatibilitySubstitutionRule::Enable); + REQUIRE(parsed_payload.option("layer_height") != nullptr); + REQUIRE_THAT(parsed_payload.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6)); + REQUIRE(parsed_payload.option("retraction_length") != nullptr); + REQUIRE_THAT(parsed_payload.opt("retraction_length")->get_at(0), Catch::Matchers::WithinAbs(1.2, 1e-6)); } release_PlateData_list(dst_plates); } From a0e95abebe6355807d9f0283f6b9e85975eddaa4 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 21 Aug 2026 13:26:17 +0800 Subject: [PATCH 14/60] Fix unit test: --- tests/libslic3r/test_preset_bundle_loading.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index df00b1a41c..d3bf4a2f49 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1216,17 +1216,20 @@ TEST_CASE("Published 3MF prefers a compatible preset over an exact but incompati petg.config.opt_string("filament_type", 0u) = "PETG"; petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; // The bare-named legacy preset (Bambu-printer only) and the exact author preset are both - // incompatible with the receiver's printer. + // incompatible with the receiver's printer. update_compatible() recomputes is_compatible + // inside load_config_model, so the incompatibility is expressed the way production derives + // it: a compatible_printers constraint no receiver printer satisfies (the fresh bundle's + // active printer is the "Default Printer" placeholder). Preset &bare = add_inmemory_preset(bundle.filaments, "Generic PLA"); bare.config.opt_string("filament_type", 0u) = "PLA"; bare.config.opt_string("filament_vendor", 0u) = "Generic"; bare.config.opt("filament_retraction_length", true)->values = { 0.5 }; - bare.is_compatible = false; + bare.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); Preset &qidi = add_inmemory_preset(bundle.filaments, "Generic PLA @Qidi Q2 0.4 nozzle"); qidi.config.opt_string("filament_type", 0u) = "PLA"; qidi.config.opt_string("filament_vendor", 0u) = "Generic"; qidi.config.opt("filament_retraction_length", true)->values = { 0.5 }; - qidi.is_compatible = false; + qidi.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); // The receiver's own compatible library preset. Preset &sys = add_inmemory_preset(bundle.filaments, "Generic PLA @System"); sys.config.opt_string("filament_type", 0u) = "PLA"; @@ -1264,11 +1267,15 @@ TEST_CASE("Published 3MF falls back to an incompatible exact preset when no comp Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); petg.config.opt_string("filament_type", 0u) = "PETG"; petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + // The exact author preset is incompatible with the receiver's printer. update_compatible() + // recomputes is_compatible inside load_config_model, so the incompatibility is expressed the + // way production derives it: a compatible_printers constraint the receiver's printer (the + // fresh bundle's "Default Printer" placeholder) does not satisfy. Preset &qidi = add_inmemory_preset(bundle.filaments, "Generic PLA @Qidi Q2 0.4 nozzle"); qidi.config.opt_string("filament_type", 0u) = "PLA"; qidi.config.opt_string("filament_vendor", 0u) = "Generic"; qidi.config.opt("filament_retraction_length", true)->values = { 0.5 }; - qidi.is_compatible = false; + qidi.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); bundle.filament_presets = { "My PETG" }; PublishedMaterialEntry entry; From a5033afaf847710c6ef49c90bd54d3ac87d5dbc6 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 21 Aug 2026 14:26:20 +0800 Subject: [PATCH 15/60] UI cleanup and translations updated --- localization/i18n/OrcaSlicer.pot | 52 +++++- localization/i18n/ca/OrcaSlicer_ca.po | 52 +++++- localization/i18n/cs/OrcaSlicer_cs.po | 54 +++++- localization/i18n/de/OrcaSlicer_de.po | 52 +++++- localization/i18n/en/OrcaSlicer_en.po | 52 +++++- localization/i18n/es/OrcaSlicer_es.po | 52 +++++- localization/i18n/eu/OrcaSlicer_eu.po | 52 +++++- localization/i18n/fr/OrcaSlicer_fr.po | 52 +++++- localization/i18n/hu/OrcaSlicer_hu.po | 54 +++++- localization/i18n/it/OrcaSlicer_it.po | 52 +++++- localization/i18n/ja/OrcaSlicer_ja.po | 54 +++++- localization/i18n/ko/OrcaSlicer_ko.po | 54 +++++- localization/i18n/lt/OrcaSlicer_lt.po | 52 +++++- localization/i18n/nl/OrcaSlicer_nl.po | 52 +++++- localization/i18n/pl/OrcaSlicer_pl.po | 54 +++++- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 52 +++++- localization/i18n/ru/OrcaSlicer_ru.po | 56 ++++++- localization/i18n/sv/OrcaSlicer_sv.po | 54 +++++- localization/i18n/th/OrcaSlicer_th.po | 52 +++++- localization/i18n/tr/OrcaSlicer_tr.po | 54 +++++- localization/i18n/uk/OrcaSlicer_uk.po | 52 +++++- localization/i18n/vi/OrcaSlicer_vi.po | 54 +++++- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 52 +++++- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 52 +++++- src/slic3r/GUI/PublishSettingsDialog.cpp | 175 +++++++++++--------- src/slic3r/GUI/PublishSettingsDialog.hpp | 40 +++-- 26 files changed, 1210 insertions(+), 273 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index 6ec0ecd9cf..c1dc798ff3 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -4565,6 +4565,9 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "" +msgid "N/A" +msgstr "" + msgid "Printing" msgstr "" @@ -5013,9 +5016,6 @@ msgstr "" msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "" -msgid "N/A" -msgstr "" - msgid "System agents" msgstr "" @@ -5945,6 +5945,12 @@ msgstr "" msgid "Save current project as" msgstr "" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "" @@ -7663,6 +7669,12 @@ msgstr "" msgid "Customized Preset" msgstr "" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "" @@ -8023,6 +8035,17 @@ msgstr "" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" @@ -9070,9 +9093,6 @@ msgstr "" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "" -msgid "Publish" -msgstr "" - msgid "Publish was canceled" msgstr "" @@ -9088,6 +9108,24 @@ msgstr "" msgid "Jump to webpage" msgstr "" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, possible-c-format, possible-boost-format msgid "Save %s as" msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index b7eb022c0d..7070c885be 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -4987,6 +4987,9 @@ msgstr "" "Sí: canviar aquesta configuració i activar el mode d'espiral automàticament\n" "No - Renunciar a utilitzar el mode espiral aquesta vegada" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Imprimint" @@ -5448,9 +5451,6 @@ msgstr "Patró no vàlid. Utilitzeu N, N#K, o una llista separada per comes amb msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Format no vàlid. Format vectorial esperat: \"%1%\"" -msgid "N/A" -msgstr "N/D" - # AI Translated msgid "System agents" msgstr "Agents del sistema" @@ -6413,6 +6413,12 @@ msgstr "Desa el projecte com a" msgid "Save current project as" msgstr "Desar el projecte actual com" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF STL/STEP/SVG/OBJ/AMF" @@ -8231,6 +8237,12 @@ msgstr "Confirmeu que els Codis-G d'aquests perfils són segurs per evitar danys msgid "Customized Preset" msgstr "Perfil personalitzat" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Els noms dels components dins del fitxer STEP no tenen format UTF8!" @@ -8619,6 +8631,17 @@ msgstr "Desa el fitxer Laminat com a:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "El fitxer %s s'ha enviat a l'emmagatzematge de la impressora i es pot visualitzar a la impressora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipus de broquet no està establert. Establiu el broquet i torneu-ho a provar." @@ -9808,9 +9831,6 @@ msgstr "Anar a la pàgina web de publicació de models" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: La preparació pot trigar uns quants minuts. Si us plau, sigui pacient." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "La publicació ha estat cancel·lada" @@ -9826,6 +9846,24 @@ msgstr "Carregant dades" msgid "Jump to webpage" msgstr "Anar a la pàgina web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Desar %s com a" diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index b521a8073b..889b659a0b 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -4945,6 +4945,10 @@ msgstr "" "Ano – změnit tato nastavení a automaticky povolit spirálový režim\n" "Ne – tentokrát nepoužít spirálový režim" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Tisk" @@ -5404,10 +5408,6 @@ msgstr "Neplatný vzor. Použijte N, N#K nebo seznam oddělený čárkami s voli msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Neplatný formát. Očekávaný vektorový formát: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Systémoví agenti" @@ -6374,6 +6374,12 @@ msgstr "Uložit projekt jako" msgid "Save current project as" msgstr "Uložit aktuální projekt jako" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importovat 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8193,6 +8199,12 @@ msgstr "Potvrďte prosím, že je G-code v těchto předvolbách bezpečný, aby msgid "Customized Preset" msgstr "Přizpůsobená předvolba" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Názvy komponent v souboru STEP nejsou ve formátu UTF-8!" @@ -8579,6 +8591,17 @@ msgstr "Uložit rozřezaný soubor jako:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Soubor %s byl odeslán do úložiště tiskárny a lze jej zobrazit na tiskárně." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publikovat" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Typ trysky není nastaven. Nastavte prosím trysku a zkuste to znovu." @@ -9756,9 +9779,6 @@ msgstr "Přejít na webovou stránku publikace modelu" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Poznámka: Příprava může trvat několik minut. Buďte prosím trpěliví." -msgid "Publish" -msgstr "Publikovat" - msgid "Publish was canceled" msgstr "Publikování bylo zrušeno" @@ -9774,6 +9794,24 @@ msgstr "Nahrávání dat" msgid "Jump to webpage" msgstr "Přejít na webovou stránku" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Uložit %s jako" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 436966457a..fe17251f32 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -4851,6 +4851,9 @@ msgstr "" "Ja - Diese Einstellungen ändern und den Spiralmodus automatisch aktivieren\n" "Nein - Spiralmodus nicht aktivieren" +msgid "N/A" +msgstr "Nicht verfügbar" + msgid "Printing" msgstr "Drucken" @@ -5310,9 +5313,6 @@ msgstr "Ungültiges Muster. Verwenden Sie N, N#K oder eine durch Kommas getrennt msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Ungültiges Format. Erwartetes Vektorformat: \"%1%\"" -msgid "N/A" -msgstr "Nicht verfügbar" - # AI Translated msgid "System agents" msgstr "Systemagenten" @@ -6266,6 +6266,12 @@ msgstr "Projekt speichern als" msgid "Save current project as" msgstr "Aktuelles Projekt speichern als" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importiere 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8063,6 +8069,12 @@ msgstr "Bitte bestätigen Sie, dass die G-Codes innerhalb dieser Profile sicher msgid "Customized Preset" msgstr "Benutzerdefinierte Profile" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Der Name der Komponenten in der Step-Datei ist nicht im UTF8-Format!" @@ -8451,6 +8463,17 @@ msgstr "Geslicte Datei speichern unter:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Die Datei %s wurde an den Speicher des Druckers gesendet und kann auf dem Drucker angezeigt werden." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Veröffentlichen" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Die Düsenart ist nicht eingestellt. Bitte stellen Sie die Düse ein und versuchen Sie es erneut." @@ -9590,9 +9613,6 @@ msgstr "Zur Modellveröffentlichungs-Webseite springen" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Hinweis: Die Vorbereitung kann einige Minuten dauern. Bitte haben Sie Geduld." -msgid "Publish" -msgstr "Veröffentlichen" - msgid "Publish was canceled" msgstr "Veröffentlichung wurde abgebrochen" @@ -9608,6 +9628,24 @@ msgstr "Daten werden hochgeladen" msgid "Jump to webpage" msgstr "Zu einer Website springen" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s speichern als" diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 88fb455959..0da5e9d2b5 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -4561,6 +4561,9 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "" +msgid "N/A" +msgstr "" + msgid "Printing" msgstr "" @@ -5009,9 +5012,6 @@ msgstr "" msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "" -msgid "N/A" -msgstr "" - msgid "System agents" msgstr "" @@ -5941,6 +5941,12 @@ msgstr "" msgid "Save current project as" msgstr "" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "" @@ -7659,6 +7665,12 @@ msgstr "" msgid "Customized Preset" msgstr "" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "" @@ -8019,6 +8031,17 @@ msgstr "" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "" @@ -9066,9 +9089,6 @@ msgstr "" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "" -msgid "Publish" -msgstr "" - msgid "Publish was canceled" msgstr "" @@ -9084,6 +9104,24 @@ msgstr "" msgid "Jump to webpage" msgstr "" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index 9c5127e50a..ded492a284 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -4723,6 +4723,9 @@ msgstr "" "Sí - Cambiar estos ajustes y activar el modo espiral automáticamente\n" "No - Dejar de usar el modo espiral esta vez" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Imprimiendo" @@ -5176,9 +5179,6 @@ msgstr "Patrón inválido. Use N, N#K, o una lista separada por comas con #K opc msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato inválido. Formato de vector esperado: \"%1%\"" -msgid "N/A" -msgstr "N/A" - msgid "System agents" msgstr "Agentes del sistema" @@ -6123,6 +6123,12 @@ msgstr "Guardar proyecto como" msgid "Save current project as" msgstr "Guardar el proyecto actual como" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7877,6 +7883,12 @@ msgstr "¡Por favor, confirme que el G-Code dentro de los perfiles son seguros p msgid "Customized Preset" msgstr "Perfil Personalizado" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "¡El nombre de los componentes dentro del archivo de pasos no tiene formato UTF8!" @@ -8254,6 +8266,17 @@ msgstr "Guardar el archivo laminado como:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "El archivo %s ha sido mandado al almacenamiento de la impresora y puede ser visualizado en la impresora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "El tipo de boquilla no está establecido. Configure la boquilla e inténtelo de nuevo." @@ -9365,9 +9388,6 @@ msgstr "Ir a la página web de publicación de modelos" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: La preparación puede llevar varios minutos. Por favor, sea paciente." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "La publicación fue cancelada" @@ -9383,6 +9403,24 @@ msgstr "Cargando datos" msgid "Jump to webpage" msgstr "Ir a la página web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Guardar %s como" diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index 03e6d6685d..e950ea3d2e 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana \n" "Language-Team: \n" @@ -4764,6 +4764,9 @@ msgstr "" "Bai - Aldatu ezarpen hauek eta gaitu espiral/loreontzi modua automatikoki\n" "Ez - Utzi bertan behera espiral modua gaitzea" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Inprimatzen" @@ -5224,9 +5227,6 @@ msgstr "Patroi baliogabea. Erabili N, N#K edo komaz bereizitako zerrenda bat, sa msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formatuak ez du balio. Espero den formatu bektoriala: \"%1%\"" -msgid "N/A" -msgstr "N/A" - msgid "System agents" msgstr "Sistema-agenteak" @@ -6168,6 +6168,12 @@ msgstr "Gorde proiektua honela" msgid "Save current project as" msgstr "Gorde uneko proiektua honela" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Inportatu 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7945,6 +7951,12 @@ msgstr "Berretsi aurrezarpen hauetako G-codea segurua dela, makinari kalterik ez msgid "Customized Preset" msgstr "Aurrezarpen pertsonalizatua" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP fitxategiko osagai-izena(k) ez dago/daude UTF-8 formatuan!" @@ -8321,6 +8333,17 @@ msgstr "Gorde xerratutako fitxategia honela:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s fitxategia inprimagailuaren biltegiratze-eremura bidali da eta inprimagailuan ikus daiteke." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Argitaratu" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Pita mota ez dago ezarrita. Ezarri pita eta saiatu berriro." @@ -9434,9 +9457,6 @@ msgstr "Joan modeloa argitaratzeko web-orrira" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Oharra: prestaketak minutu batzuk iraun ditzake. Izan pazientzia." -msgid "Publish" -msgstr "Argitaratu" - msgid "Publish was canceled" msgstr "Argitaratzea bertan behera utzi da" @@ -9452,6 +9472,24 @@ msgstr "Datuak igotzen" msgid "Jump to webpage" msgstr "Joan web-orrira" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Gorde %s honela" diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index fc58381106..5bd15b57cd 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -4801,6 +4801,9 @@ msgstr "" "Oui - Modifiez ces paramètres et activez automatiquement le mode spirale/vase\n" "Non - Annuler l'activation du mode spirale" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Impression" @@ -5260,9 +5263,6 @@ msgstr "Motif invalide. Utilisez N, N#K, ou une liste séparée par des virgules msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Format invalide. Format vectoriel attendu : \"%1%\"" -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Agents système" @@ -6215,6 +6215,12 @@ msgstr "Enregistrer le projet sous" msgid "Save current project as" msgstr "Enregistrer le projet actuel sous" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importer des fichiers 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8000,6 +8006,12 @@ msgstr "Veuillez vous assurer que les G-codes de ces préréglages sont sûrs af msgid "Customized Preset" msgstr "Préréglage personnalisé" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Le nom des composants dans le fichier STEP n'est pas au format UTF-8 !" @@ -8377,6 +8389,17 @@ msgstr "Enregistrer le fichier découpé sous :" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Le fichier %s a été envoyé vers l'espace de stockage de l'imprimante et peut être visualisé sur l'imprimante." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publier" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Le type de buse n'est pas défini. Veuillez définir la buse et réessayer." @@ -9504,9 +9527,6 @@ msgstr "Accéder à la page internet de publication des modèles" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Remarque : La préparation peut prendre plusieurs minutes. Veuillez patienter." -msgid "Publish" -msgstr "Publier" - msgid "Publish was canceled" msgstr "La publication a été annulée" @@ -9522,6 +9542,24 @@ msgstr "Téléversement des données" msgid "Jump to webpage" msgstr "Ouvrir la page internet" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Enregistrer %s sous" diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 9f8a849884..1f8c1022bb 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -4897,6 +4897,10 @@ msgstr "" "Igen - Módosítsd ezeket a beállításokat, és automatikusan kapcsold be a spirál módot\n" "Nem - Most ne kapcsold be a spirál módot" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Nyomtatás" @@ -5356,10 +5360,6 @@ msgstr "Érvénytelen minta. Használj N, N#K formátumot, vagy vesszővel elvá msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Érvénytelen formátum. Elvárt vektor formátum: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Rendszerügynökök" @@ -6317,6 +6317,12 @@ msgstr "Projekt mentése másként" msgid "Save current project as" msgstr "Jelenlegi projekt mentése másként" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importálás 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8115,6 +8121,12 @@ msgstr "Kérlek, győződj meg arról, hogy a beállításokban található G-k msgid "Customized Preset" msgstr "Egyedi beállítás" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "A STEP fájlon belüli komponens neve nem UTF-8 formátumban van!" @@ -8503,6 +8515,17 @@ msgstr "Szeletelt fájl mentése mint:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "A(z) %s fájlt elküldtük a nyomtató tárhelyére. A fájl a nyomtatón tekinthető meg." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Közzététel" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "A fúvókatípus nincs beállítva. Állítsd be a fúvókát, majd próbáld újra." @@ -9664,9 +9687,6 @@ msgstr "Ugrás a modell közzététele weboldalra" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Megjegyzés: Az előkészítés több percig is eltarthat. Kérlek, várj." -msgid "Publish" -msgstr "Közzététel" - msgid "Publish was canceled" msgstr "A közzététel törlésre került" @@ -9682,6 +9702,24 @@ msgstr "Adatok feltöltése" msgid "Jump to webpage" msgstr "Ugrás a weboldalra" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s mentése" diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index bc36e08d25..ac9d5531e5 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4899,6 +4899,9 @@ msgstr "" "Sì - Modifica queste impostazioni ed abilita la modalità spirale automaticamente\n" "No - Annulla l'attivazione della modalità a spirale" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Stampa" @@ -5358,9 +5361,6 @@ msgstr "Schema non valido. Utilizzare N, N#K o un elenco separato da virgole con msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato non valido. Formato vettoriale previsto: \"%1%\"" -msgid "N/A" -msgstr "N/D" - # AI Translated msgid "System agents" msgstr "Agenti di sistema" @@ -6319,6 +6319,12 @@ msgstr "Salva progetto con nome" msgid "Save current project as" msgstr "Salva progetto corrente con nome" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importa 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8119,6 +8125,12 @@ msgstr "Si prega di confermare che i G-code all'interno di questi profili sono s msgid "Customized Preset" msgstr "Profilo personalizzato" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Il nome dei componenti all'interno del file STEP non è in formato UTF8!" @@ -8502,6 +8514,17 @@ msgstr "Salva file elaborato con nome:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Il file %s è stato inviato alla memoria della stampante e può essere visualizzato da lì." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Pubblica" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Il tipo di ugello non è impostato. Impostare l'ugello e riprovare." @@ -9682,9 +9705,6 @@ msgstr "Vai alla pagina web di pubblicazione del modello" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: la preparazione può richiedere alcuni minuti. Si prega di avere pazienza." -msgid "Publish" -msgstr "Pubblica" - msgid "Publish was canceled" msgstr "La pubblicazione è stata annullata" @@ -9700,6 +9720,24 @@ msgstr "Caricamento dati" msgid "Jump to webpage" msgstr "Vai alla pagina web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Salva %s con nome" diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 1b70ddbf67..55ad80089b 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -4912,6 +4912,10 @@ msgstr "" "はい - 変更して、スパイラルモードを有効にします\n" "いいえ - 変更せず、スパイラルモードを有効しません" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "造形中" @@ -5370,10 +5374,6 @@ msgstr "無効なパターンです。N、N#K、またはオプション#K付き msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "無効なフォーマット、%1%であるはずです。" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "システムエージェント" @@ -6328,6 +6328,12 @@ msgstr "プロジェクトを名前を付けて保存" msgid "Save current project as" msgstr "プロジェクトを名前を付けて保存" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMFをインポート" @@ -8133,6 +8139,12 @@ msgstr "これらのプリセット内のG-codeがマシンに損傷を与えな msgid "Customized Preset" msgstr "カスタマイズされたプリセット" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "ファイルのエンコーディング方式はUTF8形式ではありません" @@ -8521,6 +8533,17 @@ msgstr "名前を付けて保存:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%sを送信しました、プリンターにて確認できます" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "公開する" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ノズルタイプが設定されていません。ノズルを設定して再試行してください。" @@ -9704,9 +9727,6 @@ msgstr "モデル公開ページに移動" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "注意: 準備するには数分かかる場合があります、暫くお待ち下さい。" -msgid "Publish" -msgstr "公開する" - msgid "Publish was canceled" msgstr "公開は取り消しました" @@ -9722,6 +9742,24 @@ msgstr "データをアップロード中" msgid "Jump to webpage" msgstr "ウェブページに移動" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%sを名前つけて保存" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 767674e525..66ce7b49a1 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -4923,6 +4923,10 @@ msgstr "" "예 - 이 설정을 변경하고 나선 모드를 자동으로 활성화합니다\n" "아니오 - 이번에는 나선 모드 사용을 포기합니다" +# AI Translated +msgid "N/A" +msgstr "해당 없음" + msgid "Printing" msgstr "출력 중" @@ -5382,10 +5386,6 @@ msgstr "잘못된 패턴입니다. N, N#K 또는 항목당 선택적 #K가 있 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "잘못된 형식입니다. 필요한 벡터 형식: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "해당 없음" - # AI Translated msgid "System agents" msgstr "시스템 에이전트" @@ -6342,6 +6342,12 @@ msgstr "프로젝트 다른 이름으로 저장" msgid "Save current project as" msgstr "현재 프로젝트 다른 이름으로 저장" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF 가져오기" @@ -8151,6 +8157,12 @@ msgstr "이러한 사전 설정 내의 Gcode가 기계 손상을 방지할 수 msgid "Customized Preset" msgstr "사용자 정의 프리셋" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 파일 내부의 구성 요소 이름이 UTF8 형식이 아닙니다!" @@ -8554,6 +8566,17 @@ msgstr "슬라이스 파일을 다음으로 저장:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s 파일이 프린터의 저장 공간으로 전송되었으며 프린터에서 볼 수 있습니다." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "게시" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "노즐 유형이 설정되지 않았습니다. 노즐을 설정하고 다시 시도하세요." @@ -9793,9 +9816,6 @@ msgstr "모델 게시 웹 페이지로 이동" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "참고: 준비하는 데 몇 분 정도 걸릴 수 있습니다. 조금만 기다려 주십시오." -msgid "Publish" -msgstr "게시" - msgid "Publish was canceled" msgstr "게시가 취소되었습니다" @@ -9812,6 +9832,24 @@ msgstr "데이터 업로드 중" msgid "Jump to webpage" msgstr "웹 페이지로 이동" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s을(를) 다음으로 저장" diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index ad5edffc9a..866f5fa3f6 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -4886,6 +4886,9 @@ msgstr "" "Taip – pakeisti šiuos nustatymus ir automatiškai įjungti spiralinį režimą\n" "Ne – nenaudoti spiralinio režimo" +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Spausdinimas" @@ -5345,9 +5348,6 @@ msgstr "Neteisingas šablonas. Naudokite N, N#K arba kableliais atskirtą sąra msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Netinkamas formatas. Tinkamas vektorinis formatas: \"%1%\"" -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Sisteminiai agentai" @@ -6305,6 +6305,12 @@ msgstr "Įrašyti projektą kaip" msgid "Save current project as" msgstr "Įrašyti dabartinį projektą kaip" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importuoti 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8110,6 +8116,12 @@ msgstr "Patvirtinkite, kad šiuose profiliuose esantis G-kodas yra saugus, kad i msgid "Customized Preset" msgstr "Pritaikytas profilis" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Komponentų pavadinimai STEP faile nėra UTF-8 formato!" @@ -8497,6 +8509,17 @@ msgstr "Išsaugoti susluoksniuotą failą kaip:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Failas %s išsiųstas į spausdintuvo laikmeną ir gali būti peržiūrimas spausdintuve." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Talpinti" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Purkštuko tipas nenustatytas. Nustatykite purkštuką ir bandykite dar kartą." @@ -9622,9 +9645,6 @@ msgstr "Pereiti į modelio talpinimo interneto puslapį" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Pastaba: paruošimas gali užtrukti kelias minutes. Būkite kantrūs." -msgid "Publish" -msgstr "Talpinti" - msgid "Publish was canceled" msgstr "Publikavimas buvo atšauktas" @@ -9640,6 +9660,24 @@ msgstr "Įkeliami duomenys" msgid "Jump to webpage" msgstr "Pereiti į interneto puslapį" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Išsaugoti %s kaip" diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 28ad63d455..470aa9fa86 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -5317,6 +5317,9 @@ msgstr "" "Ja - Pas de instellingen aan en zet de vaas modus automatisch aan\n" "Nee - Pas de vaas modus deze keer niet toe" +msgid "N/A" +msgstr "N/B" + msgid "Printing" msgstr "Printen" @@ -5853,9 +5856,6 @@ msgstr "Ongeldig patroon. Gebruik N, N#K of een door komma's gescheiden lijst me msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Onjuist formaat. Het Vector formaat wordt verwacht: \"%1%\"" -msgid "N/A" -msgstr "N/B" - # AI Translated msgid "System agents" msgstr "Systeemagenten" @@ -6880,6 +6880,12 @@ msgstr "Bewaar project als" msgid "Save current project as" msgstr "Bewaar huidig project als" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF importeren" @@ -8860,6 +8866,12 @@ msgstr "Controleer of de G-codes in deze presets veilig zijn om schade aan de ma msgid "Customized Preset" msgstr "Aangepaste voorinstelling" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Naam van componenten in step-bestand is niet UTF8-formaat!" @@ -9271,6 +9283,17 @@ msgstr "Bewaar het geslicede bestand als:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Het bestand %s is naar de opslagruimte van de printer gestuurd en kan op de printer worden bekeken." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publiceren" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Het mondstuktype is niet ingesteld. Stel het mondstuk in en probeer het opnieuw." @@ -10554,9 +10577,6 @@ msgstr "Ga naar de website om het model te publiceren" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Notitie: het voorbereiden kan enkele minuten duren. Even geduld alstublieft." -msgid "Publish" -msgstr "Publiceren" - msgid "Publish was canceled" msgstr "Het publiceren is geannuleerd" @@ -10573,6 +10593,24 @@ msgstr "Gegevens uploaden" msgid "Jump to webpage" msgstr "Ga naar de website" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Bewaar %s als" diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index e8acc62a9b..4adcf037ca 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -5005,6 +5005,10 @@ msgstr "" "Tak - Zmień te ustawienia automatycznie i włącz tryb Wazy\n" "Nie - Zrezygnuj tym razem z używania trybu Wazy" +# AI Translated +msgid "N/A" +msgstr "Nie dotyczy" + msgid "Printing" msgstr "Drukowanie" @@ -5472,10 +5476,6 @@ msgstr "Nieprawidłowy wzorzec. Użyj N, N#K lub listy rozdzielonej przecinkami msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Nieprawidłowy format. Oczekiwano formatu wektorowego: „%1%”" -# AI Translated -msgid "N/A" -msgstr "Nie dotyczy" - # AI Translated msgid "System agents" msgstr "Agenci systemowi" @@ -6461,6 +6461,12 @@ msgstr "Zapisz projekt jako" msgid "Save current project as" msgstr "Zapisz bieżący projekt jako" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importuj 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8309,6 +8315,12 @@ msgstr "Proszę potwierdź, że G-code w tych profilach jest bezpieczny, aby zap msgid "Customized Preset" msgstr "Dostosowany profil" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Nazwa komponentów w pliku step nie jest w formacie UTF8!" @@ -8711,6 +8723,17 @@ msgstr "Zapisz plik po wykonaniu cięcia jako:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Plik %s został wysłany do pamięci drukarki i można go obejrzeć na urządzeniu." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Opublikuj" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nie ustawiono typu dyszy Wprowadź ustawienia dyszy i spróbuj ponownie." @@ -9949,9 +9972,6 @@ msgstr "Przejdź do strony publikacji modelu" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Uwaga: Przygotowanie może zająć kilka minut. Proszę o cierpliwość." -msgid "Publish" -msgstr "Opublikuj" - msgid "Publish was canceled" msgstr "Publikacja została anulowana" @@ -9968,6 +9988,24 @@ msgstr "Przesyłanie danych" msgid "Jump to webpage" msgstr "Przejdź na stronę" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Zapisz %s jako" diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 1b39d4a159..0e1e34c7d7 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -4735,6 +4735,9 @@ msgstr "" "Sim - Alterar essas configurações e ativar o modo espiral/vaso automaticamente\n" "Não - Cancelar ativação do modo espiral" +msgid "N/A" +msgstr "N/D" + msgid "Printing" msgstr "Imprimindo" @@ -5188,9 +5191,6 @@ msgstr "Padrão inválido. Use N, N#K, ou uma lista separa por vírgulas com #K msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Formato inválido. Formato de vetor esperado: \"%1%\"" -msgid "N/A" -msgstr "N/D" - msgid "System agents" msgstr "Agentes do sistema" @@ -6137,6 +6137,12 @@ msgstr "Salvar projeto como" msgid "Save current project as" msgstr "Salvar o projeto atual como" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importar 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7897,6 +7903,12 @@ msgstr "Por favor, confirme se o G-code dentro dessas predefinições é seguro msgid "Customized Preset" msgstr "Predefinição Personalizada" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Os nomes dos componentes dentro do arquivo STEP não estão no formato UTF-8!" @@ -8277,6 +8289,17 @@ msgstr "Salvar arquivo fatiado como:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "O arquivo %s foi enviado para o espaço de armazenamento da impressora e pode ser visualizado na impressora." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publicar" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "O tipo de bico não está configurado. Configure o bico e tente novamente." @@ -9406,9 +9429,6 @@ msgstr "Ir para a página web de publicação de modelos" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Nota: A preparação pode levar vários minutos. Por favor, seja paciente." -msgid "Publish" -msgstr "Publicar" - msgid "Publish was canceled" msgstr "Publicação cancelada" @@ -9424,6 +9444,24 @@ msgstr "Enviando dados" msgid "Jump to webpage" msgstr "Ir para a página web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Salvar %s como" diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index 2372471707..debec4f832 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -4883,6 +4883,11 @@ msgid "" "No - Cancel enabling spiral mode" msgstr "Использовать эти настройки и режим вазы?" +# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо +# символов +msgid "N/A" +msgstr "–" + msgid "Printing" msgstr "Печать" @@ -5352,11 +5357,6 @@ msgstr "Недопустимый шаблон. Используйте N, N#K и msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Недопустимый формат. Ожидаемый векторный формат: \"%1%\"" -# Не знаю, как и почему, но это, похоже, исправляет "вопросики" вместо -# символов -msgid "N/A" -msgstr "–" - msgid "System agents" msgstr "Системные агенты" @@ -6370,6 +6370,12 @@ msgstr "Сохранить проект как" msgid "Save current project as" msgstr "Сохранить текущий проект как" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Импорт 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8168,6 +8174,12 @@ msgstr "Во избежание повреждения принтера убед msgid "Customized Preset" msgstr "Пользовательский профиль" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Имена компонентов внутри файла STEP не в формате UTF8." @@ -8556,6 +8568,17 @@ msgstr "Сохранить нарезанный файл как:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Файл %s отправлен в память принтера и может быть просмотрен на нём." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Опубликовать" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Не указан тип сопла. Укажите его и попробуйте ещё раз." @@ -9698,9 +9721,6 @@ msgstr "Перейти на веб-страницу публикации мод msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Примечание: подготовка может занять несколько минут. Пожалуйста, наберитесь терпения." -msgid "Publish" -msgstr "Опубликовать" - msgid "Publish was canceled" msgstr "Публикация была отменена" @@ -9716,6 +9736,24 @@ msgstr "Отправка данных" msgid "Jump to webpage" msgstr "Перейти на страницу" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Сохранить %s как" diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 432000f96d..7add432a60 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -5379,6 +5379,10 @@ msgstr "" "JA -Ändra dessa inställningar och möjliggör Spiral läge automatiskt\n" "NEJ -Avbryt Spiral läge denna gång" +# AI Translated +msgid "N/A" +msgstr "Ej tillämpligt" + msgid "Printing" msgstr "Utskrift pågår" @@ -5928,10 +5932,6 @@ msgstr "Ogiltigt mönster. Använd N, N#K eller en kommaseparerad lista med valf msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Ogiltligt format. Förväntat vector format: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "Ej tillämpligt" - # AI Translated msgid "System agents" msgstr "Systemagenter" @@ -6964,6 +6964,12 @@ msgstr "Spara Projekt som" msgid "Save current project as" msgstr "Spara nuvarande projekt som" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Importera 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8952,6 +8958,12 @@ msgstr "Bekräfta att G-koderna i dessa inställningar är säkra för att förh msgid "Customized Preset" msgstr "Anpassad inställning" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Komponent namnet i STEP filen är inte UTF8 format!" @@ -9364,6 +9376,17 @@ msgstr "Spara beredningen som:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Filen %s har skickats till skrivarens lagringsutrymme och kan visas på skrivaren." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Publicera" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozzeltypen är inte angiven. Ange nozzeln och försök igen." @@ -10668,9 +10691,6 @@ msgstr "Växla till modell publicerings hemsidan" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Notera: Förberedelserna kan ta flera minuter. Vänligen vänta." -msgid "Publish" -msgstr "Publicera" - msgid "Publish was canceled" msgstr "Publiceringen avbröts" @@ -10687,6 +10707,24 @@ msgstr "Laddar upp data" msgid "Jump to webpage" msgstr "Växla till hemsidan" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Spara %s som" diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index a0c0079125..60163fe29b 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -4879,6 +4879,9 @@ msgstr "" "ใช่ - เปลี่ยนการตั้งค่าเหล่านี้และเปิดใช้งานโหมดเกลียวโดยอัตโนมัติ\n" "ไม่ - เลิกใช้โหมดเกลียวในครั้งนี้" +msgid "N/A" +msgstr "ไม่มี" + msgid "Printing" msgstr "กำลังพิมพ์" @@ -5338,9 +5341,6 @@ msgstr "รูปแบบไม่ถูกต้อง ใช้ N, N#K หร msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "รูปแบบไม่ถูกต้อง รูปแบบเวกเตอร์ที่ต้องการ: \"%1%\"" -msgid "N/A" -msgstr "ไม่มี" - # AI Translated msgid "System agents" msgstr "เอเจนต์ระบบ" @@ -6296,6 +6296,12 @@ msgstr "บันทึกโปรเจกต์เป็น" msgid "Save current project as" msgstr "บันทึกโครงการปัจจุบันเป็น" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "นำเข้า 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8074,6 +8080,12 @@ msgstr "โปรดยืนยันว่ารหัส G ภายในค msgid "Customized Preset" msgstr "ค่าที่ตั้งไว้ล่วงหน้าที่กำหนดเอง" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "ชื่อของส่วนประกอบภายในไฟล์ STEP ไม่ใช่รูปแบบ UTF8!" @@ -8457,6 +8469,17 @@ msgstr "บันทึกไฟล์ที่สไลซ์เป็น:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "ไฟล์ %s ถูกส่งไปยังพื้นที่เก็บข้อมูลของเครื่องพิมพ์แล้ว และสามารถดูได้บนเครื่องพิมพ์" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "เผยแพร่" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "ไม่ได้ตั้งค่าประเภทหัวฉีด โปรดตั้งหัวฉีดแล้วลองอีกครั้ง" @@ -9595,9 +9618,6 @@ msgstr "ข้ามไปที่โมเดลเผยแพร่หน้ msgid "Note: The preparation may take several minutes. Please be patient." msgstr "หมายเหตุ: การเตรียมการอาจใช้เวลาหลายนาที กรุณาอดทน." -msgid "Publish" -msgstr "เผยแพร่" - msgid "Publish was canceled" msgstr "การเผยแพร่ถูกยกเลิก" @@ -9613,6 +9633,24 @@ msgstr "กำลังอัพโหลดข้อมูล" msgid "Jump to webpage" msgstr "ข้ามไปที่หน้าเว็บ" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "บันทึก %s เป็น" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index a31d2216f4..5d7a6707c3 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-08-04 19:36+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -4967,6 +4967,10 @@ msgstr "" "Evet - Bu ayarları değiştirin ve spiral modunu otomatik olarak etkinleştirin\n" "Hayır - Bu sefer spiral modunu kullanmaktan vazgeçin" +# AI Translated +msgid "N/A" +msgstr "Yok" + msgid "Printing" msgstr "Baskı" @@ -5426,10 +5430,6 @@ msgstr "Geçersiz kalıp. N, N#K veya giriş başına isteğe bağlı #K ile vir msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Geçersiz format. Beklenen vektör formatı: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "Yok" - # AI Translated msgid "System agents" msgstr "Sistem aracıları" @@ -6390,6 +6390,12 @@ msgstr "Projeyi farklı kaydet" msgid "Save current project as" msgstr "Mevcut projeyi farklı kaydet" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "3MF/STL/STEP/SVG/OBJ/AMF'yi içe aktar" @@ -8197,6 +8203,12 @@ msgstr "Lütfen bu ön ayarlar içindeki G-kodlarının makineye herhangi bir za msgid "Customized Preset" msgstr "Özel Ayar" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Step dosyasındaki bileşenlerin adı UTF8 formatında değil!" @@ -8582,6 +8594,17 @@ msgstr "Dilimlenmiş dosyayı şu şekilde kaydedin:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "%s dosyası yazıcının depolama alanına gönderildi ve yazıcıda görüntülenebiliyor." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Yayınla" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Nozul tipi ayarlanmamış. Lütfen nozulu ayarlayın ve tekrar deneyin." @@ -9765,9 +9788,6 @@ msgstr "Model yayınlama web sayfasına git" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Not: Hazırlık birkaç dakika sürebilir. Lütfen sabırlı olun." -msgid "Publish" -msgstr "Yayınla" - msgid "Publish was canceled" msgstr "Yayınlama iptal edildi" @@ -9783,6 +9803,24 @@ msgstr "Veriler yükleniyor" msgid "Jump to webpage" msgstr "Web sayfasına atla" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "%s'yi farklı kaydet" diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 4c3cc1d56c..fbbb095cb5 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk \n" "Language-Team: Ukrainian\n" @@ -4879,6 +4879,9 @@ msgstr "" "Так – змінити ці налаштування та автоматично включити режим спіральна ваза\n" "Ні - цього разу відмовитися від використання режиму спіральна ваза" +msgid "N/A" +msgstr "Н/Д" + msgid "Printing" msgstr "Друк" @@ -5350,9 +5353,6 @@ msgstr "Некоректний шаблон. Використовуйте N, N#K msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Невірний формат. Очікуваний векторний формат: \"%1%\"" -msgid "N/A" -msgstr "Н/Д" - # AI Translated msgid "System agents" msgstr "Системні агенти" @@ -6333,6 +6333,12 @@ msgstr "Зберегти проєкт як" msgid "Save current project as" msgstr "Зберегти поточний проєкт як" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Імпорт 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8179,6 +8185,12 @@ msgstr "Будь ласка, підтвердьте, що G-коди в цих msgid "Customized Preset" msgstr "Пристосований пресет" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + # AI Translated msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Назви компонентів усередині файлу STEP не у форматі UTF8!" @@ -8567,6 +8579,17 @@ msgstr "Зберегти нарізаний файл як:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "Файл %s надіслано до памʼяті принтера та доступний для перегляду на принтері." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Публікувати" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Тип сопла не встановлений. Будь ласка, оберіть сопло та спробуйте ще раз." @@ -9742,9 +9765,6 @@ msgstr "Перейти на веб-сторінку публікації мод msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Примітка. Підготовка може тривати кілька хвилин. Будь ласка, будьте терплячі." -msgid "Publish" -msgstr "Публікувати" - msgid "Publish was canceled" msgstr "Публікація скасована" @@ -9760,6 +9780,24 @@ msgstr "Відвантаження даних" msgid "Jump to webpage" msgstr "Перейти на вебсторінку" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Зберегти %s як" diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 00a9a558ba..519a6c3b25 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -5135,6 +5135,10 @@ msgstr "" "Yes - Thay đổi các cài đặt này và bật chế độ spiral tự động\n" "No - Từ bỏ dùng chế độ spiral lần này" +# AI Translated +msgid "N/A" +msgstr "N/A" + msgid "Printing" msgstr "Đang in" @@ -5653,10 +5657,6 @@ msgstr "Mẫu không hợp lệ. Dùng N, N#K, hoặc danh sách phân cách d msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "Định dạng không hợp lệ. Mong đợi định dạng vector: \"%1%\"" -# AI Translated -msgid "N/A" -msgstr "N/A" - # AI Translated msgid "System agents" msgstr "Tác nhân hệ thống" @@ -6681,6 +6681,12 @@ msgstr "Lưu dự án thành" msgid "Save current project as" msgstr "Lưu dự án hiện tại thành" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "Nhập 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8585,6 +8591,12 @@ msgstr "Vui lòng xác nhận G-code trong các preset này an toàn để ngăn msgid "Customized Preset" msgstr "Preset tùy chỉnh" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "Tên của các thành phần bên trong file STEP không phải định dạng UTF8!" @@ -8988,6 +9000,17 @@ msgstr "Lưu file đã slice dưới dạng:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "File %s đã được gửi đến không gian lưu trữ của máy in và có thể được xem trên máy in." +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "Xuất bản" + # AI Translated msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "Chưa đặt loại đầu phun. Vui lòng đặt đầu phun rồi thử lại." @@ -10254,9 +10277,6 @@ msgstr "Chuyển đến trang web xuất bản model" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "Lưu ý: Chuẩn bị có thể mất vài phút. Vui lòng kiên nhẫn." -msgid "Publish" -msgstr "Xuất bản" - msgid "Publish was canceled" msgstr "Xuất bản đã bị hủy" @@ -10273,6 +10293,24 @@ msgstr "Đang tải dữ liệu lên" msgid "Jump to webpage" msgstr "Chuyển đến trang web" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "Lưu %s dưới dạng" diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 133e0815a4..2abe2fc5d2 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -4735,6 +4735,9 @@ msgstr "" "是 - 自动调整这些设置并开启旋转模式\n" "否 - 暂不使用旋转模式" +msgid "N/A" +msgstr "不适用" + msgid "Printing" msgstr "打印中" @@ -5194,9 +5197,6 @@ msgstr "无效的模式。请使用 N、N#K 或逗号分隔的列表(每个条 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "无效格式,应该是\"%1%\"这种数组格式" -msgid "N/A" -msgstr "不适用" - # AI Translated msgid "System agents" msgstr "系统代理" @@ -6151,6 +6151,12 @@ msgstr "项目另存为" msgid "Save current project as" msgstr "项目另存为" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "导入 3MF/STL/STEP/SVG/OBJ/AMF" @@ -7905,6 +7911,12 @@ msgstr "请确认这些预设中的G-codes是否安全,以防止对机器造 msgid "Customized Preset" msgstr "自定义的预设" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 文件中的部件名称不是 UTF8 格式!" @@ -8286,6 +8298,17 @@ msgstr "切片文件另存为:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "文件%s已经发送到打印机的存储空间,可以在打印机上浏览。" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "发布" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "未设置喷嘴类型。请设置喷嘴并重试。" @@ -9417,9 +9440,6 @@ msgstr "跳转到发布页面" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "提示:发布前需要一些准备时间,请耐心等待。" -msgid "Publish" -msgstr "发布" - msgid "Publish was canceled" msgstr "发布已取消" @@ -9435,6 +9455,24 @@ msgstr "正在上传数据" msgid "Jump to webpage" msgstr "跳转到网页" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "另存%s为" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index cf6a2519c3..839e7cdb2b 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-19 14:07-0300\n" +"POT-Creation-Date: 2026-08-21 14:25+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -4864,6 +4864,9 @@ msgstr "" "是 - 自動調整這些設定並啟用花瓶模式\n" "否 - 不使用花瓶模式" +msgid "N/A" +msgstr "不適用" + msgid "Printing" msgstr "列印中" @@ -5323,9 +5326,6 @@ msgstr "無效的格式。請使用 N、N#K 或逗號分隔的清單,每個項 msgid "Invalid format. Expected vector format: \"%1%\"" msgstr "無效格式,應該是「%1%」這種格式" -msgid "N/A" -msgstr "不適用" - # AI Translated msgid "System agents" msgstr "系統代理程式" @@ -6281,6 +6281,12 @@ msgstr "另存專案為" msgid "Save current project as" msgstr "將目前專案另存為" +msgid "Publish 3MF" +msgstr "" + +msgid "Export a 3MF file with the selected settings embedded" +msgstr "" + msgid "Import 3MF/STL/STEP/SVG/OBJ/AMF" msgstr "匯入 3MF/STL/STEP/SVG/OBJ/AMF" @@ -8066,6 +8072,12 @@ msgstr "請確認這些預設中的 G-code 是安全的,以防止對列印裝 msgid "Customized Preset" msgstr "自訂預設" +msgid "Some published settings could not be applied:" +msgstr "" + +msgid "Some filament slots were changed to match the published materials:" +msgstr "" + msgid "Component name(s) inside step file not in UTF8 format!" msgstr "STEP 檔案內部元件的名稱不是 UTF-8 格式!" @@ -8452,6 +8464,17 @@ msgstr "切片檔案另存為:" msgid "The file %s has been sent to the printer's storage space and can be viewed on the printer." msgstr "檔案 %s 已經傳送到列印裝置的儲存空間,可以在列印裝置上瀏覽。" +msgid "Publish 3MF file as:" +msgstr "" + +msgid "" +"Failed to export the published 3MF file.\n" +"Please check whether the folder exists online or if other programs have the file open." +msgstr "" + +msgid "Publish" +msgstr "發布" + msgid "The nozzle type is not set. Please set the nozzle and try again." msgstr "噴嘴類型尚未設定。請設定噴嘴後再試一次。" @@ -9590,9 +9613,6 @@ msgstr "發布頁面" msgid "Note: The preparation may take several minutes. Please be patient." msgstr "提示:發布前需要一些準備時間,請耐心等待。" -msgid "Publish" -msgstr "發布" - msgid "Publish was canceled" msgstr "發布已取消" @@ -9608,6 +9628,24 @@ msgstr "正在上傳資料" msgid "Jump to webpage" msgstr "跳至網頁" +msgid "Material" +msgstr "" + +msgid "Publish 3MF..." +msgstr "" + +msgid "Select which settings to embed in the 3MF file" +msgstr "" + +msgid "Full Publish" +msgstr "" + +msgid "Embed the entire filament of this slot in the 3MF file" +msgstr "" + +msgid "Filter non-selected" +msgstr "" + #, c-format, boost-format msgid "Save %s as" msgstr "另存 %s 為" diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 881560f93c..cd8b82a489 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -34,6 +34,20 @@ enum { kPublishFilterNonSelected }; +// Tree-style flags shared by the outer tabs and every group's inner tabs. +constexpr long s_tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | + wxTR_FULL_ROW_HIGHLIGHT; + +// The author's hex colour for a filament slot (empty when the slot is out of range); shared by +// the header chip, the inner-tab chip and the DPI rescale paths. +std::string filament_color_hex(const DynamicPrintConfig& full, size_t slot) +{ + if (const auto* colours = full.opt("filament_colour")) + if (slot < colours->size()) + return colours->get_at(slot); + return std::string(); +} + PublishMaterialIdentity material_identity(size_t slot, const DynamicPrintConfig& full) { PublishMaterialIdentity identity; @@ -114,7 +128,6 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) e.Skip(); }); f_sizer->Add(m_filter_box, 1, wxEXPAND); - Bind(wxEVT_SET_FOCUS, [this](auto&) { m_filter_box->SetFocus(); }); m_fb_sizer = new wxBoxSizer(wxHORIZONTAL); auto create_btn = [this, f_bar](wxString title, bool select) { @@ -129,6 +142,17 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) create_btn(_L("All"), true); create_btn(_L("None"), false); + // Indicator for the menu's pseudo filters: sits where All/None go while they are hidden. + // Labels reuse the menu entries (no new strings); clicking the chip returns to text + // filtering, keeping the search box contents. + m_pseudo_chip = new wxStaticText(f_bar, wxID_ANY, ""); + m_pseudo_chip->SetForegroundColour("#009687"); + m_pseudo_chip->SetCursor(wxCURSOR_HAND); + m_pseudo_chip->SetFont(Label::Body_13); + m_pseudo_chip->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { apply_filter(m_filter_ctrl->GetValue()); }); + m_pseudo_chip->Hide(); + f_sizer->Add(m_pseudo_chip, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + m_menu_button = new wxStaticBitmap(f_bar, wxID_ANY, m_menu.bmp()); m_menu_button->SetCursor(wxCURSOR_HAND); m_menu_button->Bind(wxEVT_LEFT_DOWN, &PublishSettingsDialog::show_menu, this); @@ -136,9 +160,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) f_bar->SetSizerAndFit(f_sizer); - constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | - wxTR_FULL_ROW_HIGHLIGHT; - m_outer_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, tab_style); + m_outer_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); m_outer_tabs->SetFont(Label::Body_14); m_outer_tabs->SetBackgroundColour(GetBackgroundColour()); @@ -224,7 +246,7 @@ void PublishSettingsDialog::build_option_model() // Printer group), from the printer tab's "Extruder"/"Extruder N" pages. { size_t g = section_group_for(Section::Printer); - category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); + category_index_for(_L("Extruder"), Section::Printer, g, 0); for (Tab* tab : wxGetApp().tabs_list) { if (tab->m_type != Preset::TYPE_PRINTER) continue; @@ -249,7 +271,7 @@ void PublishSettingsDialog::build_option_model() wxString label, value, unit; if (!option_text(opt_id, pure_key, label, value, unit)) continue; - size_t cat_index = category_index_for(_L("Extruder"), Section::Printer, "custom-gcode_extruder", g, 0); + size_t cat_index = category_index_for(_L("Extruder"), Section::Printer, g, 0); size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon); add_row_ui(pure_key, label, value, unit, cat_index, sub_index); } @@ -282,18 +304,15 @@ void PublishSettingsDialog::build_option_model() for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { 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); + const size_t category_index = category_index_for(title, Section::Material, g, slot, identity); // Material requirement rows: an optional filament colour and/or a // vendor-agnostic material type for this slot, 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("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); + add_row_ui("filament_colour", _L("Color"), from_u8(filament_color_hex(full, slot)), wxString(), category_index, + req_sub, RowKind::Color); std::string type; if (const auto* types = full.opt("filament_type")) if (slot < types->size()) @@ -340,16 +359,10 @@ void PublishSettingsDialog::build_option_model() for (Tab* tab : wxGetApp().tabs_list) { if (tab->m_type != Preset::TYPE_PRINT) continue; - const auto& icon_map = tab->get_category_icon_map(); - size_t page_index = 0; + size_t page_index = 0; for (const PageShp& page : tab->m_pages) { - wxString category = Tab::translate_category(page->title(), tab->m_type); - // Page icon, keyed by the untranslated page title (per-Tab map). - std::string icon_name; - auto icon_it = icon_map.find(page->title()); - if (icon_it != icon_map.end()) - icon_name = icon_it->second; - const size_t category_index = category_index_for(category, Section::Print, icon_name, g, page_index); + wxString category = Tab::translate_category(page->title(), tab->m_type); + const size_t category_index = category_index_for(category, Section::Print, g, page_index); for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { for (const auto& opt : optgroup->opt_map()) { @@ -440,12 +453,10 @@ size_t PublishSettingsDialog::section_group_for(Section kind) } section.icon_bmp = ScalableBitmap(this, section.icon_name, 16); - constexpr long tab_style = wxTR_NO_BUTTONS | wxTR_HIDE_ROOT | wxTR_SINGLE | wxTR_NO_LINES | wxBORDER_NONE | wxWANTS_CHARS | - wxTR_FULL_ROW_HIGHLIGHT; - section.page = new wxPanel(m_outer_host, wxID_ANY); + section.page = new wxPanel(m_outer_host, wxID_ANY); section.page->SetBackgroundColour(GetBackgroundColour()); auto* page_sizer = new wxBoxSizer(wxVERTICAL); - section.tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, tab_style); + section.tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.tabs->SetFont(Label::Body_14); section.tabs->SetBackgroundColour(GetBackgroundColour()); page_sizer->Add(section.tabs, 0, wxEXPAND); @@ -466,12 +477,8 @@ size_t PublishSettingsDialog::section_group_for(Section kind) return new_index; } -size_t PublishSettingsDialog::category_index_for(const wxString& title, - Section section, - const std::string& icon_name, - size_t group, - size_t source_index, - const PublishMaterialIdentity& identity) +size_t PublishSettingsDialog::category_index_for( + const wxString& title, Section section, size_t group, size_t source_index, const PublishMaterialIdentity& identity) { for (size_t i : m_sections[group].categories) { Category& existing = m_categories[i]; @@ -485,7 +492,6 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title, category.section = section; category.group = group; category.source_index = source_index; - category.icon_name = icon_name; category.filament_type = identity.type; category.filament_vendor = identity.vendor; category.filament_id = identity.id; @@ -494,13 +500,13 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title, category.page->SetBackgroundColour(GetBackgroundColour()); auto* page_sizer = new wxBoxSizer(wxVERTICAL); + // The slot's colour chip decorates both the section header and the inner tab. + std::string hex; + if (section == Section::Material) + hex = filament_color_hex(wxGetApp().preset_bundle->full_config(), source_index); + if (section == Section::Material) { auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); - std::string hex; - const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); - if (const auto* colours = full.opt("filament_colour")) - if (source_index < colours->size()) - hex = colours->get_at(source_index); if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { 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)); @@ -534,11 +540,6 @@ size_t PublishSettingsDialog::category_index_for(const wxString& title, m_categories.push_back(std::move(category)); m_sections[group].categories.push_back(category_index); if (section == Section::Material) { - std::string hex; - const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); - if (const auto* colours = full.opt("filament_colour")) - if (source_index < colours->size()) - hex = colours->get_at(source_index); if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) m_sections[group].tabs->AppendItem(title, *chip); else @@ -564,7 +565,7 @@ size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const if (!title.IsEmpty()) { sub.header = new ::StaticLine(category.scroll, false, title, icon); sub.header->SetFont(Label::Head_14); - sub.header->SetForegroundColour("#363636"); + sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636"))); auto* wrap = new wxBoxSizer(wxVERTICAL); wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6)); sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22)); @@ -594,7 +595,6 @@ void PublishSettingsDialog::add_row_ui(const std::string& key, row.section_title = m_sections[category.group].title; row.outer_index = category.group; row.inner_index = category_index; - row.subcategory_index = subcategory_index; const size_t row_index = m_rows.size(); m_rows.push_back(std::move(row)); Row& current = m_rows[row_index]; @@ -628,10 +628,10 @@ void PublishSettingsDialog::add_row_ui(const std::string& key, void PublishSettingsDialog::on_full_toggle(size_t category_index) { - Category& cat = m_categories[category_index]; - cat.full = cat.full_check->GetValue(); + Category& cat = m_categories[category_index]; + const bool full = cat.full_check->GetValue(); for (size_t r : cat.rows) - m_rows[r].check->Enable(!cat.full); + m_rows[r].check->Enable(!full); } void PublishSettingsDialog::set_row_bold(Row& row, bool bold) @@ -707,20 +707,30 @@ void PublishSettingsDialog::bind_tab_events() void PublishSettingsDialog::apply_filter(const wxString& filter_text) { - Freeze(); - wxString filter = filter_text.Lower(); + m_filter_mode = FilterMode::Text; + refresh_filter(filter_text.Lower()); +} - // Pseudo filters (menu only): show only checked ("::sel") or only unchecked ("::nonsel"). - const bool pseudo = (filter == "::sel" || filter == "::nonsel"); +void PublishSettingsDialog::apply_pseudo_filter(bool selected_only) +{ + m_filter_mode = selected_only ? FilterMode::SelectedOnly : FilterMode::UnselectedOnly; + refresh_filter(wxString()); // the pseudo modes ignore the search text +} + +void PublishSettingsDialog::refresh_filter(const wxString& filter) +{ + Freeze(); + const bool pseudo = m_filter_mode != FilterMode::Text; + const bool want_checked = m_filter_mode == FilterMode::SelectedOnly; m_fb_sizer->Show(!pseudo); + if (pseudo) { + // "×" marks the chip as a dismissible filter state (U+00D7, present in all UI fonts). + m_pseudo_chip->SetLabel((want_checked ? _L("Filter selected") : _L("Filter non-selected")) + " " + wxString::FromUTF8("\u00d7")); + } + m_pseudo_chip->Show(pseudo); // Row matches are computed first; page and optgroup visibility is applied below. if (pseudo) { - if (m_filter_ctrl->GetValue().Lower() != filter) { - m_filter_ctrl->ChangeValue(filter); - m_filter_ctrl->SetSelection(0, -1); - } - const bool want_checked = (filter == "::sel"); for (Row& row : m_rows) row.matches_filter = row.check->IsEnabled() && row.check->GetValue() == want_checked; } else { @@ -744,7 +754,7 @@ void PublishSettingsDialog::apply_filter(const wxString& filter_text) has_match = has_match || m_rows[r].matches_filter; category.info->Show(!has_match); if (!has_match) - category.info->SetLabel(pseudo ? (filter == "::sel" ? m_info_nonsel : m_info_allsel) : m_info_empty); + category.info->SetLabel(pseudo ? (want_checked ? m_info_nonsel : m_info_allsel) : m_info_empty); if (has_match && first_inner < 0) { first_outer = s; first_inner = static_cast(inner); @@ -818,10 +828,10 @@ bool PublishSettingsDialog::row_is_visible(const Row& row) const void PublishSettingsDialog::select_visible(bool value) { - wxString filter = m_filter_ctrl->GetValue().Lower(); // In a pseudo-filter view the rows being toggled would all disappear; drop the filter // afterwards so the result stays visible. - bool clear_pseudo = (!value && filter == "::nonsel") || (value && filter == "::sel"); + const bool clear_pseudo = (m_filter_mode == FilterMode::UnselectedOnly && !value) || + (m_filter_mode == FilterMode::SelectedOnly && value); // Toggle the rows visible under the *current* filter. for (Row& row : m_rows) @@ -838,7 +848,7 @@ void PublishSettingsDialog::select_visible(bool value) void PublishSettingsDialog::show_menu(wxMouseEvent& evt) { - bool filtering = !m_filter_ctrl->GetValue().IsEmpty(); + bool filtering = !m_filter_ctrl->GetValue().IsEmpty() || m_filter_mode != FilterMode::Text; bool list_empty = true; if (m_selected_outer >= 0) { for (const Row& row : m_rows) @@ -855,8 +865,10 @@ void PublishSettingsDialog::show_menu(wxMouseEvent& evt) m.Append(kPublishSelectVisible, _L("Select visible"))->Enable(!list_empty && filtering); m.Append(kPublishDeselectVisible, _L("Deselect visible"))->Enable(!list_empty && filtering); m.AppendSeparator(); - m.Append(kPublishFilterSelected, _L("Filter selected")); - m.Append(kPublishFilterNonSelected, _L("Filter nonSelected")); + m.AppendCheckItem(kPublishFilterSelected, _L("Filter selected")); + m.AppendCheckItem(kPublishFilterNonSelected, _L("Filter non-selected")); + m.Check(kPublishFilterSelected, m_filter_mode == FilterMode::SelectedOnly); + m.Check(kPublishFilterNonSelected, m_filter_mode == FilterMode::UnselectedOnly); m.Bind( wxEVT_MENU, @@ -866,8 +878,19 @@ void PublishSettingsDialog::show_menu(wxMouseEvent& evt) case kPublishDeselectAll: select_all(false); break; case kPublishSelectVisible: select_visible(true); break; case kPublishDeselectVisible: select_visible(false); break; - case kPublishFilterSelected: apply_filter("::sel"); break; - case kPublishFilterNonSelected: apply_filter("::nonsel"); break; + case kPublishFilterSelected: + // Clicking the active entry again clears the pseudo filter. + if (m_filter_mode == FilterMode::SelectedOnly) + apply_filter(m_filter_ctrl->GetValue()); + else + apply_pseudo_filter(true); + break; + case kPublishFilterNonSelected: + if (m_filter_mode == FilterMode::UnselectedOnly) + apply_filter(m_filter_ctrl->GetValue()); + else + apply_pseudo_filter(false); + break; default: break; } }, @@ -926,9 +949,9 @@ std::vector PublishSettingsDialog::GetPublishedM // The author's preset id distinguishes exact variants that share filament_id // ("Generic PLA" vs "Generic PLA Matte"), so the receiver can match precisely; the // preset name is the most direct identity and is matched first on load. - PresetBundle *bundle = wxGetApp().preset_bundle; + PresetBundle* bundle = wxGetApp().preset_bundle; if (bundle != nullptr && cat.filament_slot < bundle->filament_presets.size()) { - if (const Preset *preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) { + if (const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[cat.filament_slot], false, true)) { entry.setting_id = preset->setting_id; entry.preset_name = preset->name; } @@ -995,22 +1018,15 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) m_menu_button->SetBitmap(m_menu.bmp()); m_outer_tabs->Rescale(); + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + for (Category& cat : m_categories) { - if (cat.icon != nullptr && cat.icon_bmp.bmp().IsOk()) { - cat.icon_bmp.msw_rescale(); - cat.icon->SetBitmap(cat.icon_bmp.bmp()); - } 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(); - if (const auto* colours = full.opt("filament_colour")) - if (cat.filament_slot < colours->size()) - hex = colours->get_at(cat.filament_slot); - if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) + if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, cat.filament_slot), "", FromDIP(12), FromDIP(12))) cat.filament_color_chip->SetBitmap(*chip); } cat.scroll->FitInside(); @@ -1033,16 +1049,11 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) } } - 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]; if (category.section != Section::Material) continue; - std::string hex; - if (const auto* colours = full.opt("filament_colour")) - if (category.filament_slot < colours->size()) - hex = colours->get_at(category.filament_slot); - if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { + if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), "", FromDIP(12), FromDIP(12))) { const SectionGroup& section = m_sections[category.group]; const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); if (iter != section.categories.end()) diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 74fc6a7077..b474baa938 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -73,14 +73,13 @@ private: RowKind kind{RowKind::Setting}; size_t outer_index{0}; size_t inner_index{0}; - size_t subcategory_index{0}; bool dirty{false}; // matches a dirty base key: pre-checked + bold bool matches_filter{false}; // survives the active filter (computed by apply_filter) 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 + wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer }; // An optgroup heading. Rows store indices into m_rows. @@ -97,22 +96,17 @@ private: { wxString title; Section section{Section::Print}; - size_t group{0}; // index into m_sections / outer page - size_t source_index{0}; // stable source page or material slot index - std::string source_title; - std::string icon_name; + size_t group{0}; // index into m_sections / outer page + size_t source_index{0}; // stable source page or material slot index wxPanel* page{nullptr}; wxScrolledWindow* scroll{nullptr}; wxBoxSizer* list_sizer{nullptr}; wxStaticText* info{nullptr}; wxPoint scroll_pos{0, 0}; - ScalableBitmap icon_bmp; // scalable bitmap for DPI changes - wxStaticBitmap* icon{nullptr}; wxStaticBitmap* filament_color_chip{nullptr}; wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere) // "Full Publish": while checked, the whole slot preset is serialized and its rows // (incl. Color/Type) are disabled. - bool full{false}; wxCheckBox* full_check{nullptr}; // Material identity, only for Section::Material categories. std::string filament_type; @@ -141,6 +135,12 @@ private: void build_option_model(); void apply_filter(const wxString& filter_text); + // 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. + void apply_pseudo_filter(bool selected_only); + // Recompute row matches and visibility for the active filter mode. filter is the lowered + // search text; it is ignored by the pseudo modes. + void refresh_filter(const wxString& filter); void select_all(bool value); void select_visible(bool value); void show_menu(wxMouseEvent& evt); @@ -149,11 +149,19 @@ private: 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 category_index_for(const wxString& title, + Section section, + 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, RowKind kind = RowKind::Setting); + 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, + RowKind kind = RowKind::Setting); // The non-structural filament keys of a slot's preset, for a "Full Publish" entry. std::vector full_keys_for_slot() const; void save_scroll_position(Category& category); @@ -170,9 +178,15 @@ private: wxBoxSizer* m_outer_host_sizer{nullptr}; int m_selected_outer{-1}; wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer + // Active filter mode: free text from the search box, or one of the menu's pseudo filters. + enum class FilterMode { Text, SelectedOnly, UnselectedOnly }; + FilterMode m_filter_mode{FilterMode::Text}; TextInput* m_filter_box{nullptr}; wxTextCtrl* m_filter_ctrl{nullptr}; wxStaticBitmap* m_menu_button{nullptr}; + // Shown while a pseudo filter is active (the search box keeps the user's text, so the chip + // carries the visible state); clicking it returns to text filtering. + wxStaticText* m_pseudo_chip{nullptr}; wxString m_info_nonsel; wxString m_info_allsel; wxString m_info_empty; From 95e392e206108782c557b44009f5d620a333ea18 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 21 Aug 2026 18:36:30 +0800 Subject: [PATCH 16/60] Code cleanup --- src/libslic3r/Format/bbs_3mf.cpp | 16 +++------- src/libslic3r/PresetBundle.cpp | 31 +++++++++++++------ src/libslic3r/PublishSettings.cpp | 28 +++++++---------- src/libslic3r/PublishSettings.hpp | 13 ++++++-- src/slic3r/GUI/ConfigValueFormatter.hpp | 5 +-- src/slic3r/GUI/Plater.cpp | 4 +-- src/slic3r/GUI/Plater.hpp | 2 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 12 +++---- src/slic3r/GUI/PublishSettingsDialog.hpp | 5 +-- .../libslic3r/test_preset_bundle_loading.cpp | 10 ++++-- 10 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 06cc89de99..6415f29413 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -6987,17 +6987,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">" << xml_escape(item.second) << "\n"; if (item.first == BBL_APPLICATION_TAG) { - // The OrcaSlicer tag is the version receivers compare against their own to - // pick the import branch, and every graceful config-less branch of an - // Orca-classified file shows a baked-in "old OrcaSlicer version" popup. A - // minimal published 3MF omits the tag (together with the Application tag - // above): old receivers then classify it From_Other and import the geometry - // silently, while this build rebuilds the config from the published metadata - // payload before the branch runs. - if (!m_minimal_published) { - stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" - << xml_escape(SoftFever_VERSION) << "\n"; - } + // The OrcaSlicer tag is only written for files that carry the Application + // tag, which a minimal published 3MF omits (see the map assignment above): + // the branch below is unreachable in minimal mode. + stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" + << xml_escape(SoftFever_VERSION) << "\n"; } } diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b6a3ed2d4b..fffd9c7fa4 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -43,6 +43,8 @@ namespace Slic3r { // Project-level options imported from a loaded 3MF into project_config. s_project_options_published // below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync. +// s_project_options_published additionally carries wipe_tower_rotation_angle, which normal +// loads do not import (it is not listed here): published-only plate geometry. static std::vector s_project_options { "flush_volumes_vector", "flush_volumes_matrix", @@ -73,8 +75,9 @@ static std::vector s_project_options { "enable_filament_dynamic_map" }; -// Project options applied when loading a "published" 3MF project: the full s_project_options -// minus the filament/purge keys. A published file must not port the author's filament data +// Project options applied when loading a "published" 3MF project: s_project_options minus the +// filament/purge keys, plus wipe_tower_rotation_angle (plate geometry that only published +// loads import today). A published file must not port the author's filament data // (colors, colour types, filament/map/AMS slot state, purge/prime/flush volumes, nozzle // volume types, filament switcher state) to the receiver's project_config, which feeds the // scene colors, AMS slot colors and purge data. Only the plate/bed geometry keys cross over; @@ -4799,7 +4802,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (applied_keys.count(key) != 0) continue; // already applied // A '#' suffix denotes a variant key; resolve the base key. - const std::string base_key = key.substr(0, key.find('#')); + const std::string base_key = publish_base_key(key); // Structural keys are never applied (not "skipped due to mismatch"), so bail // out before the applied/skipped bookkeeping. if (structural_keys.count(base_key) != 0) @@ -4936,9 +4939,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // type-only pick may surface an unrelated preset, e.g. a different vendor's // PLA). auto candidate_score = [](const Preset &candidate, const PublishedMaterialEntry &entry, const std::string &resolved_name) -> int { - // Exact preset name: raw and collection-resolved forms both score above the - // fuzzy bare/alias tier, so a receiver preset literally named "Generic PLA" - // can never beat the author's exact "Generic PLA @Vendor" preset on a tie. + // Exact preset name: raw and collection-resolved forms both outrank the + // fuzzy bare/alias tier by tier value, so a receiver preset literally named + // "Generic PLA" (tier 4) can never beat the author's exact + // "Generic PLA @Vendor" (tier 5). Within one tier the strict ">" + // comparison keeps the first candidate in collection order. if (!entry.preset_name.empty() && candidate.name == entry.preset_name) return 5; if (!resolved_name.empty() && candidate.name == resolved_name) @@ -5137,8 +5142,14 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (colours != nullptr && !colours->values.empty()) seed = colours->values.front(); } - if (seed.empty()) - seed = "#F2754E"; // filament_colour default + if (seed.empty()) { + // Fall back to the option's registered default instead of a + // duplicated literal; if the lookup fails the chip stays blank. + if (const ConfigOptionDef *colour_def = print_config_def.get("filament_colour")) + if (const auto *default_colours = dynamic_cast(colour_def->default_value.get())) + if (!default_colours->values.empty()) + seed = default_colours->values.front(); + } } if (proj_colour && slot < proj_colour->values.size()) proj_colour->values[slot] = seed; @@ -5153,7 +5164,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool auto apply_slot_keys = [&](DynamicPrintConfig &preset_config, const std::vector &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('#')); + const std::string base_key = publish_base_key(key); if (structural_keys.count(base_key) != 0) continue; const ConfigOption *src_opt = config.option(base_key); @@ -5375,7 +5386,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool for (const std::string &key : published_config->published_keys) { if (applied_keys.count(key) != 0) continue; - const std::string base_key = key.substr(0, key.find('#')); + const std::string base_key = publish_base_key(key); // Structural keys are silently ignored, never reported as skipped: a hand-crafted // 3MF must not trigger the "could not be applied" warning for them. if (structural_keys.count(base_key) != 0) diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 8e2e58f5ee..e212770fd8 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -5,7 +5,6 @@ #include "PrintConfig.hpp" #include "MaterialType.hpp" -#include #include #include @@ -101,22 +100,18 @@ const std::set& publishable_printer_keys() std::vector collect_dirty_settings_keys(const PresetBundle& bundle) { - std::vector keys; - - auto append_dirty = [&keys](const std::vector& dirty) { - for (const std::string& key : dirty) { - if (std::find(keys.begin(), keys.end(), key) == keys.end()) - keys.push_back(key); - } - }; + std::set keys; // Union the dirty keys of each collection's edited preset (filaments may span multiple // slots); feeds only the Publish dialog's pre-check. - append_dirty(bundle.prints.current_dirty_options(true)); - append_dirty(bundle.printers.current_dirty_options(true)); - append_dirty(bundle.filaments.current_dirty_options(true)); + for (const std::string& key : bundle.prints.current_dirty_options(true)) + keys.insert(key); + for (const std::string& key : bundle.printers.current_dirty_options(true)) + keys.insert(key); + for (const std::string& key : bundle.filaments.current_dirty_options(true)) + keys.insert(key); - return keys; + return std::vector(keys.begin(), keys.end()); } DynamicPrintConfig filter_published_config( @@ -136,6 +131,7 @@ DynamicPrintConfig filter_published_config( std::map> slot_mask_map; // 1. Mandatory material identity & slot count keys for 3MF validation/normalization + // (filament_ids: exported for validation, denylisted on apply - see publish_structural_keys). static const std::vector s_material_identity_keys = { "filament_colour", "filament_type", @@ -163,7 +159,7 @@ DynamicPrintConfig filter_published_config( // 3. Process and printer published keys for (const std::string &key : published_keys) { - const std::string base_key = key.substr(0, key.find('#')); + const std::string base_key = publish_base_key(key); if (!base_key.empty()) { base_keys_to_include.insert(base_key); mask_exempt_keys.insert(base_key); @@ -175,7 +171,7 @@ DynamicPrintConfig filter_published_config( // slot-less entries (hand-crafted files) stay unmasked (whole vector). for (const PublishedMaterialEntry &entry : material_keys) { for (const std::string &key : entry.keys) { - const std::string base_key = key.substr(0, key.find('#')); + const std::string base_key = publish_base_key(key); if (base_key.empty()) continue; base_keys_to_include.insert(base_key); @@ -183,7 +179,7 @@ DynamicPrintConfig filter_published_config( slot_mask_map[base_key].insert(entry.slot); } for (const std::string &key : entry.full_keys) { - const std::string base_key = key.substr(0, key.find('#')); + const std::string base_key = publish_base_key(key); if (base_key.empty()) continue; base_keys_to_include.insert(base_key); diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index 0dc579c2a9..929c08dd51 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -6,8 +6,17 @@ namespace Slic3r { class PresetBundle; -// Structural keys that must never be published (single source of truth for the denylist): -// publishing them would rewrite the user's preset inheritance/structure. +// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length"). +inline std::string publish_base_key(const std::string &key) +{ + const size_t pos = key.find('#'); + return pos == std::string::npos ? key : key.substr(0, pos); +} + +// Structural keys that are never applied onto the receiver's presets when loading a published +// 3MF (single source of truth for the denylist): applying them would rewrite the user's preset +// inheritance/structure. filament_ids is nevertheless exported via the identity list in +// filter_published_config because 3MF validation needs it - exported, never applied. const std::set& publish_structural_keys(); // One row of the printer tab's "Retraction" / "Z-Hop" optgroups (key + tab icon id), kept diff --git a/src/slic3r/GUI/ConfigValueFormatter.hpp b/src/slic3r/GUI/ConfigValueFormatter.hpp index c51a8e8a17..67b16f9326 100644 --- a/src/slic3r/GUI/ConfigValueFormatter.hpp +++ b/src/slic3r/GUI/ConfigValueFormatter.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_ConfigValueFormatter_hpp_ -#define slic3r_ConfigValueFormatter_hpp_ +#pragma once #include @@ -25,5 +24,3 @@ wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConf } // namespace GUI } // namespace Slic3r - -#endif // slic3r_ConfigValueFormatter_hpp_ diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 908e4dc222..52d90623fe 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16851,7 +16851,7 @@ void publish(Model &model, SaveStrategy strategy) { } // BBS: backup -int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn, const DynamicPrintConfig* override_config) +int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) { int ret = 0; //if (p->model.objects.empty()) { @@ -16873,7 +16873,7 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // modify model publish(p->model, strategy); - DynamicPrintConfig cfg = override_config ? *override_config : wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index af599d6161..61e6bee15c 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -504,7 +504,7 @@ public: //void export_amf(); //BBS add extra param for exporting 3mf silence // BBS: backup - int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr, const DynamicPrintConfig* override_config = nullptr); + int export_3mf(const boost::filesystem::path& output_path = boost::filesystem::path(), SaveStrategy strategy = SaveStrategy::Default, int export_plate_idx = -1, Export3mfProgressFn proFn = nullptr); //BBS void publish_project(); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index cd8b82a489..08adae903b 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -332,7 +332,7 @@ void PublishSettingsDialog::build_option_model() for (const auto& opt : optgroup->opt_map()) { // Row keys are base keys; the load side applies them positionally. const std::string& opt_id = opt.first; - std::string base = opt_id.substr(0, opt_id.find('#')); + std::string base = publish_base_key(opt_id); if (!material_added.insert(base).second) continue; // Show the value of this slot; fall back to slot 0 if out of range. @@ -388,15 +388,13 @@ void PublishSettingsDialog::build_option_model() // Pre-check the dirty (modified) settings and mark them bold (base-key match, across all // sections; collect_dirty_settings_keys unions the prints, printers and filaments). std::set dirty_base; - for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) { - auto n = key.find('#'); - dirty_base.insert(n == std::string::npos ? key : key.substr(0, n)); - } + for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) + dirty_base.insert(publish_base_key(key)); for (Row& row : m_rows) { // The Color/Type requirement rows are not "dirty overrides": never auto-checked. if (row.kind != RowKind::Setting) continue; - std::string base = row.key.substr(0, row.key.find('#')); + std::string base = publish_base_key(row.key); row.dirty = dirty_base.count(base) > 0; if (row.dirty) { row.check->SetValue(true); @@ -919,7 +917,7 @@ std::vector PublishSettingsDialog::GetPublishedKeys() const // build). Publish every extruder element so the load side can apply per-extruder // values even when the receiver has a different extruder count; a scalar printer // key is published as-is. - const std::string base_key = row.key.substr(0, row.key.find('#')); + const std::string base_key = publish_base_key(row.key); if (const ConfigOption* opt = full.option(base_key)) { if (const auto* vec = dynamic_cast(opt)) { for (size_t i = 0; i < vec->size(); ++i) diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index b474baa938..312931c0c1 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_GUI_PublishSettingsDialog_hpp_ -#define slic3r_GUI_PublishSettingsDialog_hpp_ +#pragma once #include "GUI_Utils.hpp" #include "wxExtensions.hpp" @@ -200,5 +199,3 @@ private: }; }} // namespace Slic3r::GUI - -#endif // slic3r_GUI_PublishSettingsDialog_hpp_ diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index d3bf4a2f49..b68fafbc2a 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -584,6 +584,7 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the config.opt_string("print_settings_id", true) = "file process"; config.opt("flush_multiplier")->values = { 2., 2. }; // must NOT cross over config.opt("wipe_tower_x")->values = { 100. }; // plate geometry, does cross over + config.opt("wipe_tower_rotation_angle")->value = 45.; // published-only plate geometry, crosses over config.option("curr_bed_type")->setInt(BedType::btPC); // must NOT cross over return config; }; @@ -639,6 +640,7 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the CHECK(bundle.project_config.opt("flush_multiplier")->values == seed_flush_multiplier); CHECK(bundle.project_config.option("curr_bed_type")->getInt() == seed_bed_type); CHECK(bundle.project_config.opt("wipe_tower_x")->values == std::vector{ 100. }); + CHECK_THAT(bundle.project_config.opt("wipe_tower_rotation_angle")->value, Catch::Matchers::WithinAbs(45., 0.000001)); // e) The published path keeps the user's currently-selected presets: same preset, same size. CHECK(bundle.prints.get_edited_preset().name == pre_load_name); @@ -676,6 +678,9 @@ TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys PresetBundle bundle; bundle.printers.get_edited_preset().config.opt("retraction_length")->values = { 0.8 }; + // A recognizable non-default value: the skipped mismatch below must leave it untouched + // (asserting the default instead would silently test PrintConfig's retraction_speed). + bundle.printers.get_edited_preset().config.opt("retraction_speed")->values = { 33. }; bundle.printers.get_edited_preset().config.opt_string("machine_start_gcode") = "G28 ; user"; PublishedConfig pub; @@ -683,9 +688,10 @@ TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys pub.published_keys = { "retraction_length", "retraction_speed", "machine_start_gcode" }; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // Matching-size retraction vector applied; mismatched vector reported as skipped. + // Matching-size retraction vector applied; mismatched vector reported as skipped and the + // receiver's own value survives. CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 1.4 }); - CHECK(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values == std::vector{ 30. }); + CHECK(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values == std::vector{ 33. }); CHECK(contains_key(pub.skipped_keys, "retraction_speed")); // Contract-excluded printer key: silently ignored, absent from skipped_keys. CHECK(bundle.printers.get_edited_preset().config.opt_string("machine_start_gcode") == "G28 ; user"); From 36e5d4770b2cb833ed73f121b7fcd4f5ebc74476 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 21 Aug 2026 19:04:10 +0800 Subject: [PATCH 17/60] Code cleanup and fixes --- src/libslic3r/PresetBundle.cpp | 56 ++++++++++++++----- src/libslic3r/PublishSettings.cpp | 44 +++++++++------ tests/libslic3r/test_3mf.cpp | 33 +++++++++++ .../libslic3r/test_preset_bundle_loading.cpp | 41 +++++++++++++- 4 files changed, 141 insertions(+), 33 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index fffd9c7fa4..d29dbdf46f 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4834,10 +4834,26 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 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). if (key.size() > base_key.size()) { - const size_t idx = static_cast(std::atoi(key.c_str() + base_key.size() + 1)); - if (idx >= static_cast(src_opt)->size() || + // Strict numeric suffix parse: a malformed variant ("#abc", "#1x") + // must be reported as skipped, not silently applied as element 0. + const std::string suffix = key.substr(base_key.size() + 1); + size_t idx = 0; + bool valid = !suffix.empty(); + for (const char c : suffix) { + if (c < '0' || c > '9') { + valid = false; + break; + } + idx = idx * 10 + size_t(c - '0'); + if (idx > 1000000) { // overflow guard; real vector sizes are tiny + valid = false; + break; + } + } + if (!valid || + idx >= static_cast(src_opt)->size() || idx >= static_cast(dst_opt)->size()) - continue; // out-of-range variant: cannot apply; reported as skipped + continue; // malformed or out-of-range variant: cannot apply; reported as skipped } else if (static_cast(src_opt)->size() != static_cast(dst_opt)->size()) { // Whole-vector base key: the receiver must have a matching vector size, @@ -4894,15 +4910,18 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Grow the receiver's slots only as far as the highest published slot (never // shrink, never pull filler materials for unpublished slots). bool has_published_entries = false; - size_t target_slots = this->filament_presets.size(); + size_t grow_target = 0; for (const PublishedMaterialEntry &entry : published_config->material_keys) { has_published_entries = true; if (entry.slot >= 0) - target_slots = std::max(target_slots, size_t(entry.slot) + 1); + grow_target = std::max(grow_target, size_t(entry.slot) + 1); } if (has_published_entries) { - // Defensive cap: never exceed the file's own filament count. - target_slots = std::min(target_slots, num_filaments); + // Defensive cap: growth never exceeds the file's own filament count. The + // receiver's current slot count is a floor: neither the preset list nor the + // project vectors are ever shrunk, even when the file carries fewer filaments + // than the receiver has slots. + const size_t target_slots = std::max(this->filament_presets.size(), std::min(grow_target, num_filaments)); // Slots carrying published content, steering the initial preset selection of // newly grown slots. std::set published_slots; @@ -5122,13 +5141,22 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool ConfigOptionInts *proj_nozzle_map = this->project_config.opt("filament_nozzle_map"); ConfigOptionInts *proj_volume_map = this->project_config.opt("filament_volume_map"); const size_t old_colour_count = (proj_colour != nullptr) ? proj_colour->values.size() : 0; - if (proj_colour) proj_colour->resize(target_slots); - if (proj_multi_colour) proj_multi_colour->values.resize(target_slots); - if (proj_colour_type) proj_colour_type->values.resize(target_slots); - if (proj_map) proj_map->values.resize(target_slots, 1); - if (proj_nozzle_map) proj_nozzle_map->values.resize(target_slots, 0); - if (proj_volume_map) proj_volume_map->values.resize(target_slots, static_cast(NozzleVolumeType::nvtStandard)); - this->ams_multi_color_filment.resize(target_slots); + // Grow-only: an already-larger project vector is left untouched (the receiver's + // slot count never shrinks below its own setup). + if (proj_colour && proj_colour->values.size() < target_slots) + proj_colour->resize(target_slots); + if (proj_multi_colour && proj_multi_colour->values.size() < target_slots) + proj_multi_colour->values.resize(target_slots); + if (proj_colour_type && proj_colour_type->values.size() < target_slots) + proj_colour_type->values.resize(target_slots); + if (proj_map && proj_map->values.size() < target_slots) + proj_map->values.resize(target_slots, 1); + if (proj_nozzle_map && proj_nozzle_map->values.size() < target_slots) + proj_nozzle_map->values.resize(target_slots, 0); + if (proj_volume_map && proj_volume_map->values.size() < target_slots) + proj_volume_map->values.resize(target_slots, static_cast(NozzleVolumeType::nvtStandard)); + if (this->ams_multi_color_filment.size() < target_slots) + this->ams_multi_color_filment.resize(target_slots); for (size_t slot = old_colour_count; slot < target_slots; ++slot) { std::string seed; for (const PublishedMaterialEntry &entry : published_config->material_keys) diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index e212770fd8..ac25d815f1 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -5,6 +5,8 @@ #include "PrintConfig.hpp" #include "MaterialType.hpp" +#include + #include #include @@ -188,17 +190,20 @@ DynamicPrintConfig filter_published_config( } } - // Mask non-published vector slots with the option default; keys without a default stay - // unmasked (whole vector, matching partial-publish behavior). + // Masking restores every non-published slot of a vector option with the option default, so + // a partial publish does not leak unrelated slot data. can_mask_slots reports whether a key + // is maskable at all (vector option plus a registered default of the same type); an + // unmaskable key is dropped from the payload entirely instead of shipping the author's + // whole vector. + auto can_mask_slots = [](const ConfigOption &opt, const ConfigOptionDef *def) -> bool { + if (def == nullptr || !def->default_value || def->default_value->type() != opt.type()) + return false; + const auto *vec = dynamic_cast(&opt); + const auto *default_vec = dynamic_cast(def->default_value.get()); + return vec != nullptr && vec->size() > 0 && default_vec != nullptr && !default_vec->empty(); + }; auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set &keep_slots) { auto *vec = dynamic_cast(&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(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(idx)) == 0) vec->set_at(def->default_value.get(), idx, 0); @@ -206,15 +211,20 @@ DynamicPrintConfig filter_published_config( // Copy the selected options from full_config into the filtered config. for (const std::string &key : base_keys_to_include) { - if (const ConfigOption *opt = full_config.option(key)) { - ConfigOption *cloned = opt->clone(); - if (mask_exempt_keys.count(key) == 0) { - const auto it = slot_mask_map.find(key); - if (it != slot_mask_map.end() && !it->second.empty()) - mask_slots(*cloned, print_config_def.get(key), it->second); - } - filtered.set_key_value(key, cloned); + const ConfigOption *opt = full_config.option(key); + if (opt == nullptr) + continue; + const auto mask_it = slot_mask_map.find(key); + const bool needs_masking = mask_exempt_keys.count(key) == 0 && mask_it != slot_mask_map.end() && !mask_it->second.empty(); + if (needs_masking && !can_mask_slots(*opt, print_config_def.get(key))) { + BOOST_LOG_TRIVIAL(warning) << "publish: dropping unmaskable key \"" << key + << "\" from the published payload (no usable option default)"; + continue; } + ConfigOption *cloned = opt->clone(); + if (needs_masking) + mask_slots(*cloned, print_config_def.get(key), mask_it->second); + filtered.set_key_value(key, cloned); } return filtered; diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 6a20613a79..8898dae2fe 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -841,6 +841,39 @@ SCENARIO("Partial-publish entries mask the other slots like full entries", "[3mf } } +// A key needing slot masking that cannot be masked (no registered option default of the same +// type) is dropped from the payload entirely instead of shipping the author's whole vector. +SCENARIO("Unmaskable keys are dropped from the published payload instead of leaking", "[3mf]") { + GIVEN("a config carrying a synthetic def-less vector key and a maskable one") { + DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); + full_cfg.opt("filament_diameter")->values = { 1.75, 1.75 }; + full_cfg.opt("filament_colour")->values = { "#111111", "#222222" }; + // Not a PrintConfig key: print_config_def has no default to mask with. + full_cfg.set_key_value("orca_synthetic_setting", new ConfigOptionFloats({ 9.9, 8.8 })); + full_cfg.opt("filament_flow_ratio", true)->values = { 1.02, 0.98 }; + + PublishedMaterialEntry partial_entry; + partial_entry.slot = 1; + partial_entry.keys = { "orca_synthetic_setting", "filament_flow_ratio" }; + + WHEN("filtering with a partial entry for slot 1") { + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { partial_entry }); + + THEN("the unmaskable synthetic key is not published") { + REQUIRE(filtered_cfg.option("orca_synthetic_setting") == nullptr); + } + THEN("the maskable key is present, author slot kept, other slot masked") { + REQUIRE(filtered_cfg.opt("filament_flow_ratio") != nullptr); + REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[1] == 0.98); + REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[0] == 1.0); + } + THEN("the identity keys stay present") { + REQUIRE(filtered_cfg.option("filament_colour") != nullptr); + } + } + } +} + // 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 extended material metadata", "[3mf]") { diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index b68fafbc2a..3a344e0fdc 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1890,6 +1890,41 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published REQUIRE(bundle.filament_presets.size() == 3); CHECK(bundle.filament_presets[1] == "My PLA"); } + + // A receiver with more slots than the file's filament count keeps its setup: neither the + // preset list nor the project-level vectors are shrunk to the file's smaller size. + { + PresetBundle bundle; + add_pla_preset(bundle); + bundle.filament_presets = { "My PLA", "My PLA", "My PLA" }; + // Distinct project colours make a shrink observable. + bundle.project_config.opt("filament_colour")->values = { "#111111", "#222222", "#333333" }; + bundle.project_config.opt("filament_multi_colour")->values = { "#111111", "#222222", "#333333" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_color_entry(0) }; // highest published slot: 0 + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + // A one-filament file: num_filaments (1) is below the receiver's slot count (3). + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_ids")->values = { "GFL99" }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 3); + // No shrink: all three project entries survive, with slot 0 synced to the published + // colour at its unshifted index and slots 1-2 untouched. + CHECK(bundle.project_config.opt("filament_colour")->values == std::vector{ "#ABCDEF", "#222222", "#333333" }); + CHECK(bundle.project_config.opt("filament_multi_colour")->values == std::vector{ "#ABCDEF", "#222222", "#333333" }); + CHECK(bundle.project_config.opt("filament_map")->values.size() == 3); + // The published colour still reached slot 0's preset in place. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + } } // A published slot is seeded from an unused library preset and the values are written onto it @@ -2108,14 +2143,16 @@ TEST_CASE("Published 3MF rejects out-of-range vector variants and variant-suffix PublishedConfig pub; pub.published = true; - pub.published_keys = { "wiping_volumes_extruders#5", "wiping_volumes_extruders#1", "layer_height#0" }; + pub.published_keys = { "wiping_volumes_extruders#5", "wiping_volumes_extruders#1", "wiping_volumes_extruders#abc", "layer_height#0" }; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); // In-range variant applied element-wise; the out-of-range one did not resize the vector. CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 10., 150. }); CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values.size() == 2); - // Out-of-range variant and variant-suffixed scalar are reported as skipped. + // Out-of-range variant, malformed variant and variant-suffixed scalar are reported as + // skipped; the malformed one must not fall back to element 0. CHECK(contains_key(pub.skipped_keys, "wiping_volumes_extruders#5")); + CHECK(contains_key(pub.skipped_keys, "wiping_volumes_extruders#abc")); CHECK(contains_key(pub.skipped_keys, "layer_height#0")); CHECK_FALSE(contains_key(pub.skipped_keys, "wiping_volumes_extruders#1")); // The scalar was never applied. From 2aff31c07a887f04c09aa2b7c36d6fe1aa83bfd5 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 24 Aug 2026 13:19:57 +0800 Subject: [PATCH 18/60] Published 3MF: match filament slots by exported identity without a Type requirement --- src/libslic3r/PresetBundle.cpp | 115 +++++++++++++---- .../libslic3r/test_preset_bundle_loading.cpp | 119 ++++++++++++++++++ 2 files changed, 209 insertions(+), 25 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 755f2590e2..ce88c7a030 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5020,7 +5020,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool const std::string vendor = (vendors != nullptr && !vendors->values.empty()) ? vendors->get_at(0) : std::string(); if (!entry.filament_id.empty() && candidate.filament_id == entry.filament_id) return 2; - if (normalize_filament_type(type) == entry.publish_type_value) { + // Tier 0/1 family gate: the explicit type requirement when published as + // such, otherwise the entry's own material family - so identity scoring + // also applies to entries published without a checked Type row. + const std::string required_type = !entry.publish_type_value.empty() + ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + if (!required_type.empty() && normalize_filament_type(type) == required_type) { if (!entry.filament_vendor.empty() && vendor == entry.filament_vendor) return 1; return 0; @@ -5036,7 +5041,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // presets first and falling back to incompatible ones only when no // compatible candidate exists... for (const PublishedMaterialEntry &entry : published_config->material_keys) { - if (entry.slot != static_cast(new_slot_idx) || !entry.publish_type || entry.publish_type_value.empty()) + // Scored for every entry with an identity (name / ids / family), + // not only when a Type requirement was checked; candidate_score's + // family tiers fall back to the entry's own filament_type. + if (entry.slot != static_cast(new_slot_idx)) continue; const std::string resolved_name = resolved_name_for(entry.slot); int best_score = -1; @@ -5073,14 +5081,32 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx << " with " << initial_preset << " (score " << best_score << ", preset_name \"" << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; + if (best_score >= 0 && best_score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty())) + published_config->material_replacements.emplace_back( + "slot " + std::to_string(new_slot_idx) + ": " + initial_preset + + " (substitute: no exact material match)"); break; } - // ...otherwise any visible preset not already used by another slot. + // ...otherwise any visible preset not already used by another slot, + // preferring the published material's own family when it is known. if (initial_preset.empty()) { + std::string slot_family; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if (entry.slot == static_cast(new_slot_idx)) { + slot_family = !entry.publish_type_value.empty() + ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + break; + } for (size_t i = first_candidate; i < this->filaments.size(); ++i) { const Preset &candidate = this->filaments.preset(i); if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) continue; + if (!slot_family.empty()) { + const ConfigOptionStrings *types = candidate.config.opt("filament_type"); + const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + if (normalize_filament_type(cand_type) != slot_family) + continue; + } initial_preset = candidate.name; break; } @@ -5114,7 +5140,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::string replacement; int best_score = -1; for (const PublishedMaterialEntry &entry : published_config->material_keys) { - if (entry.slot != static_cast(slot) || !entry.publish_type || entry.publish_type_value.empty()) + // Scored for every entry with an identity, not only when a Type + // requirement was checked (same as the growth seeding above). + if (entry.slot != static_cast(slot)) continue; const std::string resolved_name = resolved_name_for(entry.slot); auto scan = [&](bool compatible_only) -> std::pair { @@ -5153,20 +5181,39 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; break; } - // ...otherwise any distinct visible preset not referenced by another slot. + // ...otherwise any distinct visible preset not referenced by another slot, + // preferring the published material's own family when it is known. if (replacement.empty()) { - for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); - if (candidate.is_visible && !referenced_elsewhere(candidate.name, size_t(-1))) { - replacement = candidate.name; + std::string slot_family; + for (const PublishedMaterialEntry &entry : published_config->material_keys) + if (entry.slot == static_cast(slot)) { + slot_family = !entry.publish_type_value.empty() + ? entry.publish_type_value : normalize_filament_type(entry.filament_type); break; } + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible || referenced_elsewhere(candidate.name, size_t(-1))) + continue; + if (!slot_family.empty()) { + const ConfigOptionStrings *types = candidate.config.opt("filament_type"); + const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : std::string(); + if (normalize_filament_type(cand_type) != slot_family) + continue; + } + replacement = candidate.name; + break; } } if (replacement.empty()) continue; // every visible preset is referenced: aliasing is unavoidable + const std::string aliased_name = this->filament_presets[slot]; this->filament_presets[slot] = replacement; material_applied = true; + // The re-point used to be silent; surface it like the other slot changes. + published_config->material_replacements.emplace_back( + "slot " + std::to_string(slot) + ": " + aliased_name + " -> " + replacement + + " (de-aliased: shared profile)"); } // Grow the per-slot colour/type/map project vectors to the new slot count and // seed the new entries so the slots render with colours instead of blank chips @@ -5367,25 +5414,43 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // No same-type library preset: fall back to the first available // visible preset, preferring one no other slot references, and // apply the author's full values on top of it (the dump carries - // filament_type, so the preset takes the author's type). + // filament_type, so the preset takes the author's type). A preset + // of the published material's own family is preferred overall - + // except that a referenced preset must never win just on family, + // since the dump would mutate it for every sharing slot too. std::string fallback; - for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) - continue; - if (fallback.empty()) - fallback = candidate.name; - bool referenced = false; - for (size_t s = 0; s < this->filament_presets.size(); ++s) - if (this->filament_presets[s] == candidate.name) { - referenced = true; - break; + const std::string wanted_family = !entry.publish_type_value.empty() + ? entry.publish_type_value : normalize_filament_type(entry.filament_type); + auto pick_fallback = [&](bool want_family) -> std::string { + std::string first; + for (size_t i = first_candidate; i < this->filaments.size(); ++i) { + const Preset &candidate = this->filaments.preset(i); + if (!candidate.is_visible) + continue; + if (want_family) { + if (wanted_family.empty()) + break; + const ConfigOptionStrings *cand_types = candidate.config.opt("filament_type"); + if (normalize_filament_type(cand_types != nullptr && !cand_types->values.empty() ? cand_types->get_at(0) : std::string()) != wanted_family) + continue; } - if (!referenced) { - fallback = candidate.name; - break; + bool referenced = false; + for (size_t s = 0; s < this->filament_presets.size(); ++s) + if (this->filament_presets[s] == candidate.name) { + referenced = true; + break; + } + if (!referenced) + return candidate.name; + if (first.empty()) + first = candidate.name; } - } + return first; + }; + if (!wanted_family.empty()) + fallback = pick_fallback(true); + if (fallback.empty()) + fallback = pick_fallback(false); if (!fallback.empty() && fallback != recv->name) { const std::string old_name = recv->name; this->filament_presets[slot] = fallback; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 6909b9b335..3cf5e95ce7 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -2010,6 +2010,72 @@ TEST_CASE("Published 3MF seeds published slots from unused presets and mutates t CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); } +// Without a checked Type row, a grown published slot is still seeded from the published +// material's identity - an exact filament_id outranks any arbitrary unused preset, and an +// entry carrying only a family constrains the pick to that family. +TEST_CASE("Published 3MF seeds a grown slot by published identity or family without a type requirement", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + return config; + }; + + PublishedMaterialEntry entry; + entry.slot = 2; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + // An unused preset sorting before everything else: an unconstrained pick would take it. + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &arbitrary = add_inmemory_preset(bundle.filaments, "Aaa PLA"); + arbitrary.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA" }; + + SECTION("an exact filament_id outranks the first unused preset") { + Preset &authored = add_inmemory_preset(bundle.filaments, "Zzz PLA"); + authored.config.opt_string("filament_type", 0u) = "PLA"; + authored.filament_id = "GFA00"; + entry.filament_id = "GFA00"; + + 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.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "Zzz PLA"); + // An exact identity match is not a substitute, so nothing is reported. + CHECK(pub.material_replacements.empty()); + } + + SECTION("a family-only entry picks an unused preset of that family") { + Preset &petg = add_inmemory_preset(bundle.filaments, "Bbb PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + entry.filament_type = "PETG"; + + 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.size() == 3); + CHECK(bundle.filament_presets[2] == "Bbb PETG"); + } +} + // The GUI displays the edited preset, a snapshot of the selected collection preset taken at // selection time. Since the overlay mutates the collection presets in place, the load must // re-select the first slot's filament so the applied values - and slot replacements - surface @@ -2415,6 +2481,59 @@ TEST_CASE("Published 3MF gives each published slot its own preset on an aliased CHECK(pub.skipped_keys.empty()); } +// De-aliasing runs on the exported identity even without a checked Type row: the re-pointed +// slot lands on the exact published material (by filament_id) rather than an arbitrary spare, +// and the formerly silent re-point is surfaced through the replacements notification list. +TEST_CASE("Published 3MF de-aliases an aliased slot by published identity without a type requirement", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99" }; + config.option("filament_retraction_length", true)->values = { 0.6, 0.9 }; + + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + mine.config.opt("filament_retraction_length", true)->values = { 0.5 }; + // A spare sorting before the exact match: an unconstrained pick would take it. + Preset &spare = add_inmemory_preset(bundle.filaments, "Aaa PLA"); + spare.config.opt_string("filament_type", 0u) = "PLA"; + spare.config.opt("filament_retraction_length", true)->values = { 0.5 }; + Preset &match = add_inmemory_preset(bundle.filaments, "Zzz PLA"); + match.config.opt_string("filament_type", 0u) = "PLA"; + match.config.opt("filament_retraction_length", true)->values = { 0.5 }; + match.filament_id = "GFA00"; + bundle.filament_presets = { "My PLA", "My PLA" }; + + PublishedMaterialEntry entry; + entry.slot = 1; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.filament_id = "GFA00"; + entry.keys = { "filament_retraction_length" }; + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 2); + CHECK(bundle.filament_presets[0] == "My PLA"); + CHECK(bundle.filament_presets[1] == "Zzz PLA"); + // The published key was written onto the re-pointed slot's own preset. + Preset *target = bundle.filaments.find_preset("Zzz PLA", false, true); + REQUIRE(target != nullptr); + CHECK(target->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0].find("(de-aliased") != std::string::npos); + CHECK(pub.skipped_keys.empty()); +} + // Printer retraction keys are published per-extruder ("#N"): a receiver with a different // extruder count still receives the in-range elements; out-of-range variants are reported as // skipped instead of corrupting the receiver's vector. From 52aa5a52e9e2943485195d3408793cf2220e4a58 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 12:27:45 +0800 Subject: [PATCH 19/60] Published 3MF: import Full Publish materials as standalone detached presets inside the project --- src/libslic3r/Preset.cpp | 75 ++++++++++++++++++++ src/libslic3r/Preset.hpp | 18 +++++ src/libslic3r/PresetBundle.cpp | 111 ++++++++++++++++++++++++++++++ src/libslic3r/PublishSettings.cpp | 33 +++++++++ src/libslic3r/PublishSettings.hpp | 30 +++++--- 5 files changed, 259 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 1334bd4e7a..c422253dcd 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3053,6 +3053,81 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det this->get_selected_preset().save(nullptr); } +// A detached standalone preset for the Full Publish receiver: create a user preset holding +// the full resolved filament config (no inheritance, no vendor/alias links), parentless and +// universally compatible. Mirrors save_current_preset(detach=true)'s creation branch but +// does not force-select or diff against a parent; the caller decides whether to select it. +// The published entry's filament_id is forwarded so user bases keep their stable +// material grouping (get_filament_presets() groups user bases by filament_id). +// save_to_project=true (the Full Publish default) creates a project-embedded preset: +// it lives inside the loaded project only (serialized into the saved .3mf, restored by +// load_project_embedded_presets) and never touches the user's library directory; +// Preset::save() early-returns for embedded presets, so persistence is skipped here too. +// Returns the final (uniquified) name; on collision "" -> " (Published)" -> +// " (Published 2)" ... +std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config, + const std::string &filament_id, bool save_to_project) +{ + if (name_base.empty()) + return std::string(); + Preset stored(m_type, name_base); + stored.config = std::move(config); + stored.filament_id = filament_id; + + // Uniquify verbatim; only on collision append " (Published)" then " (Published 2)". + const std::string base_name = name_base; + std::string final_name = base_name; + auto exists = [this](const std::string &candidate) -> bool { + const auto it = this->find_preset_internal(candidate); + return it != m_presets.end() && it->name == candidate; + }; + if (exists(final_name)) { + final_name = base_name + " (Published)"; + for (int i = 2; exists(final_name); ++i) + final_name = base_name + " (Published " + std::to_string(i) + ")"; + } + + // Creation branch of save_current_preset(detach=true), without its selection side + // effects or project-embedded path. + lock(); + const auto it = this->find_preset_internal(final_name); + Preset &preset = *m_presets.insert(it, stored); + stored.name.clear(); // avoid stale copied name being used below + stored.config.clear(); + preset.name = final_name; + preset.vendor = nullptr; + preset.alias.clear(); + preset.renamed_from.clear(); + preset.m_excluded_from.clear(); + preset.setting_id.clear(); + preset.inherits().clear(); + preset.version = Semver::parse(SoftFever_VERSION) ? *Semver::parse(SoftFever_VERSION) : Semver(); + preset.is_default = false; + preset.is_system = false; + preset.is_external = false; + preset.bundle_id.clear(); + preset.file = this->path_for_preset(preset); + preset.is_visible = true; + preset.is_project_embedded = save_to_project; + if (m_type == Preset::TYPE_PRINT) + preset.config.option("print_settings_id", true)->value = final_name; + else if (m_type == Preset::TYPE_FILAMENT) + preset.config.option("filament_settings_id", true)->values[0] = final_name; + else if (m_type == Preset::TYPE_PRINTER) + preset.config.option("printer_settings_id", true)->value = final_name; + unlock(); + + if (!save_to_project) { + // Persist the full resolved config (no parent). Project-embedded presets are + // serialized into the .3mf instead; Preset::save() would early-return anyway. + // find by final_name — m_presets may have reallocated, so don't keep a raw ref. + auto persist_it = this->find_preset_internal(final_name); + if (persist_it != m_presets.end() && persist_it->name == final_name) + persist_it->save(nullptr); + } + return final_name; +} + bool PresetCollection::delete_current_preset() { Preset &selected = this->get_selected_preset(); diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index c9b3197a6f..e904851a1d 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -631,6 +631,24 @@ public: // All presets are marked as not modified and the new preset is activated. //BBS: add project embedded preset logic void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr); + // Insert a standalone user preset holding the full resolved config (no inheritance, + // no vendor links): the libslic3r equivalent of "Detach from parent". Takes a + // resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id. + // Unlike save_current_preset it does not force-select or diff against a parent. + // Used by the published-3MF Full Publish path. The optional filament_id seeds the + // preset's stable material grouping (get_filament_presets groups user bases by + // filament_id); the published entry's filament_id is forwarded so the copy keeps + // the author's grouping. + // With save_to_project=true (default) the copy is a project-embedded preset + // ("Preset Inside Project"): it lives inside the loaded project only, is serialized + // into the saved .3mf via get_current_project_embedded_presets(), and is never + // written to the user's library directory. With false it persists as a normal + // user preset file. + // Returns the final (uniquified) name; on collision the suffix rule is: + // "" -> " (Published)" -> " (Published 2)" ... + std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config, + const std::string &filament_id = std::string(), + bool save_to_project = true); // Delete the current preset, activate the first visible preset. // returns true if the preset was deleted successfully. diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index ce88c7a030..96373eae7c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5312,6 +5312,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // slots wrote to it; that compound case is not chased.) const bool edited_survives_load = this->filament_presets.empty() || this->filament_presets.front() == this->filaments.get_edited_preset().name; + // Full Publish within-load dedup: identical Full materials (same setting_id + // + preset_name identity) share one created instance, so an author who + // pointed two slots at one preset yields one standalone copy here. + std::map published_full_dedup; for (const PublishedMaterialEntry &entry : published_config->material_keys) { if (entry.slot < 0 || size_t(entry.slot) >= this->filament_presets.size()) continue; // out of range: nothing to do for this slot @@ -5327,6 +5331,113 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool ? (entry.publish_type_value.empty() ? entry.filament_type : entry.publish_type_value) : entry.filament_id; + // Full Publish: always create a standalone detached copy (even on exact + // identity match) as a "Preset Inside Project" (project-embedded: lives + // in this project only, never written to the library), universally + // compatible, named after the author's preset with its variant tail + // stripped ("(Published)" uniquification on collision). No existing + // preset is ever mutated. + if (entry.full) { + std::string dedup_key = entry.setting_id + std::string("\x1f") + entry.preset_name; + // Identity-less hand-crafted files (empty setting_id+name) must not + // collide: fall back to slot-scoped key so each slot gets its own copy + // unless the dedup above is meaningful. + if (dedup_key == std::string("\x1f")) + dedup_key = dedup_key + std::to_string(slot); + else if (!entry.filament_id.empty()) + dedup_key += std::string("\x1f") + entry.filament_id; + std::string new_name; + const auto dedup_it = published_full_dedup.find(dedup_key); + if (dedup_it != published_full_dedup.end()) { + new_name = dedup_it->second; + } else { + // Baseline: clone the receiver slot's stored preset config (schema- + // complete across versions), then overlay the published full_keys. + DynamicPrintConfig new_cfg = recv != nullptr + ? recv->config : this->filaments.default_preset().config; + for (const std::string &key : entry.full_keys) { + const std::string base_key = publish_base_key(key); + if (structural_keys.count(base_key) != 0) + continue; + const ConfigOption *src_opt = config.option(base_key); + if (src_opt == nullptr || !src_opt->is_vector() || + entry.slot < 0 || + entry.slot >= static_cast(static_cast(src_opt)->size())) + continue; + ConfigOption *dst_opt = new_cfg.option(base_key); + if (dst_opt == nullptr || !dst_opt->is_vector() || + static_cast(dst_opt)->empty() || + dst_opt->type() != src_opt->type()) + continue; + static_cast(dst_opt)->set_at(src_opt, 0, entry.slot); + } + // The published colour is authoritative even for Full (the payload's + // filament_colour plus the explicit publish_color field). + if (entry.publish_color && !entry.color.empty()) { + if (ConfigOptionStrings *col = new_cfg.opt("filament_colour", true)) { + if (col->values.empty()) + col->values.emplace_back(); + col->values[0] = entry.color; + } + } + make_publish_universal(new_cfg); + // Naming: stripped variant tail ("Generic PLA @System" -> "Generic + // PLA"), then identity fallbacks; collisions uniquify with + // "(Published)" / "(Published N)" inside add_detached_preset. + std::string base_name = entry.preset_name.empty() ? std::string() : publish_material_base_name(entry.preset_name); + if (base_name.empty()) { + base_name = !entry.filament_id.empty() ? entry.filament_id + : (!entry.publish_type_value.empty() ? entry.publish_type_value : entry.filament_type); + if (base_name.empty()) + base_name = "Published Filament"; + } + new_name = this->filaments.add_detached_preset(base_name, std::move(new_cfg), entry.filament_id); + published_full_dedup.emplace(dedup_key, new_name); + } + const std::string old_name = this->filament_presets[slot]; + this->filament_presets[slot] = new_name; + material_applied = true; + published_config->material_replacements.emplace_back( + "slot " + std::to_string(slot) + ": " + old_name + " -> " + new_name + + " (published material imported)"); + // Colour is slot-scoped and project-visible: sync into project_config + // (the copy already baked it, this makes the chips render). + if (entry.publish_color && !entry.color.empty()) { + if (ConfigOptionStrings *proj_colour = this->project_config.opt("filament_colour")) { + if (slot < proj_colour->values.size()) + proj_colour->values[slot] = entry.color; + } + if (ConfigOptionStrings *proj_multi_colour = this->project_config.opt("filament_multi_colour")) { + if (slot < proj_multi_colour->values.size()) + proj_multi_colour->values[slot] = entry.color; + } + } else if (proj_colour && new_name != old_name) { + // Fall back to the copy's colour so the chip is never blank: try + // the newly created preset's filament_colour, then the option default. + std::string seed; + if (const Preset *created = this->filaments.find_preset(new_name, false, true)) { + if (const ConfigOptionStrings *cols = created->config.opt("filament_colour")) + if (!cols->values.empty()) + seed = cols->values.front(); + } + if (seed.empty()) { + if (const ConfigOptionDef *colour_def = print_config_def.get("filament_colour")) + if (const auto *defaults = dynamic_cast(colour_def->default_value.get())) + if (!defaults->values.empty()) + seed = defaults->values.front(); + } + if (!seed.empty()) { + if (proj_colour && slot < proj_colour->values.size()) + proj_colour->values[slot] = seed; + if (proj_multi_colour && slot < proj_multi_colour->values.size()) + proj_multi_colour->values[slot] = seed; + } + } + if (proj_colour_type && slot < proj_colour_type->values.size()) + proj_colour_type->values[slot] = "1"; + continue; + } + bool apply_slot = true; // The gate compares against the slot's effective material type: the edited // layer when the slot references the collection's edited preset and that diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index ac25d815f1..3a3d241e81 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -6,12 +6,19 @@ #include "MaterialType.hpp" #include +#include #include #include namespace Slic3r { +std::string publish_base_key(const std::string &key) +{ + const size_t pos = key.find('#'); + return pos == std::string::npos ? key : key.substr(0, pos); +} + std::string normalize_filament_type(const std::string& type) { if (type.empty()) @@ -29,6 +36,32 @@ std::string normalize_filament_type(const std::string& type) return type; } +void make_publish_universal(DynamicPrintConfig &config) +{ + // Lists: empty => compatible with every printer / every print preset. Conditions: + // empty so a leftover expression left behind by the baseline clone can never + // re-narrow the match (see is_compatible_with_printer, Preset.cpp:840). All four + // keys exist on filament presets; nil-guard for hand-crafted future schemas. + if (auto *opt = config.opt("compatible_printers", false)) + opt->values.clear(); + if (auto *opt = config.opt("compatible_prints", false)) + opt->values.clear(); + if (auto *opt = config.opt("compatible_printers_condition", false)) + opt->value.clear(); + if (auto *opt = config.opt("compatible_prints_condition", false)) + opt->value.clear(); +} + +std::string publish_material_base_name(const std::string &preset_name) +{ + if (preset_name.empty()) + return preset_name; + const size_t at = preset_name.find('@'); + std::string base = (at == std::string::npos) ? preset_name : preset_name.substr(0, at); + boost::trim_right(base); + return base; +} + const std::set& publish_structural_keys() { // Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index 929c08dd51..bcb3397dcc 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -7,11 +7,7 @@ namespace Slic3r { class PresetBundle; // Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length"). -inline std::string publish_base_key(const std::string &key) -{ - const size_t pos = key.find('#'); - return pos == std::string::npos ? key : key.substr(0, pos); -} +std::string publish_base_key(const std::string &key); // Structural keys that are never applied onto the receiver's presets when loading a published // 3MF (single source of truth for the denylist): applying them would rewrite the user's preset @@ -56,14 +52,20 @@ struct PublishedMaterialEntry { // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector keys; - // "Full Publish": serialize the whole filament preset (full_keys); the type gate then - // decides whether the receiver keeps its material (type match) or is replaced. + // "Full Publish": the whole filament preset (full_keys) is published. On the receiver + // Full Publish always creates a standalone parentless copy (libslic3r's "Detach from + // parent"): a new project-embedded preset ("Preset Inside Project") with the full + // resolved config, universally compatible (compatible_printers/condition cleared). + // It lives inside the loaded project only - never written to the user's library, + // no existing preset is ever selected-by-reference or mutated. Identical Full + // entries inside one load share one created instance (within-load dedup). bool full{false}; // All non-structural filament keys of the author's slot preset; values travel in the file // config, masked to the author's slot index. std::vector full_keys; // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on // mismatch the slot is replaced with a same-type filament from the receiver's library. + // For Full entries the baked filament_type on the created copy satisfies the type gate. bool publish_type{false}; std::string publish_type_value; // Required filament colour, applied on load regardless of the type match. @@ -74,9 +76,21 @@ struct PublishedMaterialEntry { // "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact. std::string normalize_filament_type(const std::string& type); +class DynamicPrintConfig; +// Clear the compatibility lists/conditions on a filament config so it is compatible +// with every printer and every print profile. A detached published material is +// universally compatible by construction: the baseline clone may carry machine-specific +// restrictions. Empty lists + empty conditions => compatible with everything +// (see is_compatible_with_printer, Preset.cpp:840). +void make_publish_universal(DynamicPrintConfig &config); + +// Naming base for a detached published-material copy: "Generic PLA @System" -> +// "Generic PLA" (truncate at the first '@' variant tail, right-trimmed). Unchanged +// when the name carries no '@'. Empty result means "fall back to identity fields". +std::string publish_material_base_name(const std::string &preset_name); + // Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys, // material keys, identity fields and plate geometry keys. -class DynamicPrintConfig; DynamicPrintConfig filter_published_config( const DynamicPrintConfig &full_config, const std::vector &published_keys, From 0ba7bae79490d08d6b9093bb5423033efc807220 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 13:18:00 +0800 Subject: [PATCH 20/60] Fix conflicts. Update unit tests --- src/slic3r/GUI/Widgets/Button.hpp | 2 - .../libslic3r/test_preset_bundle_loading.cpp | 819 +++++++----------- 2 files changed, 318 insertions(+), 503 deletions(-) diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 9b97d0eb86..9ad0810804 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -46,8 +46,6 @@ class Button : public StaticBox bool isCenter = true; bool vertical = false; - wxTipWindow* tipWindow = nullptr; - static const int buttonWidth = 200; static const int buttonHeight = 50; diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 3cf5e95ce7..ba92f43943 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -795,12 +795,10 @@ TEST_CASE("Published 3MF applies positional material keys onto the receiver's ma CHECK(pub.skipped_keys.empty()); } -// A "full publish" slot serializes the whole filament. On load the slot is matched positionally -// against the published type: a matching receiver type still receives the author's full values -// (like a normal save/load of the filament), a mismatched type replaces it with the first -// same-type visible preset (applying the author's full values on top), and a type not in the -// receiver's library falls back to the first available visible preset. -TEST_CASE("Published 3MF full-published slots replace or ignore the receiver material by type", "[Preset][Bundle][Published]") +// A "full publish" slot serializes the whole filament. On load the slot always receives a +// standalone detached copy of the author's material - created even when the receiver's own +// material matches the published type - and no receiver library preset is ever mutated. +TEST_CASE("Published 3MF full-published slots are imported as standalone detached copies", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -826,7 +824,7 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat return entry; }; - SECTION("type match applies the full dump onto the receiver's material") { + SECTION("type match still creates a detached copy instead of mutating the receiver material") { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.config.opt_string("filament_type", 0u) = "PLA"; @@ -843,14 +841,20 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // The type matches, but a full publish behaves like a normal save: the author's values - // are written onto the slot's preset wholesale. - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // The type matches, but the import still detaches: the slot lands on a fresh copy + // named from the published type (no identity fields in this entry), carrying the + // author's values; the receiver's own material is untouched. + CHECK(bundle.filament_presets[0] == "PLA"); + Preset *copy = bundle.filaments.find_preset("PLA", false, true); + REQUIRE(copy != nullptr); + CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); CHECK(pub.skipped_keys.empty()); - CHECK(pub.material_replacements.empty()); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PLA -> PLA (published material imported)"); } - SECTION("type mismatch replaces the slot with the first same-type preset and applies the full dump") { + SECTION("type mismatch also detaches: a fresh same-type copy replaces the slot") { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.config.opt_string("filament_type", 0u) = "PLA"; @@ -868,19 +872,20 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 1); - // The slot is re-pointed at the library's ABS preset and the author's full values are - // written onto it in place (the original 0.3 is overwritten); the receiver's own - // material is untouched. - CHECK(bundle.filament_presets[0] == "My ABS"); - CHECK(bundle.filaments.find_preset("My ABS", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // No substitution search runs: a brand-new copy named after the published type is + // created and both pre-existing presets stay exactly as they were. + CHECK(bundle.filament_presets[0] == "ABS"); + Preset *copy = bundle.filaments.find_preset("ABS", false, true); + REQUIRE(copy != nullptr); + CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("My ABS", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.3 }); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); - // The entry carries no identity fields, so the pick cannot be judged as a substitute. - CHECK(pub.material_replacements[0] == "slot 0: My PLA -> My ABS"); + CHECK(pub.material_replacements[0] == "slot 0: My PLA -> ABS (published material imported)"); } - SECTION("no same-type match falls back to the first available visible preset") { + SECTION("the author's identity rides on the copy when the type has no library match") { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); pla.config.opt_string("filament_type", 0u) = "PLA"; @@ -891,8 +896,8 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat bundle.filament_presets = { "My PLA" }; PublishedMaterialEntry full = make_full_abs_entry(); - // The dump carries the identity too, so the fallback preset must take the author's type - // and vendor. + // The dump carries the identity too, so the created copy takes the author's type + // and vendor instead of the baseline clone's. full.full_keys = { "filament_retraction_length", "filament_type", "filament_vendor" }; PublishedConfig pub; @@ -904,97 +909,27 @@ TEST_CASE("Published 3MF full-published slots replace or ignore the receiver mat Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // No ABS in the library: the slot falls back to the first available visible preset (the - // unused "Other PLA") and the author's full values are written onto it, type and vendor - // included. The receiver's own material is untouched. - CHECK(bundle.filament_presets[0] == "Other PLA"); - Preset *fallback = bundle.filaments.find_preset("Other PLA", false, true); - REQUIRE(fallback != nullptr); - CHECK(fallback->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(fallback->config.opt_string("filament_type", 0u) == "ABS"); - CHECK(fallback->config.opt_string("filament_vendor", 0u) == "Generic"); + // No ABS preset needs to exist in the library: the copy carries the author's values, + // type and vendor included. Neither receiver preset was touched. + CHECK(bundle.filament_presets[0] == "ABS"); + Preset *copy = bundle.filaments.find_preset("ABS", false, true); + REQUIRE(copy != nullptr); + CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(copy->config.opt_string("filament_type", 0u) == "ABS"); + CHECK(copy->config.opt_string("filament_vendor", 0u) == "Generic"); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt_string("filament_type", 0u) == "PLA"); + CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PLA -> Other PLA (substitute: no ABS available)"); + CHECK(pub.material_replacements[0] == "slot 0: My PLA -> ABS (published material imported)"); } } -// Regression for the author's published material being skipped by a type-only replacement -// search: the slot must prefer the exact published material (filament_id) over the first other -// same-type preset, even when the exact preset is already referenced by another slot. -TEST_CASE("Published 3MF replaces a mismatched slot with the exact published material when available", "[Preset][Bundle][Published]") -{ - auto make_file_config = [] { - DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - config.opt("filament_diameter")->values = { 1.75 }; - config.opt("filament_self_index")->values = { 1 }; - config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; - config.opt("filament_colour")->values = { "#FF0000" }; - config.opt("filament_type")->values = { "PLA" }; - config.opt("filament_vendor")->values = { "Generic" }; - config.opt("filament_ids")->values = { "GFL99" }; - config.option("filament_retraction_length", true)->values = { 0.9 }; - return config; - }; - - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // The receiver's second slot already uses the exact published material. - Preset &generic_pla = add_inmemory_preset(bundle.filaments, "Generic PLA"); - generic_pla.filament_id = "GFL99"; - generic_pla.config.opt_string("filament_type", 0u) = "PLA"; - generic_pla.config.opt_string("filament_vendor", 0u) = "Generic"; - generic_pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; - // An unrelated PLA, unreferenced: a type-only search picks it because Generic PLA is - // referenced by slot 1. - Preset &bambu = add_inmemory_preset(bundle.filaments, "Bambu PLA Basic"); - bambu.filament_id = "GFB00"; - bambu.config.opt_string("filament_type", 0u) = "PLA"; - bambu.config.opt_string("filament_vendor", 0u) = "Bambu Lab"; - bambu.config.opt("filament_retraction_length", true)->values = { 0.4 }; - bundle.filament_presets = { "My PETG", "Generic PLA" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.filament_id = "GFL99"; - entry.filament_vendor = "Generic"; - entry.full_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 exact published material (id GFL99) wins over the unreferenced type-only preset. - CHECK(bundle.filament_presets[0] == "Generic PLA"); - CHECK(bundle.filament_presets[1] == "Generic PLA"); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - // The receiver's own material is untouched; the unrelated PLA too. - CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); - CHECK(bundle.filaments.find_preset("Bambu PLA Basic", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.4 }); - // Accepted mutate tradeoff: the shared exact-material preset was mutated, so slot 1 also - // carries the author's values (the leak is documented, not accidental). - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - // An exact-material pick is reported without a substitute qualifier. - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA"); - CHECK(pub.skipped_keys.empty()); -} - -// The author's preset name travels in the file, so a type mismatch resolves to the exact -// published material even when the identity fields are absent or stale (older files): a -// "Generic PLA" author must land on the receiver's "Generic PLA", never on a same-type -// substitute like "Bambu PLA Basic". -TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by name", "[Preset][Bundle][Published]") +// The author's preset name travels in the file and names the created standalone copy (variant +// tail stripped), regardless of what the receiver's library holds: an exact-name library preset +// is never reused nor mutated. +TEST_CASE("Published 3MF imports a full material under the author's stripped name", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -1019,7 +954,7 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by nam return &preset; }; - SECTION("full identity (name, setting_id, filament_id): the name match wins") { + SECTION("an exact-name library preset exists: a detached copy is created beside it") { PresetBundle bundle; Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); petg.config.opt_string("filament_type", 0u) = "PETG"; @@ -1046,15 +981,18 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by nam Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - CHECK(bundle.filament_presets[0] == "Generic PLA @System"); - CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // The copy is named after the stripped author name; the receiver's exact-name preset + // keeps its own values. + CHECK(bundle.filament_presets[0] == "Generic PLA"); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (published material imported)"); CHECK(pub.skipped_keys.empty()); } - SECTION("only the name is present (broken/stale ids): still the exact preset") { + SECTION("only the name is present (broken/stale ids): the copy is still created") { PresetBundle bundle; Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); petg.config.opt_string("filament_type", 0u) = "PETG"; @@ -1078,43 +1016,14 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by nam Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - CHECK(bundle.filament_presets[0] == "Generic PLA @System"); + CHECK(bundle.filament_presets[0] == "Generic PLA"); CHECK(bundle.filaments.find_preset("Bambu PLA Basic @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (published material imported)"); CHECK(pub.skipped_keys.empty()); } - SECTION("the exact preset is hidden in the library: the exact match still wins") { - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - Preset &generic_pla = *add_pla(bundle, "Generic PLA @System", "OGFL99", "Generic", "RcBNzytWgwRrwXXz"); - generic_pla.is_visible = false; - add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); - bundle.filament_presets = { "My PETG" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @System"; - entry.full_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); - - CHECK(bundle.filament_presets[0] == "Generic PLA @System"); - CHECK(pub.skipped_keys.empty()); - } - - SECTION("grown slot (author slot 1) is seeded with the exact preset by name") { + SECTION("grown slot (author slot 1) receives its own detached copy") { auto two_slot_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.opt("filament_diameter")->values = { 1.75, 1.75 }; @@ -1153,53 +1062,27 @@ TEST_CASE("Published 3MF replaces a mismatched slot with the exact preset by nam REQUIRE(bundle.filament_presets.size() == 2); CHECK(bundle.filament_presets[0] == "My PETG"); - CHECK(bundle.filament_presets[1] == "Generic PLA @System"); - // The full dump applies the author's slot-1 value onto the grown slot's preset. - CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); - CHECK(pub.skipped_keys.empty()); - } - - SECTION("no exact preset in the library: falls back to the substitute") { - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_pla(bundle, "Bambu PLA Basic @System", "OGFA00", "Bambu Lab", "zkc85XTKi4cb6cOw"); - bundle.filament_presets = { "My PETG" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @System"; - entry.filament_id = "OGFL99"; - entry.full_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); - - CHECK(bundle.filament_presets[0] == "Bambu PLA Basic @System"); + // The grown slot lands on a freshly created copy carrying the author's slot-1 value; + // the library preset that seeded it stays untouched. + CHECK(bundle.filament_presets[1] == "Generic PLA"); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Bambu PLA Basic @System (substitute: no exact material match)"); + CHECK(pub.material_replacements[0] == "slot 1: Generic PLA @System -> Generic PLA (published material imported)"); + CHECK(pub.skipped_keys.empty()); } } -// The receiver library can hold several presets that all look like "Generic PLA": a bare-named -// legacy copy, the author's exact vendor preset and the Orca library preset (whose alias is the -// bare name). The exact preset name must outrank the fuzzy bare/alias tier, so the author's -// "Generic PLA @Qidi Q2 0.4 nozzle" wins regardless of collection order. -TEST_CASE("Published 3MF prefers the exact preset over same-bare-name presets", "[Preset][Bundle][Published]") +// The stripped author name can collide with an existing library preset ("Generic PLA"): the +// created copy must uniquify with the "(Published)" suffix rather than overwrite, reuse or +// mutate any of the receiver's own presets. +TEST_CASE("Published 3MF uniquifies an imported full material name on collision", "[Preset][Bundle][Published]") { PresetBundle bundle; Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); petg.config.opt_string("filament_type", 0u) = "PETG"; petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // A legacy bundle preset literally named "Generic PLA": bare-name match, sorts first. + // A legacy bundle preset literally named "Generic PLA" - collides with the stripped name. Preset &bare = add_inmemory_preset(bundle.filaments, "Generic PLA"); bare.config.opt_string("filament_type", 0u) = "PLA"; bare.config.opt_string("filament_vendor", 0u) = "Generic"; @@ -1209,7 +1092,7 @@ TEST_CASE("Published 3MF prefers the exact preset over same-bare-name presets", qidi.config.opt_string("filament_type", 0u) = "PLA"; qidi.config.opt_string("filament_vendor", 0u) = "Generic"; qidi.config.opt("filament_retraction_length", true)->values = { 0.5 }; - // The Orca library preset: alias "Generic PLA" matches the bare-name tier too. + // The Orca library preset. Preset &sys = add_inmemory_preset(bundle.filaments, "Generic PLA @System"); sys.config.opt_string("filament_type", 0u) = "PLA"; sys.config.opt_string("filament_vendor", 0u) = "Generic"; @@ -1231,103 +1114,15 @@ TEST_CASE("Published 3MF prefers the exact preset over same-bare-name presets", Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // The exact preset beats the bare/alias matches even though "Generic PLA" sorts first. - CHECK(bundle.filament_presets[0] == "Generic PLA @Qidi Q2 0.4 nozzle"); + // The copy lands beside the collision, suffixed; every pre-existing preset keeps its own + // values. + CHECK(bundle.filament_presets[0] == "Generic PLA (Published)"); + CHECK(bundle.filaments.find_preset("Generic PLA (Published)", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filaments.find_preset("Generic PLA @Qidi Q2 0.4 nozzle", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @Qidi Q2 0.4 nozzle"); - CHECK(pub.skipped_keys.empty()); -} - -// The author's exact preset exists on the receiver but is incompatible with the active printer -// (a Qidi bundle preset on a non-Qidi printer); a same-family preset that IS compatible must -// win instead, so the slot never ends up with a filament the printer cannot use. -TEST_CASE("Published 3MF prefers a compatible preset over an exact but incompatible one", "[Preset][Bundle][Published]") -{ - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // The bare-named legacy preset (Bambu-printer only) and the exact author preset are both - // incompatible with the receiver's printer. update_compatible() recomputes is_compatible - // inside load_config_model, so the incompatibility is expressed the way production derives - // it: a compatible_printers constraint no receiver printer satisfies (the fresh bundle's - // active printer is the "Default Printer" placeholder). - Preset &bare = add_inmemory_preset(bundle.filaments, "Generic PLA"); - bare.config.opt_string("filament_type", 0u) = "PLA"; - bare.config.opt_string("filament_vendor", 0u) = "Generic"; - bare.config.opt("filament_retraction_length", true)->values = { 0.5 }; - bare.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); - Preset &qidi = add_inmemory_preset(bundle.filaments, "Generic PLA @Qidi Q2 0.4 nozzle"); - qidi.config.opt_string("filament_type", 0u) = "PLA"; - qidi.config.opt_string("filament_vendor", 0u) = "Generic"; - qidi.config.opt("filament_retraction_length", true)->values = { 0.5 }; - qidi.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); - // The receiver's own compatible library preset. - Preset &sys = add_inmemory_preset(bundle.filaments, "Generic PLA @System"); - sys.config.opt_string("filament_type", 0u) = "PLA"; - sys.config.opt_string("filament_vendor", 0u) = "Generic"; - sys.config.opt("filament_retraction_length", true)->values = { 0.5 }; - bundle.filament_presets = { "My PETG" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @Qidi Q2 0.4 nozzle"; - entry.full_keys = { "filament_retraction_length" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { entry }; - DynamicPrintConfig config = published_pla_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filament_presets[0] == "Generic PLA @System"); - // A same-family name-tier pick is not reported as a substitute. - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); - CHECK(pub.skipped_keys.empty()); -} - -// When every candidate is incompatible with the receiver's printer, the search still falls back -// to the best (exact) match rather than leaving the slot on the mismatched type. -TEST_CASE("Published 3MF falls back to an incompatible exact preset when no compatible candidate exists", "[Preset][Bundle][Published]") -{ - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // The exact author preset is incompatible with the receiver's printer. update_compatible() - // recomputes is_compatible inside load_config_model, so the incompatibility is expressed the - // way production derives it: a compatible_printers constraint the receiver's printer (the - // fresh bundle's "Default Printer" placeholder) does not satisfy. - Preset &qidi = add_inmemory_preset(bundle.filaments, "Generic PLA @Qidi Q2 0.4 nozzle"); - qidi.config.opt_string("filament_type", 0u) = "PLA"; - qidi.config.opt_string("filament_vendor", 0u) = "Generic"; - qidi.config.opt("filament_retraction_length", true)->values = { 0.5 }; - qidi.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); - bundle.filament_presets = { "My PETG" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @Qidi Q2 0.4 nozzle"; - entry.full_keys = { "filament_retraction_length" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { entry }; - DynamicPrintConfig config = published_pla_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filament_presets[0] == "Generic PLA @Qidi Q2 0.4 nozzle"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @Qidi Q2 0.4 nozzle"); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (Published) (published material imported)"); CHECK(pub.skipped_keys.empty()); } @@ -1393,35 +1188,46 @@ TEST_CASE("Published 3MF writes to the stored preset when the edited layer is re CHECK(pub.skipped_keys.empty()); } -// The author published "Generic PLA @System"; the receiver's copy "Generic PLA" drops the -// "@System" suffix (and has no setting_id, a different filament_id), so only the trimmed -// bare-name tier can reach it - the truncated form carries trailing whitespace that must not -// defeat the match. -TEST_CASE("Published 3MF name matching accepts suffix-less receiver presets", "[Preset][Bundle][Published]") +// The created copy is named after the author's preset with the "@variant" tail stripped; +// trailing whitespace left behind by the truncation must be trimmed away, and names without +// a tail pass through unchanged. +TEST_CASE("publish_material_base_name strips the variant tail from a published preset name", "[Preset][Bundle][Published]") +{ + CHECK(publish_material_base_name("Generic PLA @System") == "Generic PLA"); + CHECK(publish_material_base_name("Generic PLA @Qidi Q2 0.4 nozzle") == "Generic PLA"); + // Truncation at '@' leaves the space before the tail; it must not survive. + CHECK(publish_material_base_name("Generic PLA @System") == "Generic PLA"); + CHECK(publish_material_base_name("Voron Generic PLA") == "Voron Generic PLA"); + CHECK(publish_material_base_name("") == ""); + // A tail-only name strips to nothing; the caller falls back to identity fields. + CHECK(publish_material_base_name("@System").empty()); +} + +// A full-published material arrives as a brand-new standalone preset: parentless, visible, +// project-embedded ("Preset Inside Project"), carrying the author's values and colour - and +// never touching any of the receiver's own presets. +TEST_CASE("Published 3MF imports a full material as a detached project-embedded preset", "[Preset][Bundle][Published]") { PresetBundle bundle; Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); petg.config.opt_string("filament_type", 0u) = "PETG"; petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - Preset &plain = add_inmemory_preset(bundle.filaments, "Generic PLA"); - plain.config.opt_string("filament_type", 0u) = "PLA"; - plain.config.opt_string("filament_vendor", 0u) = "Generic"; - plain.config.opt("filament_retraction_length", true)->values = { 0.5 }; - // An unrelated same-type preset that would win a type-only search. - Preset &bambu = add_inmemory_preset(bundle.filaments, "Bambu PLA Basic"); - bambu.config.opt_string("filament_type", 0u) = "PLA"; - bambu.config.opt_string("filament_vendor", 0u) = "Bambu Lab"; - bambu.config.opt("filament_retraction_length", true)->values = { 0.4 }; + Preset &spare = add_inmemory_preset(bundle.filaments, "Spare PLA"); + spare.config.opt_string("filament_type", 0u) = "PLA"; + spare.config.opt("filament_retraction_length", true)->values = { 0.4 }; bundle.filament_presets = { "My PETG" }; PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @System"; - entry.filament_id = "GFL99"; - entry.full_keys = { "filament_retraction_length" }; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.publish_color = true; + entry.color = "#ABCDEF"; + entry.filament_id = "AFL01"; + entry.setting_id = "Sid000111222"; + entry.preset_name = "Author PLA @Vendor"; + entry.full_keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; @@ -1430,37 +1236,57 @@ TEST_CASE("Published 3MF name matching accepts suffix-less receiver presets", "[ Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // The trimmed bare-name tier fires: the suffix-less copy wins over the type-only - // candidate, without a substitute qualifier. - CHECK(bundle.filament_presets[0] == "Generic PLA"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA"); + // The slot lands on the freshly created copy, named after the stripped author name. + CHECK(bundle.filament_presets[0] == "Author PLA"); + Preset *copy = bundle.filaments.find_preset("Author PLA", false, true); + REQUIRE(copy != nullptr); + // Detached + project-embedded contract. + CHECK(copy->is_project_embedded); + CHECK(copy->inherits().empty()); + CHECK(copy->setting_id.empty()); + CHECK(copy->vendor == nullptr); + CHECK_FALSE(copy->is_system); + CHECK_FALSE(copy->is_default); + CHECK_FALSE(copy->is_external); + CHECK(copy->is_visible); + CHECK(copy->filament_id == "AFL01"); + CHECK(copy->config.opt("filament_settings_id")->values == std::vector{ "Author PLA" }); + // The published values and colour live on the copy. + CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(copy->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // Universally compatible: no printer/print restrictions survive the import. + CHECK(copy->config.opt("compatible_printers")->values.empty()); + CHECK(copy->config.opt("compatible_prints")->values.empty()); + CHECK(copy->config.opt("compatible_printers_condition")->value.empty()); + CHECK(copy->config.opt("compatible_prints_condition")->value.empty()); + // Nothing pre-existing was touched. + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + CHECK(bundle.filaments.find_preset("Spare PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.4 }); CHECK(pub.skipped_keys.empty()); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Author PLA (published material imported)"); } -// The receiver's vendor renamed "Generic PLA @System" to "PLA Generic @System" (renamed_from -// records the old name); the published file still names the old preset, which resolves through -// the collection's rename map instead of degrading to a same-type substitute. -TEST_CASE("Published 3MF name matching follows the receiver's preset renames", "[Preset][Bundle][Published]") +// Compatibility restrictions riding on the receiver's baseline preset must not leak onto the +// imported copy: a detached full material is usable with every printer and print profile. +TEST_CASE("Published 3MF clears printer restrictions on the imported full material", "[Preset][Bundle][Published]") { PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_inmemory_preset(bundle.filaments, "PLA Generic @System"); - bundle.filament_presets = { "My PETG" }; - - set_renamed_from(bundle.filaments, "PLA Generic @System", { "Generic PLA @System" }); - AppConfig app_config; - bundle.load_installed_printers(app_config); // rebuild the rename map + Preset &restricted = add_inmemory_preset(bundle.filaments, "Restricted PLA"); + restricted.config.opt_string("filament_type", 0u) = "PLA"; + restricted.config.opt("filament_retraction_length", true)->values = { 0.5 }; + restricted.config.set_key_value("compatible_printers", new ConfigOptionStrings({ "Unrelated Printer" })); + restricted.config.set_key_value("compatible_prints", new ConfigOptionStrings({ "Unrelated Print" })); + restricted.config.option("compatible_printers_condition", true)->value = "printer_settings_id==\"Nope\""; + restricted.config.option("compatible_prints_condition", true)->value = "print_settings_id==\"Nope\""; + bundle.filament_presets = { "Restricted PLA" }; PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Generic PLA @System"; // the pre-rename name - entry.full_keys = { "filament_retraction_length" }; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.full_keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; @@ -1469,215 +1295,206 @@ TEST_CASE("Published 3MF name matching follows the receiver's preset renames", " Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // find_preset2 resolves the pre-rename name through the collection's rename map. - CHECK(bundle.filament_presets[0] == "PLA Generic @System"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> PLA Generic @System"); + // The copy (named from the published type; no identity fields) has no restrictions left. + CHECK(bundle.filament_presets[0] == "PLA"); + Preset *copy = bundle.filaments.find_preset("PLA", false, true); + REQUIRE(copy != nullptr); + CHECK(copy->config.opt("compatible_printers")->values.empty()); + CHECK(copy->config.opt("compatible_prints")->values.empty()); + CHECK(copy->config.opt("compatible_printers_condition")->value.empty()); + CHECK(copy->config.opt("compatible_prints_condition")->value.empty()); + // The receiver's own restricted preset keeps its restrictions. + const Preset *original = bundle.filaments.find_preset("Restricted PLA", false, true); + REQUIRE(original != nullptr); + CHECK(original->config.opt("compatible_printers")->values == std::vector{ "Unrelated Printer" }); + CHECK(original->config.opt("compatible_printers_condition")->value == "printer_settings_id==\"Nope\""); CHECK(pub.skipped_keys.empty()); } -// The author's preset "Voron Generic PLA" (a vendor-specific generic) is gone from the -// receiver's profile set; find_preset2 auto-matches it to the Orca Filament Library instead of -// falling back to a same-type substitute. -TEST_CASE("Published 3MF name matching falls back to the library for removed vendor generics", "[Preset][Bundle][Published]") -{ - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_inmemory_preset(bundle.filaments, "Generic PLA @System"); - bundle.filament_presets = { "My PETG" }; - - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.preset_name = "Voron Generic PLA"; - entry.full_keys = { "filament_retraction_length" }; - - PublishedConfig pub; - pub.published = true; - pub.material_keys = { entry }; - DynamicPrintConfig config = published_pla_file_config(); - Preset::normalize(config); - bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - - CHECK(bundle.filament_presets[0] == "Generic PLA @System"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA @System"); - CHECK(pub.skipped_keys.empty()); -} - -// The replacement search prefers the published identity: exact filament_id, then vendor+type, -// then type only (collection order decides equal scores; the pick is reported as a substitute -// when it is not the exact published material). -TEST_CASE("Published 3MF prefers the published material identity when replacing a slot", "[Preset][Bundle][Published]") +// Identical Full materials (same setting_id + preset_name identity) share one created +// instance: an author who pointed several slots at one material gets one standalone copy, +// and the first entry's slot values win. +TEST_CASE("Published 3MF shares one imported copy between identical full slots", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - config.opt("filament_diameter")->values = { 1.75 }; - config.opt("filament_self_index")->values = { 1 }; - config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; - config.opt("filament_colour")->values = { "#FF0000" }; - config.opt("filament_type")->values = { "PLA" }; - config.opt("filament_vendor")->values = { "Generic" }; - config.opt("filament_ids")->values = { "GFL99" }; - config.option("filament_retraction_length", true)->values = { 0.9 }; + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "AFL01", "AFL01" }; + config.option("filament_retraction_length", true)->values = { 0.9, 0.8 }; return config; }; - auto make_entry = [] { + auto make_entry = [](int slot) { PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.filament_id = "GFL99"; - entry.filament_vendor = "Generic"; - entry.full_keys = { "filament_retraction_length" }; + entry.slot = slot; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "AFL01"; + entry.setting_id = "Sid000111222"; + entry.preset_name = "Author PLA @Vendor"; + entry.full_keys = { "filament_retraction_length" }; return entry; }; - auto add_pla = [](PresetBundle &bundle, const char *name, const char *id, const char *vendor) { - Preset &preset = add_inmemory_preset(bundle.filaments, name); - preset.filament_id = id; - preset.config.opt_string("filament_type", 0u) = "PLA"; - preset.config.opt_string("filament_vendor", 0u) = vendor; - preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; - return &preset; - }; - auto load = [&](PresetBundle &bundle, PublishedConfig &pub) { - PublishedMaterialEntry entry = make_entry(); - 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); - }; - SECTION("exact filament_id beats collection order") { - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - // "Bambu PLA Basic" sorts before "Zebra PLA"; only the latter carries the published id. - add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); - add_pla(bundle, "Zebra PLA", "GFL99", "Generic"); - bundle.filament_presets = { "My PETG" }; + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + Preset &other = add_inmemory_preset(bundle.filaments, "Other PETG"); + other.config.opt_string("filament_type", 0u) = "PETG"; + other.config.opt("filament_retraction_length", true)->values = { 0.65 }; + bundle.filament_presets = { "My PETG", "Other PETG" }; - PublishedConfig pub; - load(bundle, pub); - CHECK(bundle.filament_presets[0] == "Zebra PLA"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Zebra PLA"); - } + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_entry(0), make_entry(1) }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - SECTION("vendor and type beat a type-only preset, reported as a substitute") { - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); - add_pla(bundle, "Zebra PLA", "ZZZ99", "Generic"); // same vendor+type, different id - bundle.filament_presets = { "My PETG" }; - - PublishedConfig pub; - load(bundle, pub); - CHECK(bundle.filament_presets[0] == "Zebra PLA"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Zebra PLA (substitute: no exact material match)"); - } - - SECTION("type-only candidates keep collection order and are reported as substitutes") { - PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_pla(bundle, "Bambu PLA Basic", "GFB00", "Bambu Lab"); - add_pla(bundle, "Zebra PLA", "ZZZ99", "Acme"); // no identity match at all - bundle.filament_presets = { "My PETG" }; - - PublishedConfig pub; - load(bundle, pub); - CHECK(bundle.filament_presets[0] == "Bambu PLA Basic"); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Bambu PLA Basic (substitute: no exact material match)"); - } + // Both slots point at the single shared copy; no second "(Published)" instance exists. + REQUIRE(bundle.filament_presets.size() == 2); + CHECK(bundle.filament_presets[0] == "Author PLA"); + CHECK(bundle.filament_presets[1] == "Author PLA"); + CHECK(bundle.filaments.find_preset("Author PLA", false, true) != nullptr); + CHECK(bundle.filaments.find_preset("Author PLA (Published)", false, true) == nullptr); + // The first entry's slot values won. + CHECK(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + // Both slots reported, same target; originals untouched. + REQUIRE(pub.material_replacements.size() == 2); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Author PLA (published material imported)"); + CHECK(pub.material_replacements[1] == "slot 1: Other PETG -> Author PLA (published material imported)"); + CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + CHECK(bundle.filaments.find_preset("Other PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.65 }); + CHECK(pub.skipped_keys.empty()); } -// "Generic PLA" and "Generic PLA Matte" share their inherited filament_id (OGFL99), so the -// exact variant can only be matched via the preset setting_id carried in the published file. -TEST_CASE("Published 3MF matches the exact published variant via setting_id", "[Preset][Bundle][Published]") +// Without a preset name the copy falls back to the stable material id; fully anonymous +// hand-crafted entries never share instances between slots (their dedup key is slot-scoped). +TEST_CASE("Published 3MF names unidentified full materials from their fallback fields", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); - config.opt("filament_diameter")->values = { 1.75 }; - config.opt("filament_self_index")->values = { 1 }; - config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; - config.opt("filament_colour")->values = { "#FF0000" }; - config.opt("filament_type")->values = { "PLA" }; - config.opt("filament_vendor")->values = { "Generic" }; - config.opt("filament_ids")->values = { "OGFL99" }; - config.option("filament_retraction_length", true)->values = { 0.9 }; + config.opt("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFT99" }; + config.option("filament_retraction_length", true)->values = { 0.9, 1.2 }; return config; }; - auto add_pla = [](PresetBundle &bundle, const char *name, const char *setting_id) { - Preset &preset = add_inmemory_preset(bundle.filaments, name); - preset.setting_id = setting_id; - preset.filament_id = "OGFL99"; // shared by all Generic PLA variants - preset.config.opt_string("filament_type", 0u) = "PLA"; - preset.config.opt_string("filament_vendor", 0u) = "Generic"; - preset.config.opt("filament_retraction_length", true)->values = { 0.5 }; - return &preset; - }; - auto load = [&](PresetBundle &bundle, PublishedConfig &pub, const std::string &setting_id) { - PublishedMaterialEntry entry; - entry.slot = 0; - entry.full = true; - entry.publish_type = true; - entry.publish_type_value = "PLA"; - entry.filament_id = "OGFL99"; - entry.filament_vendor = "Generic"; - entry.setting_id = setting_id; - entry.full_keys = { "filament_retraction_length" }; - 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); - }; - SECTION("the published variant wins over its same-id sibling") { + SECTION("empty preset_name falls back to the filament_id") { PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_pla(bundle, "Generic PLA", "RcBNzytWgwRrwXXz"); - add_pla(bundle, "Generic PLA Matte", "RFs9eCKYOMUSmvZf"); - bundle.filament_presets = { "My PETG" }; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt("filament_retraction_length", true)->values = { 0.5 }; + bundle.filament_presets = { "My PLA" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "AFL01"; + entry.full_keys = { "filament_retraction_length" }; PublishedConfig pub; - load(bundle, pub, "RFs9eCKYOMUSmvZf"); // the author published "Generic PLA Matte" - CHECK(bundle.filament_presets[0] == "Generic PLA Matte"); - CHECK(bundle.filaments.find_preset("Generic PLA Matte", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA Matte"); + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = published_pla_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "AFL01"); + CHECK(bundle.filaments.find_preset("AFL01", false, true) != nullptr); CHECK(pub.skipped_keys.empty()); } - SECTION("without a setting_id the same-id siblings fall back to collection order") { + SECTION("fully anonymous entries get slot-scoped copies") { PresetBundle bundle; - Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); - petg.config.opt_string("filament_type", 0u) = "PETG"; - petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; - add_pla(bundle, "Generic PLA", "RcBNzytWgwRrwXXz"); - add_pla(bundle, "Generic PLA Matte", "RFs9eCKYOMUSmvZf"); - bundle.filament_presets = { "My PETG" }; + Preset &first = add_inmemory_preset(bundle.filaments, "First PLA"); + first.config.opt_string("filament_type", 0u) = "PLA"; + first.config.opt("filament_retraction_length", true)->values = { 0.5 }; + Preset &second = add_inmemory_preset(bundle.filaments, "Second PLA"); + second.config.opt_string("filament_type", 0u) = "PLA"; + second.config.opt("filament_retraction_length", true)->values = { 0.55 }; + bundle.filament_presets = { "First PLA", "Second PLA" }; + + PublishedMaterialEntry entry0; + entry0.slot = 0; + entry0.full = true; + entry0.publish_type = true; + entry0.publish_type_value = "ABS"; // the only naming field present + entry0.full_keys = { "filament_retraction_length" }; + PublishedMaterialEntry entry1 = entry0; + entry1.slot = 1; PublishedConfig pub; - load(bundle, pub, ""); // legacy file without the field - CHECK(bundle.filament_presets[0] == "Generic PLA"); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + pub.published = true; + pub.material_keys = { entry0, entry1 }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // No shared identity: each slot gets its own uniquified copy. + CHECK(bundle.filament_presets[0] == "ABS"); + CHECK(bundle.filament_presets[1] == "ABS (Published)"); + CHECK(bundle.filaments.find_preset("ABS", false, true) != nullptr); + CHECK(bundle.filaments.find_preset("ABS (Published)", false, true) != nullptr); + CHECK(pub.skipped_keys.empty()); + } +} + +// Within-load dedup does not span loads: importing the same published file again into the same +// session creates a second, uniquified copy instead of mutating or reusing the first. +TEST_CASE("Re-importing a published full material uniquifies the second copy", "[Preset][Bundle][Published]") +{ + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + bundle.filament_presets = { "My PETG" }; + + auto make_entry = [] { + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.filament_id = "AFL01"; + entry.setting_id = "Sid000111222"; + entry.preset_name = "Author PLA @Vendor"; + entry.full_keys = { "filament_retraction_length" }; + return entry; + }; + + for (int round = 0; round < 2; ++round) { + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_entry() }; + DynamicPrintConfig config = published_pla_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + if (round == 0) { + CHECK(bundle.filament_presets[0] == "Author PLA"); + CHECK(bundle.filaments.find_preset("Author PLA (Published)", false, true) == nullptr); + } else { + // The second import uniquifies beside the first instead of touching it. + CHECK(bundle.filament_presets[0] == "Author PLA (Published)"); + CHECK(bundle.filaments.find_preset("Author PLA (Published)", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + CHECK(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: Author PLA -> Author PLA (Published) (published material imported)"); + } CHECK(pub.skipped_keys.empty()); } } From d4cb739b8b735b7014994db481017f7ce4dff455 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 14:17:56 +0800 Subject: [PATCH 21/60] Dead code removal and comment cleanup --- src/libslic3r/Preset.cpp | 12 ++-- src/libslic3r/PresetBundle.cpp | 97 +++++++------------------------ src/libslic3r/PresetBundle.hpp | 7 ++- src/libslic3r/PublishSettings.hpp | 24 ++++---- 4 files changed, 43 insertions(+), 97 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 802da6ae8c..a0fd9a9e04 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3056,9 +3056,11 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det } // A detached standalone preset for the Full Publish receiver: create a user preset holding -// the full resolved filament config (no inheritance, no vendor/alias links), parentless and -// universally compatible. Mirrors save_current_preset(detach=true)'s creation branch but -// does not force-select or diff against a parent; the caller decides whether to select it. +// the full resolved filament config (no inheritance, no vendor/alias links), parentless. +// Note: universal printer compatibility is not enforced here - callers apply +// make_publish_universal() to the config before handing it over when they need it. +// Mirrors save_current_preset(detach=true)'s creation branch but does not force-select or +// diff against a parent; the caller decides whether to select it. // The published entry's filament_id is forwarded so user bases keep their stable // material grouping (get_filament_presets() groups user bases by filament_id). // save_to_project=true (the Full Publish default) creates a project-embedded preset: @@ -3094,8 +3096,6 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base, lock(); const auto it = this->find_preset_internal(final_name); Preset &preset = *m_presets.insert(it, stored); - stored.name.clear(); // avoid stale copied name being used below - stored.config.clear(); preset.name = final_name; preset.vendor = nullptr; preset.alias.clear(); @@ -3103,7 +3103,7 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base, preset.m_excluded_from.clear(); preset.setting_id.clear(); preset.inherits().clear(); - preset.version = Semver::parse(SoftFever_VERSION) ? *Semver::parse(SoftFever_VERSION) : Semver(); + preset.version = Semver::parse(SoftFever_VERSION).value_or(Semver()); preset.is_default = false; preset.is_system = false; preset.is_external = false; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 96373eae7c..f1b653feac 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4921,25 +4921,21 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Material pass: 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: the full dump still applies wholesale (every setting, as if the slot's - // filament had been loaded from a normal save); a partial entry's keys are applied - // as usual; - // - type mismatch: the slot is replaced with the best visible candidate, scored by the - // published identity (exact preset name resolved through the collection's name - // machinery, then exact setting_id, exact filament_id, then vendor+type, then type - // only); a preset no other slot references wins on equal scores, and a shared - // exact-material preset is taken even though mutating it also affects the other - // slot; 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 the first available visible - // preset, applying the author's values on top of it; a partial entry keeps the - // receiver's material and reports its keys as skipped. - // All applied values (colour and keys) are written onto the slot's effective preset - - // the collection's edited layer when the slot references the edited preset (visible as - // a modification, revertible, the user's unsaved edits preserved), otherwise the stored - // preset in place: the receiver's material keeps its identity and is simply overridden. + // - full: the slot always lands on a freshly created standalone detached copy + // ("Detach from parent", project-embedded, universally compatible, within-load + // deduped) - handled up front in the entry loop below; no library preset is ever + // reused or mutated; + // - partial: a type requirement gates the application - on type match (or no + // requirement) the keys are applied onto the slot's effective preset; on mismatch + // the slot is replaced with the best visible candidate, scored by the published + // identity (exact preset name resolved through the collection's name machinery, + // then exact setting_id, exact filament_id, then vendor+type, then type only); a + // preset no other slot references wins on equal scores; with no replacement + // available the receiver's material is kept and the keys are reported as skipped; + // - colour: applied to the slot regardless of the type gate. + // Applied partial values land on the collection's edited layer when the slot references + // it and that layer survives the load (visible as a modification, revertible, the user's + // unsaved edits preserved), otherwise on the stored preset in place. // To keep slot-to-slot aliasing (several slots referencing one preset) from leaking one // slot's values into another, published slots sharing a preset with another slot are // re-pointed at distinct presets before the values are applied. @@ -5521,64 +5517,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty())) replacement_line += " (substitute: no exact material match)"; published_config->material_replacements.emplace_back(std::move(replacement_line)); - } else if (entry.full) { - // No same-type library preset: fall back to the first available - // visible preset, preferring one no other slot references, and - // apply the author's full values on top of it (the dump carries - // filament_type, so the preset takes the author's type). A preset - // of the published material's own family is preferred overall - - // except that a referenced preset must never win just on family, - // since the dump would mutate it for every sharing slot too. - std::string fallback; - const std::string wanted_family = !entry.publish_type_value.empty() - ? entry.publish_type_value : normalize_filament_type(entry.filament_type); - auto pick_fallback = [&](bool want_family) -> std::string { - std::string first; - for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset &candidate = this->filaments.preset(i); - if (!candidate.is_visible) - continue; - if (want_family) { - if (wanted_family.empty()) - break; - const ConfigOptionStrings *cand_types = candidate.config.opt("filament_type"); - if (normalize_filament_type(cand_types != nullptr && !cand_types->values.empty() ? cand_types->get_at(0) : std::string()) != wanted_family) - continue; - } - bool referenced = false; - for (size_t s = 0; s < this->filament_presets.size(); ++s) - if (this->filament_presets[s] == candidate.name) { - referenced = true; - break; - } - if (!referenced) - return candidate.name; - if (first.empty()) - first = candidate.name; - } - return first; - }; - if (!wanted_family.empty()) - fallback = pick_fallback(true); - if (fallback.empty()) - fallback = pick_fallback(false); - if (!fallback.empty() && fallback != recv->name) { - const std::string old_name = recv->name; - this->filament_presets[slot] = fallback; - recv = this->filaments.find_preset(fallback, false, true); - material_applied = true; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF slot " << slot << " material " << old_name - << " -> " << fallback << " (full-publish fallback, preset_name \"" - << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; - published_config->material_replacements.emplace_back( - "slot " + std::to_string(slot) + ": " + old_name + " -> " + fallback + - " (substitute: no " + entry.publish_type_value + " available)"); - } - // No visible preset at all: keep the receiver's material and let - // the full dump mutate it below. } else { // Partial publish with no replacement: keep the receiver's - // material and report the slot's keys as skipped. + // material and report the slot's keys as skipped. (Full entries + // never reach this gate: they detach above.) for (const std::string &key : entry.keys) skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); apply_slot = false; @@ -5590,7 +5532,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // layer when the slot references the collection's edited preset and that // layer will survive the load (visible as a modification and revertible, // preserving the user's unsaved edits), otherwise the stored preset it ended - // up on (original, type replacement or the full-publish fallback), in place. + // up on (original or partial type replacement), in place. Full entries never + // get here - they detach above. DynamicPrintConfig &write_config = (edited_survives_load && recv->name == this->filaments.get_edited_preset().name) ? this->filaments.get_edited_preset().config : recv->config; @@ -5619,7 +5562,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool } if (apply_slot && recv != nullptr) - apply_slot_keys(write_config, entry.full ? entry.full_keys : entry.keys, entry.slot, material_label); + apply_slot_keys(write_config, entry.keys, entry.slot, material_label); } } } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 98b83e053b..c3c8d0f8b0 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -175,9 +175,10 @@ struct PublishedConfig { bool published = false; std::vector published_keys; - // Per-slot published material keys, applied positionally (author slot N -> receiver slot N), - // gated by the author's optional type requirement and written onto the slot's stored preset - // in place (see PublishedMaterialEntry in PublishSettings.hpp). + // Per-slot published material keys, applied positionally (author slot N -> receiver slot N). + // Partial entries are gated by the author's optional type requirement and written onto the + // slot's stored preset in place; full entries instead detach (see PublishedMaterialEntry in + // PublishSettings.hpp). std::vector material_keys; // 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. diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index bcb3397dcc..d10472ce71 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -35,19 +35,20 @@ const std::set& publishable_printer_keys(); std::vector collect_dirty_settings_keys(const PresetBundle& bundle); // Per-slot published material keys, applied positionally (author slot N -> receiver slot N). -// The identity fields are carried for reference/notification labels only; the type gate -// (publish_type) is the author's explicit opt-in for requiring a material type. +// The identity fields drive the created copy's naming and grouping on Full entries, the +// notification labels, and the partial type gate (publish_type) is the author's explicit +// opt-in for requiring a material type. struct PublishedMaterialEntry { std::string filament_type; // material family, e.g. "PLA" (may be empty) std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty) std::string filament_id; // stable material id, e.g. "GFL99" (may be empty) - // Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id"); - // used on load to match the exact published variant, which filament_id alone cannot - // distinguish ("Generic PLA" and "Generic PLA Matte" share their inherited id). + // Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id"). + // Not matched against the receiver's library; carried so identical Full entries within one + // load share one created instance (within-load dedup key). std::string setting_id; - // Canonical name of the author's slot preset (e.g. "Generic PLA @System"). The receiver - // prefers an exact name/alias match over id matching: ids can be shared across variants - // or missing from older files, the name is what the author actually selected. + // Canonical name of the author's slot preset (e.g. "Generic PLA @System"). On Full import + // it names the created copy after its "@variant" tail is stripped; never matched against + // the receiver's library. std::string preset_name; // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; @@ -63,9 +64,10 @@ struct PublishedMaterialEntry { // All non-structural filament keys of the author's slot preset; values travel in the file // config, masked to the author's slot index. std::vector full_keys; - // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on - // mismatch the slot is replaced with a same-type filament from the receiver's library. - // For Full entries the baked filament_type on the created copy satisfies the type gate. + // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a + // partial entry's mismatch the slot is replaced with a same-type filament from the + // receiver's library. Full entries consult no gate: they detach unconditionally, and the + // copy carries whatever values the payload bakes. bool publish_type{false}; std::string publish_type_value; // Required filament colour, applied on load regardless of the type match. From 795514900da47f420739f2d1b407b7f8250cd19e Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 15:18:38 +0800 Subject: [PATCH 22/60] Fix Windows paint on resize issue. Automatically resize to fit tabs and button content --- src/slic3r/GUI/PublishSettingsDialog.cpp | 40 +++++++++++++++++++++--- src/slic3r/GUI/PublishSettingsDialog.hpp | 2 ++ src/slic3r/GUI/Widgets/StaticBox.cpp | 7 +++++ src/slic3r/GUI/Widgets/StaticBox.hpp | 2 ++ src/slic3r/GUI/Widgets/TabCtrl.cpp | 9 ++++++ src/slic3r/GUI/Widgets/TabCtrl.hpp | 2 ++ 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 08adae903b..0ebb784b01 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -17,6 +17,7 @@ #include "libslic3r/PublishSettings.hpp" #include +#include #include #include @@ -97,7 +98,7 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) _L("Publish 3MF..."), wxDefaultPosition, wxDefaultSize, - wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER) + wxCAPTION | wxCLOSE_BOX | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE) , m_search(this, "search", 16) , m_menu(this, "filter", 16) { @@ -193,11 +194,42 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) w_sizer->Add(dlg_btns, 0, wxEXPAND); SetSizerAndFit(w_sizer); - SetMinSize(FromDIP(wxSize(600, 500))); - SetSize(FromDIP(wxSize(600, 500))); // initial size only; the dialog is resizable + fit_to_content(); // initial size only; the dialog is resizable wxGetApp().UpdateDlgDarkUI(this); } +// Size the window to its content: width follows the widest tab strip so no filament tab is +// hidden (TabCtrl::relayout hides overflowing buttons), height scales proportionally. Both +// are floored at the 600x500 base and capped at hard DIP limits - deliberately not the whole +// display - with one last-resort clamp so the dialog can never open larger than the screen. +// Also owns the resize floor: the window cannot be resized below what the tabs need, so +// shrinking never re-hides a filament tab. +void PublishSettingsDialog::fit_to_content() +{ + static const wxSize BASE{600, 500}; + static const wxSize CAP{1300, 850}; + + int strip = m_outer_tabs->buttons_best_width(); + for (const SectionGroup& section : m_sections) + strip = std::max(strip, section.tabs->buttons_best_width()); + + // Minimum width: whatever keeps every tab visible (never below the base). strip is device + // pixels (Button min sizes); BASE/CAP are DIP and converted over. + const int min_w = std::max(strip + 2 * FromDIP(10), FromDIP(BASE.x)); + + // Initial size: prefer proportional growth within the caps. + const int w = std::clamp(min_w, FromDIP(BASE.x), FromDIP(CAP.x)); + const double f = double(w) / FromDIP(BASE.x); + const int h = std::clamp(int(FromDIP(BASE.y) * f), FromDIP(BASE.y), FromDIP(CAP.y)); + + const wxRect area = wxDisplay(this).GetClientArea(); + const int max_w = area.width * 9 / 10; + const int max_h = area.height * 9 / 10; + + SetMinSize(wxSize(std::min(min_w, max_w), std::min(FromDIP(BASE.y), max_h))); + SetSize(std::min(w, max_w), std::min(h, max_h)); +} + PublishSettingsDialog::~PublishSettingsDialog() {} void PublishSettingsDialog::build_option_model() @@ -1059,7 +1091,7 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) } } - SetMinSize(FromDIP(wxSize(600, 500))); + fit_to_content(); // tab buttons' min widths grew with the DPI: re-fit (incl. resize floor) Refresh(); } diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 312931c0c1..2b094da75b 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -48,6 +48,8 @@ protected: void on_dpi_changed(const wxRect& suggested_rect) override; private: + void fit_to_content(); + // Which part of the settings the row/category came from. enum class Section { Print, Printer, Material }; diff --git a/src/slic3r/GUI/Widgets/StaticBox.cpp b/src/slic3r/GUI/Widgets/StaticBox.cpp index 6391c1a8c4..f6e7ca67f6 100644 --- a/src/slic3r/GUI/Widgets/StaticBox.cpp +++ b/src/slic3r/GUI/Widgets/StaticBox.cpp @@ -7,6 +7,7 @@ BEGIN_EVENT_TABLE(StaticBox, wxWindow) // catch paint events //EVT_ERASE_BACKGROUND(StaticBox::eraseEvent) +EVT_SIZE(StaticBox::sizeEvent) EVT_PAINT(StaticBox::paintEvent) END_EVENT_TABLE() @@ -140,6 +141,12 @@ void StaticBox::eraseEvent(wxEraseEvent& evt) #endif } +void StaticBox::sizeEvent(wxSizeEvent& evt) +{ + Refresh(); + evt.Skip(); +} + void StaticBox::paintEvent(wxPaintEvent& evt) { // depending on your system you may need to look at double-buffered dcs diff --git a/src/slic3r/GUI/Widgets/StaticBox.hpp b/src/slic3r/GUI/Widgets/StaticBox.hpp index b7cdee34ef..363d431c8e 100644 --- a/src/slic3r/GUI/Widgets/StaticBox.hpp +++ b/src/slic3r/GUI/Widgets/StaticBox.hpp @@ -46,6 +46,8 @@ public: protected: void eraseEvent(wxEraseEvent& evt); + void sizeEvent(wxSizeEvent& evt); + void paintEvent(wxPaintEvent& evt); void render(wxDC& dc); diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 6aeffd2dce..5c2eb2d693 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -295,6 +295,15 @@ void TabCtrl::relayout() Layout(); } +int TabCtrl::buttons_best_width() const +{ + // Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing. + int width = 10; + for (const Button *btn : btns) + width += btn->GetMinSize().x + TAB_BUTTON_SPACE * 2; + return width; +} + void TabCtrl::buttonClicked(wxCommandEvent &event) { SetFocus(); diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index e79dd1dee6..5631eeb6a7 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -73,6 +73,8 @@ private: void relayout(); + int buttons_best_width() const; + void buttonClicked(wxCommandEvent & event); void keyDown(wxKeyEvent &event); From 51b646efcf7aeae7cc5a7e4790b31fa8baf4bbe1 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 16:40:18 +0800 Subject: [PATCH 23/60] Better safety if publish workflow crashes. Cleanup on Button --- src/slic3r/GUI/Plater.cpp | 56 +++++++++++++++++++----------- src/slic3r/GUI/Widgets/Button.cpp | 21 ++++------- src/slic3r/GUI/Widgets/Button.hpp | 3 -- src/slic3r/GUI/Widgets/TabCtrl.cpp | 2 +- src/slic3r/GUI/Widgets/TabCtrl.hpp | 5 +-- 5 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 52d90623fe..bdca6b7c77 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16323,29 +16323,43 @@ int Plater::export_published_3mf(const std::vector& published_keys, if (full_pathnames) save_strategy = save_strategy | SaveStrategy::FullPathSources; - const int ret = export_3mf(into_path(path), save_strategy, -1, nullptr); + // Restore the previous metadata state (both on success and on failure): a thrown export + // must not leave the published metadata on the in-memory project, or a later Save Project + // would write a hybrid file (full config + slicer tags + published metadata) that receivers + // silently load in published mode, skipping the project's own presets. + auto restore_metadata = [&]() { + if (!had_model_info) { + model.model_info = nullptr; + } else { + if (had_published) + model.model_info->metadata_items["published"] = prev_published; + else + model.model_info->metadata_items.erase("published"); + if (had_published_keys) + model.model_info->metadata_items["published_keys"] = prev_published_keys; + else + model.model_info->metadata_items.erase("published_keys"); + if (had_material_keys) + model.model_info->metadata_items["published_material_keys"] = prev_material_keys; + else + model.model_info->metadata_items.erase("published_material_keys"); + if (had_payload) + model.model_info->metadata_items["published_config"] = prev_payload; + else + model.model_info->metadata_items.erase("published_config"); + } + }; - // Restore the previous metadata state (both on success and on failure). - if (!had_model_info) { - model.model_info = nullptr; - } else { - if (had_published) - model.model_info->metadata_items["published"] = prev_published; - else - model.model_info->metadata_items.erase("published"); - if (had_published_keys) - model.model_info->metadata_items["published_keys"] = prev_published_keys; - else - model.model_info->metadata_items.erase("published_keys"); - if (had_material_keys) - model.model_info->metadata_items["published_material_keys"] = prev_material_keys; - else - model.model_info->metadata_items.erase("published_material_keys"); - if (had_payload) - model.model_info->metadata_items["published_config"] = prev_payload; - else - model.model_info->metadata_items.erase("published_config"); + int ret; + try { + ret = export_3mf(into_path(path), save_strategy, -1, nullptr); + } catch (...) { + restore_metadata(); + MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), + _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); + return wxID_CANCEL; } + restore_metadata(); if (ret < 0) { MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 3129d608ca..5a5bd89403 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -80,7 +80,6 @@ bool Button::SetFont(const wxFont& font) void Button::SetIcon(const wxString& icon) { - custom_icon = wxNullBitmap; auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt()); if (!icon.IsEmpty()) { //BBS set button icon default size to 20 @@ -104,13 +103,6 @@ void Button::SetIcon(const wxBitmap& icon) Refresh(); } -void Button::SetBitmap(const wxBitmap& bitmap) -{ - custom_icon = bitmap; - messureSize(); - Refresh(); -} - void Button::SetMinSize(const wxSize& size) { minSize = size; @@ -310,8 +302,7 @@ void Button::render(wxDC& dc) } } auto szContent = textSize; - const bool has_custom_icon = custom_icon.IsOk(); - if (has_custom_icon || icon.bmp().IsOk()) { + if (icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon if (vertical) @@ -319,7 +310,7 @@ void Button::render(wxDC& dc) else szContent.x += spacing; } - szIcon = has_custom_icon ? custom_icon.GetSize() : icon.GetBmpSize(); + szIcon = icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; if (szIcon.x > szContent.x) szContent.x = szIcon.x; @@ -342,12 +333,12 @@ void Button::render(wxDC& dc) } // start draw wxPoint pt = rcContent.GetLeftTop(); - if (has_custom_icon || icon.bmp().IsOk()) { + if (icon.bmp().IsOk()) { if (vertical) pt.x += (rcContent.width - szIcon.x) / 2; else pt.y += (rcContent.height - szIcon.y) / 2; - dc.DrawBitmap(has_custom_icon ? custom_icon : icon.bmp(), pt); + dc.DrawBitmap(icon.bmp(), pt); //BBS norrow size between text and icon if (vertical) { pt.y += szIcon.y + spacing; @@ -380,7 +371,7 @@ void Button::messureSize() wxClientDC dc(this); dc.GetTextExtent(GetLabel(), &textSize.width, &textSize.height, &textSize.x, &textSize.y); wxSize szContent = textSize.GetSize(); - if (custom_icon.IsOk() || this->active_icon.bmp().IsOk()) { + if (this->active_icon.bmp().IsOk()) { if (szContent.y > 0) { //BBS norrow size between text and icon if (vertical) @@ -388,7 +379,7 @@ void Button::messureSize() else szContent.x += 5; } - wxSize szIcon = custom_icon.IsOk() ? custom_icon.GetSize() : this->active_icon.GetBmpSize(); + wxSize szIcon = this->active_icon.GetBmpSize(); if (vertical) { szContent.y += szIcon.y; if (szIcon.x > szContent.x) szContent.x = szIcon.x; diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 9ad0810804..19dcd24938 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -36,7 +36,6 @@ class Button : public StaticBox wxSize minSize; // set by outer wxSize paddingSize; ScalableBitmap active_icon; - wxBitmap custom_icon; StateColor text_color; @@ -63,8 +62,6 @@ public: void SetIcon(const wxString& icon); void SetIcon(const wxBitmap& icon); - void SetBitmap(const wxBitmap& bitmap); - void SetMinSize(const wxSize& size) override; void SetMaxSize(const wxSize& size) override; diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 5c2eb2d693..7817c9111a 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -182,7 +182,7 @@ void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap) { if (item >= btns.size()) return; - btns[item]->SetBitmap(bitmap); + btns[item]->SetIcon(bitmap); relayout(); } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index 5631eeb6a7..5374a32a73 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -64,6 +64,9 @@ public: int GetNextVisible(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). + int buttons_best_width() const; + private: virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO); @@ -73,8 +76,6 @@ private: void relayout(); - int buttons_best_width() const; - void buttonClicked(wxCommandEvent & event); void keyDown(wxKeyEvent &event); From e1a79c41126870d9d4d510be2f9b193a510b7901 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 25 Aug 2026 17:37:04 +0800 Subject: [PATCH 24/60] Code cleanup and renamed published_* flags to orca_published_* flags to be less generic --- src/libslic3r/Format/bbs_3mf.cpp | 2 +- src/libslic3r/Format/bbs_3mf.hpp | 9 ++++ src/slic3r/GUI/Plater.cpp | 56 ++++++++++++------------ src/slic3r/GUI/PublishSettingsDialog.hpp | 3 +- tests/libslic3r/test_3mf.cpp | 48 ++++++++++---------- 5 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 6415f29413..4a7cac7055 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -1224,7 +1224,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Reads the parse-time metadata: the model XML carries it before its resources, while // m_model->model_info is only filled in after the whole XML has been parsed. bool _is_published_3mf() const { - return this->model_info.metadata_items.find("published") != this->model_info.metadata_items.end(); + return this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG) != this->model_info.metadata_items.end(); } bool _is_svg_shape_file(const std::string &filename) const; diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 57a40c2012..647488ad55 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -154,6 +154,15 @@ enum class SaveStrategy Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel, }; +// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal, +// tag-less publish export, the others carry the author-selected settings payload. Namespaced +// with the "orca_published" prefix because metadata_items round-trips verbatim through other +// slicers, where a bare "published" key could collide. +inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published"; +inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys"; +inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys"; +inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config"; + inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs) { using T = std::underlying_type_t ; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index bdca6b7c77..dfeca24a19 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6920,11 +6920,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ // metadata payload, which must fill config_loaded before the chain decides // whether to import geometry only. if (model.model_info != nullptr) { - auto published_it = model.model_info->metadata_items.find("published"); + auto published_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG); if (published_it != model.model_info->metadata_items.end() && (published_it->second == "true" || published_it->second == "1")) { published_config.published = true; - auto keys_it = model.model_info->metadata_items.find("published_keys"); + auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG); if (keys_it != model.model_info->metadata_items.end()) { try { auto j = nlohmann::json::parse(keys_it->second); @@ -6937,7 +6937,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - auto material_keys_it = model.model_info->metadata_items.find("published_material_keys"); + auto material_keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG); if (material_keys_it != model.model_info->metadata_items.end()) { try { auto jm = nlohmann::json::parse(material_keys_it->second); @@ -6995,7 +6995,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // file carries no project_settings.config, so config_loaded is filled // from here; a missing or malformed payload leaves it empty and the // fallback chain below imports the geometry only. - auto payload_it = model.model_info->metadata_items.find("published_config"); + auto payload_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG); if (payload_it != model.model_info->metadata_items.end()) { try { ConfigSubstitutions payload_substitutions = config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable); @@ -7320,10 +7320,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ 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"); - this->model.model_info->metadata_items.erase("published_config"); + this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG); + this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG); + this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG); + this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG); } if (load_config) { @@ -16289,19 +16289,19 @@ int Plater::export_published_3mf(const std::vector& published_keys, // Save the previous metadata so it can be restored after the export, keeping the in-memory // project pristine (the published flag lives only in the exported file). const bool had_model_info = (model.model_info != nullptr); - const bool had_published = had_model_info && (model.model_info->metadata_items.find("published") != model.model_info->metadata_items.end()); - const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find("published_keys") != model.model_info->metadata_items.end()); - const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find("published_material_keys") != model.model_info->metadata_items.end()); - const bool had_payload = had_model_info && (model.model_info->metadata_items.find("published_config") != model.model_info->metadata_items.end()); - const std::string prev_published = had_published ? model.model_info->metadata_items.at("published") : std::string(); - const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at("published_keys") : std::string(); - const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at("published_material_keys") : std::string(); - const std::string prev_payload = had_payload ? model.model_info->metadata_items.at("published_config") : std::string(); + const bool had_published = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG) != model.model_info->metadata_items.end()); + const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG) != model.model_info->metadata_items.end()); + const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG) != model.model_info->metadata_items.end()); + const bool had_payload = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG) != model.model_info->metadata_items.end()); + const std::string prev_published = had_published ? model.model_info->metadata_items.at(ORCA_PUBLISHED_TAG) : std::string(); + const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) : std::string(); + const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) : std::string(); + const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string(); if (model.model_info == nullptr) model.model_info = std::make_shared(); - model.model_info->metadata_items["published"] = "1"; - model.model_info->metadata_items["published_keys"] = j.dump(); - model.model_info->metadata_items["published_material_keys"] = jm.dump(); + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump(); + model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump(); // Minimal published export: filter full_config to the published keys, material keys, // identity fields and plate geometry keys, and omit the project config file, the @@ -16314,7 +16314,7 @@ int Plater::export_published_3mf(const std::vector& published_keys, std::string payload; for (const std::string &key : filtered_cfg.keys()) payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; - model.model_info->metadata_items["published_config"] = std::move(payload); + model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); // Same file layout as save_project(), plus Silence (so export_3mf does not set the project // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. @@ -16332,21 +16332,21 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info = nullptr; } else { if (had_published) - model.model_info->metadata_items["published"] = prev_published; + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = prev_published; else - model.model_info->metadata_items.erase("published"); + model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG); if (had_published_keys) - model.model_info->metadata_items["published_keys"] = prev_published_keys; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = prev_published_keys; else - model.model_info->metadata_items.erase("published_keys"); + model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG); if (had_material_keys) - model.model_info->metadata_items["published_material_keys"] = prev_material_keys; + model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = prev_material_keys; else - model.model_info->metadata_items.erase("published_material_keys"); + model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG); if (had_payload) - model.model_info->metadata_items["published_config"] = prev_payload; + model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = prev_payload; else - model.model_info->metadata_items.erase("published_config"); + model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG); } }; diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 2b094da75b..2cf49e7eb9 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -11,7 +11,6 @@ #include #include #include -#include // Forward declarations (all are global classes, see Widgets/TextInput.hpp and // Widgets/StaticLine.hpp). @@ -30,7 +29,7 @@ struct PublishMaterialIdentity // Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout // mirroring the Process settings (Printer / Filament / Process outer tabs, category or material // tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become -// "published_keys" and the material rows become "published_material_keys". +// "orca_published_keys" and the material rows become "orca_published_material_keys". class PublishSettingsDialog : public DPIDialog { public: diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 8898dae2fe..089cc7a40d 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -501,8 +501,8 @@ SCENARIO("Nozzle-group metadata .3mf round-trip", "[3mf][MultiNozzle]") { } } -// Locks the serialization contract of the "Publish" metadata: the published flag and the -// published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf -> +// Locks the serialization contract of the "Publish" metadata: the orca_published flag and the +// orca_published_keys JSON array in model.model_info->metadata_items must survive a store_bbs_3mf -> // load_bbs_3mf round-trip unchanged. (The full preset-preservation behavior is exercised // headlessly in test_preset_bundle_loading.cpp.) SCENARIO("Published 3MF round-trips the published flag and published_keys metadata", "[3mf]") { @@ -513,8 +513,8 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada model.add_default_instances(); model.model_info = std::make_shared(); - model.model_info->metadata_items["published"] = "1"; - model.model_info->metadata_items["published_keys"] = R"(["layer_height","wall_thickness"])"; + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","wall_thickness"])"; // store_bbs_3mf stages project_settings.config through the model's backup path; point // it at a writable temp dir (the default lives under a read-only root in CI). @@ -546,12 +546,12 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada THEN("the published metadata round-trips unchanged") { REQUIRE(loaded); REQUIRE(dst_model.model_info != nullptr); - REQUIRE(dst_model.model_info->metadata_items["published"] == "1"); - REQUIRE(dst_model.model_info->metadata_items["published_keys"] == R"(["layer_height","wall_thickness"])"); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1"); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","wall_thickness"])"); - // The published_keys value is a JSON array of setting keys; it must parse back to + // The orca_published_keys value is a JSON array of setting keys; it must parse back to // the same keys that were selected. - nlohmann::json keys = nlohmann::json::parse(dst_model.model_info->metadata_items["published_keys"]); + nlohmann::json keys = nlohmann::json::parse(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG]); REQUIRE(keys.is_array()); REQUIRE(keys.size() == 2); REQUIRE(keys[0] == "layer_height"); @@ -563,7 +563,7 @@ SCENARIO("Published 3MF round-trips the published flag and published_keys metada } // A normal 3MF (no Publish metadata) must load identically: the loader must not fabricate a -// "published" flag or published_keys for files that never carried them. +// "orca_published" flag or orca_published_keys for files that never carried them. SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { GIVEN("a model without any published metadata") { Model model; @@ -599,8 +599,8 @@ SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { THEN("no published key is fabricated") { REQUIRE(loaded); if (dst_model.model_info != nullptr) { - REQUIRE(dst_model.model_info->metadata_items.count("published") == 0); - REQUIRE(dst_model.model_info->metadata_items.count("published_keys") == 0); + REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_TAG) == 0); + REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_KEYS_TAG) == 0); } } release_PlateData_list(dst_plates); @@ -608,8 +608,8 @@ SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { } } -// Locks the serialization contract of the published_material_keys metadata: the per-entry JSON -// must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like published_keys. +// Locks the serialization contract of the orca_published_material_keys metadata: the per-entry JSON +// must survive a store_bbs_3mf -> load_bbs_3mf round-trip verbatim, exactly like orca_published_keys. SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf]") { GIVEN("a model carrying published material keys metadata") { Model model; @@ -621,7 +621,7 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99"},"slot":0,"keys":["filament_retraction_length","filament_z_hop"]}])"; model.model_info = std::make_shared(); - model.model_info->metadata_items["published_material_keys"] = material_keys_json; + model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json; ScopedTemporaryDir backup_dir("orca_pub_mat"); model.set_backup_path(backup_dir.string()); @@ -651,7 +651,7 @@ SCENARIO("Published 3MF round-trips the published_material_keys metadata", "[3mf THEN("the published material keys 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); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json); // The value must parse back to one material entry carrying the nested identity // object, the author slot ordinal and the key list. @@ -706,9 +706,9 @@ SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer ta payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; model.model_info = std::make_shared(); - model.model_info->metadata_items["published"] = "1"; - model.model_info->metadata_items["published_keys"] = R"(["layer_height","retraction_length"])"; - model.model_info->metadata_items["published_config"] = payload; + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = R"(["layer_height","retraction_length"])"; + model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = payload; ScopedTemporaryDir backup_dir("orca_min_pub"); model.set_backup_path(backup_dir.string()); @@ -760,13 +760,13 @@ SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer ta } THEN("the published metadata and payload round-trip unchanged") { REQUIRE(dst_model.model_info != nullptr); - REQUIRE(dst_model.model_info->metadata_items["published"] == "1"); - REQUIRE(dst_model.model_info->metadata_items["published_keys"] == R"(["layer_height","retraction_length"])"); - REQUIRE(dst_model.model_info->metadata_items["published_config"] == payload); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1"); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height","retraction_length"])"); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] == payload); } THEN("the payload parses back to the published values") { DynamicPrintConfig parsed_payload; - parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items["published_config"], ForwardCompatibilitySubstitutionRule::Enable); + parsed_payload.load_from_ini_string(dst_model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG], ForwardCompatibilitySubstitutionRule::Enable); REQUIRE(parsed_payload.option("layer_height") != nullptr); REQUIRE_THAT(parsed_payload.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-6)); REQUIRE(parsed_payload.option("retraction_length") != nullptr); @@ -887,7 +887,7 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { R"([{"material":{"filament_type":"PLA","filament_vendor":"Generic","filament_id":"GFL99","setting_id":"RFs9eCKYOMUSmvZf","name":"Generic PLA Matte @System"},"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(); - model.model_info->metadata_items["published_material_keys"] = material_keys_json; + model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = material_keys_json; ScopedTemporaryDir backup_dir("orca_pub_mat2"); model.set_backup_path(backup_dir.string()); @@ -917,7 +917,7 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { 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); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json); // The value must parse back with every extended field intact. nlohmann::json entries = nlohmann::json::parse(material_keys_json); From cc267055e1064d39a9f60d3d03ed4c04a0b450ca Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 26 Aug 2026 11:33:50 +0800 Subject: [PATCH 25/60] Use proper floating point comparison functions in publish unit test --- tests/libslic3r/test_3mf.cpp | 12 +- .../libslic3r/test_preset_bundle_loading.cpp | 116 ++++++++++-------- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 089cc7a40d..621b35a8c7 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -799,10 +799,10 @@ SCENARIO("Full-publish entries filter the whole slot and mask the other slots", 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("filament_flow_ratio")->values[1] == 0.98); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); } THEN("the non-published slot is masked to its default") { - REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[0] == 1.0); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); @@ -829,10 +829,10 @@ SCENARIO("Partial-publish entries mask the other slots like full entries", "[3mf THEN("the partial key is present with the author's slot value") { REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr); - REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[1] == 0.98); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); } THEN("the non-published slot is masked to its default") { - REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[0] == 1.0); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); @@ -864,8 +864,8 @@ SCENARIO("Unmaskable keys are dropped from the published payload instead of leak } THEN("the maskable key is present, author slot kept, other slot masked") { REQUIRE(filtered_cfg.opt("filament_flow_ratio") != nullptr); - REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[1] == 0.98); - REQUIRE(filtered_cfg.opt("filament_flow_ratio")->values[0] == 1.0); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); + REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); } THEN("the identity keys stay present") { REQUIRE(filtered_cfg.option("filament_colour") != nullptr); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index ba92f43943..34cbcfaf4e 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -9,6 +9,7 @@ #include "test_utils.hpp" #include +#include using namespace Slic3r; @@ -22,6 +23,14 @@ bool contains_key(const std::vector &keys, const std::string &key) return std::find(keys.begin(), keys.end(), key) != keys.end(); } +void check_double_vector(const std::vector &actual, std::initializer_list expected) +{ + REQUIRE(actual.size() == expected.size()); + size_t index = 0; + for (double value : expected) + REQUIRE_THAT(actual[index++], Catch::Matchers::WithinAbs(value, 1e-6)); +} + void write_print_preset(const DynamicPrintConfig &default_config, const fs::path &file, const std::string &name, const std::string &inherits = {}) { DynamicPrintConfig config(default_config); @@ -646,7 +655,7 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the // a) Process scalar overlaid; matching-size vector applied, mismatched one lands in // skipped_keys; applied keys are not reported. CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); - CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 140., 150. }); + check_double_vector(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values, { 140., 150. }); CHECK(bundle.prints.get_edited_preset().config.opt("post_process")->values == std::vector{ "existing-script" }); CHECK(contains_key(pub.skipped_keys, "post_process")); CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); @@ -665,7 +674,7 @@ TEST_CASE("Published 3MF overlays only the author-selected process keys onto the CHECK(bundle.project_config.opt("filament_colour")->values == seed_filament_colour); CHECK(bundle.project_config.opt("flush_multiplier")->values == seed_flush_multiplier); CHECK(bundle.project_config.option("curr_bed_type")->getInt() == seed_bed_type); - CHECK(bundle.project_config.opt("wipe_tower_x")->values == std::vector{ 100. }); + check_double_vector(bundle.project_config.opt("wipe_tower_x")->values, { 100. }); CHECK_THAT(bundle.project_config.opt("wipe_tower_rotation_angle")->value, Catch::Matchers::WithinAbs(45., 0.000001)); // e) The published path keeps the user's currently-selected presets: same preset, same size. @@ -716,8 +725,8 @@ TEST_CASE("Published 3MF overlays only the allowlisted retraction and z-hop keys // Matching-size retraction vector applied; mismatched vector reported as skipped and the // receiver's own value survives. - CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 1.4 }); - CHECK(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values == std::vector{ 33. }); + check_double_vector(bundle.printers.get_edited_preset().config.opt("retraction_length")->values, { 1.4 }); + check_double_vector(bundle.printers.get_edited_preset().config.opt("retraction_speed")->values, { 33. }); CHECK(contains_key(pub.skipped_keys, "retraction_speed")); // Contract-excluded printer key: silently ignored, absent from skipped_keys. CHECK(bundle.printers.get_edited_preset().config.opt_string("machine_start_gcode") == "G28 ; user"); @@ -784,9 +793,9 @@ TEST_CASE("Published 3MF applies positional material keys onto the receiver's ma bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); // The author's slot values are written onto the receiver's stored presets in place. - CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); - CHECK(bundle.filaments.find_preset("My PETG")->config.opt("filament_z_hop")->values == std::vector{ 0.3 }); + check_double_vector(bundle.filaments.find_preset("My PLA")->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("My PETG")->config.opt("filament_retraction_length")->values, { 1.2 }); + check_double_vector(bundle.filaments.find_preset("My PETG")->config.opt("filament_z_hop")->values, { 0.3 }); // Structural keys inside a material entry are silently ignored: the receiver's own // filament_settings_id is untouched and nothing is reported for it. CHECK(bundle.filaments.find_preset("My PLA")->config.opt("filament_settings_id")->values == std::vector{ "receiver-pla" }); @@ -847,8 +856,8 @@ TEST_CASE("Published 3MF full-published slots are imported as standalone detache CHECK(bundle.filament_presets[0] == "PLA"); Preset *copy = bundle.filaments.find_preset("PLA", false, true); REQUIRE(copy != nullptr); - CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(copy->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PLA -> PLA (published material imported)"); @@ -877,9 +886,9 @@ TEST_CASE("Published 3MF full-published slots are imported as standalone detache CHECK(bundle.filament_presets[0] == "ABS"); Preset *copy = bundle.filaments.find_preset("ABS", false, true); REQUIRE(copy != nullptr); - CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("My ABS", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.3 }); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(copy->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("My ABS", false, true)->config.opt("filament_retraction_length")->values, { 0.3 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PLA -> ABS (published material imported)"); @@ -914,12 +923,12 @@ TEST_CASE("Published 3MF full-published slots are imported as standalone detache CHECK(bundle.filament_presets[0] == "ABS"); Preset *copy = bundle.filaments.find_preset("ABS", false, true); REQUIRE(copy != nullptr); - CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(copy->config.opt("filament_retraction_length")->values, { 0.9 }); CHECK(copy->config.opt_string("filament_type", 0u) == "ABS"); CHECK(copy->config.opt_string("filament_vendor", 0u) == "Generic"); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt_string("filament_type", 0u) == "PLA"); - CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.7 }); + check_double_vector(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.7 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PLA -> ABS (published material imported)"); @@ -984,9 +993,9 @@ TEST_CASE("Published 3MF imports a full material under the author's stripped nam // The copy is named after the stripped author name; the receiver's exact-name preset // keeps its own values. CHECK(bundle.filament_presets[0] == "Generic PLA"); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); + check_double_vector(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values, { 0.6 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (published material imported)"); CHECK(pub.skipped_keys.empty()); @@ -1017,7 +1026,7 @@ TEST_CASE("Published 3MF imports a full material under the author's stripped nam bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); CHECK(bundle.filament_presets[0] == "Generic PLA"); - CHECK(bundle.filaments.find_preset("Bambu PLA Basic @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("Bambu PLA Basic @System", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (published material imported)"); CHECK(pub.skipped_keys.empty()); @@ -1065,8 +1074,8 @@ TEST_CASE("Published 3MF imports a full material under the author's stripped nam // The grown slot lands on a freshly created copy carrying the author's slot-1 value; // the library preset that seeded it stays untouched. CHECK(bundle.filament_presets[1] == "Generic PLA"); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.8 }); - CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.8 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 1: Generic PLA @System -> Generic PLA (published material imported)"); CHECK(pub.skipped_keys.empty()); @@ -1117,10 +1126,10 @@ TEST_CASE("Published 3MF uniquifies an imported full material name on collision" // The copy lands beside the collision, suffixed; every pre-existing preset keeps its own // values. CHECK(bundle.filament_presets[0] == "Generic PLA (Published)"); - CHECK(bundle.filaments.find_preset("Generic PLA (Published)", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(bundle.filaments.find_preset("Generic PLA @Qidi Q2 0.4 nozzle", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA (Published)", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA @Qidi Q2 0.4 nozzle", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (Published) (published material imported)"); CHECK(pub.skipped_keys.empty()); @@ -1178,13 +1187,13 @@ TEST_CASE("Published 3MF writes to the stored preset when the edited layer is re Preset *stored = bundle.filaments.find_preset("My PLA", false, true); REQUIRE(stored != nullptr); CHECK(stored->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(stored->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(stored->config.opt("filament_retraction_length")->values, { 0.9 }); // The load re-selected slot 0's material, mirroring a normal project load. CHECK(bundle.filaments.get_edited_preset().name == "My PETG"); // Selecting the slot's material afterwards surfaces the applied values. REQUIRE(bundle.filaments.select_preset_by_name("My PLA", false)); CHECK(bundle.filaments.get_edited_preset().config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(bundle.filaments.get_edited_preset().config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(bundle.filaments.get_edited_preset().config.opt("filament_retraction_length")->values, { 0.9 }); CHECK(pub.skipped_keys.empty()); } @@ -1252,7 +1261,7 @@ TEST_CASE("Published 3MF imports a full material as a detached project-embedded CHECK(copy->filament_id == "AFL01"); CHECK(copy->config.opt("filament_settings_id")->values == std::vector{ "Author PLA" }); // The published values and colour live on the copy. - CHECK(copy->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(copy->config.opt("filament_retraction_length")->values, { 0.9 }); CHECK(copy->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); // Universally compatible: no printer/print restrictions survive the import. CHECK(copy->config.opt("compatible_printers")->values.empty()); @@ -1260,8 +1269,8 @@ TEST_CASE("Published 3MF imports a full material as a detached project-embedded CHECK(copy->config.opt("compatible_printers_condition")->value.empty()); CHECK(copy->config.opt("compatible_prints_condition")->value.empty()); // Nothing pre-existing was touched. - CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); - CHECK(bundle.filaments.find_preset("Spare PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.4 }); + check_double_vector(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values, { 0.6 }); + check_double_vector(bundle.filaments.find_preset("Spare PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.4 }); CHECK(pub.skipped_keys.empty()); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Author PLA (published material imported)"); @@ -1364,13 +1373,13 @@ TEST_CASE("Published 3MF shares one imported copy between identical full slots", CHECK(bundle.filaments.find_preset("Author PLA", false, true) != nullptr); CHECK(bundle.filaments.find_preset("Author PLA (Published)", false, true) == nullptr); // The first entry's slot values won. - CHECK(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); // Both slots reported, same target; originals untouched. REQUIRE(pub.material_replacements.size() == 2); CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Author PLA (published material imported)"); CHECK(pub.material_replacements[1] == "slot 1: Other PETG -> Author PLA (published material imported)"); - CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.6 }); - CHECK(bundle.filaments.find_preset("Other PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.65 }); + check_double_vector(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values, { 0.6 }); + check_double_vector(bundle.filaments.find_preset("Other PETG", false, true)->config.opt("filament_retraction_length")->values, { 0.65 }); CHECK(pub.skipped_keys.empty()); } @@ -1490,8 +1499,8 @@ TEST_CASE("Re-importing a published full material uniquifies the second copy", " } else { // The second import uniquifies beside the first instead of touching it. CHECK(bundle.filament_presets[0] == "Author PLA (Published)"); - CHECK(bundle.filaments.find_preset("Author PLA (Published)", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); - CHECK(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(bundle.filaments.find_preset("Author PLA (Published)", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); + check_double_vector(bundle.filaments.find_preset("Author PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0] == "slot 0: Author PLA -> Author PLA (Published) (published material imported)"); } @@ -1544,7 +1553,7 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish // Type matched: keys and colour applied onto the receiver's preset in place. Preset *pla_preset = bundle.filaments.find_preset("My PLA", false, true); REQUIRE(pla_preset != nullptr); - CHECK(pla_preset->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(pla_preset->config.opt("filament_retraction_length")->values, { 0.9 }); CHECK(pla_preset->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(pub.skipped_keys.empty()); CHECK(pub.material_replacements.empty()); @@ -1578,7 +1587,7 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish CHECK(bundle.filament_presets[0] == "My PLA"); Preset *pla_preset = bundle.filaments.find_preset("My PLA", false, true); REQUIRE(pla_preset != nullptr); - CHECK(pla_preset->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(pla_preset->config.opt("filament_retraction_length")->values, { 0.5 }); CHECK(pla_preset->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); CHECK(contains_key(pub.skipped_keys, "material:ABS (filament_retraction_length)")); CHECK(pub.material_replacements.empty()); @@ -1613,8 +1622,8 @@ TEST_CASE("Published 3MF partial slots apply colour and gate keys by the publish // then receives the author's values in place. REQUIRE(bundle.filament_presets.size() == 2); CHECK(bundle.filament_presets[1] == "My PETG"); - CHECK(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 1.2 }); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("My PETG", false, true)->config.opt("filament_retraction_length")->values, { 1.2 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); } } @@ -1944,9 +1953,9 @@ TEST_CASE("Published 3MF refreshes the edited preset so the applied material val // visible as a modification while the stored preset stays untouched. CHECK(edited.name == "My PLA"); CHECK(edited.config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(edited.config.opt("filament_retraction_length")->values, { 0.9 }); CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); // The overlay is a visible, revertible modification of the edited preset. CHECK(bundle.filaments.current_is_dirty()); CHECK(pub.skipped_keys.empty()); @@ -1978,7 +1987,7 @@ TEST_CASE("Published 3MF refreshes the edited preset so the applied material val const Preset &edited = bundle.filaments.get_edited_preset(); CHECK(edited.name == "My ABS"); CHECK(edited.config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(edited.config.opt("filament_retraction_length")->values, { 0.9 }); } } @@ -2018,15 +2027,15 @@ TEST_CASE("Published 3MF preserves unsaved edits on the edited filament preset", const Preset &edited = bundle.filaments.get_edited_preset(); CHECK(edited.name == "My PLA"); CHECK(edited.config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(edited.config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(edited.config.opt("filament_retraction_length")->values, { 0.9 }); // ...the user's unsaved edit on a non-published key survives... - CHECK(edited.config.opt("filament_z_hop")->values == std::vector{ 0.7 }); + check_double_vector(edited.config.opt("filament_z_hop")->values, { 0.7 }); // ...and the stored preset is untouched. Preset *stored = bundle.filaments.find_preset("My PLA", false, true); REQUIRE(stored != nullptr); CHECK(stored->config.opt("filament_colour")->values == std::vector{ "#123456" }); - CHECK(stored->config.opt("filament_retraction_length")->values == std::vector{ 0.5 }); - CHECK(stored->config.opt("filament_z_hop")->values == std::vector{ 0.1 }); + check_double_vector(stored->config.opt("filament_retraction_length")->values, { 0.5 }); + check_double_vector(stored->config.opt("filament_z_hop")->values, { 0.1 }); // The overlay is a visible, revertible modification of the edited preset. CHECK(bundle.filaments.current_is_dirty()); CHECK(pub.skipped_keys.empty()); @@ -2056,7 +2065,7 @@ TEST_CASE("Published 3MF rejects out-of-range vector variants and variant-suffix bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); // In-range variant applied element-wise; the out-of-range one did not resize the vector. - CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 10., 150. }); + check_double_vector(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values, { 10., 150. }); CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values.size() == 2); // Out-of-range variant, malformed variant and variant-suffixed scalar are reported as // skipped; the malformed one must not fall back to element 0. @@ -2096,7 +2105,7 @@ TEST_CASE("Published 3MF reports type-mismatched keys as skipped instead of abor // The load completes; the type-mismatched key is reported as skipped and the receiver's // value is untouched; the matching scalar key still applies. CHECK(contains_key(pub.skipped_keys, "wiping_volumes_extruders")); - CHECK(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values == std::vector{ 10., 20. }); + check_double_vector(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values, { 10., 20. }); CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 0.000001)); CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); } @@ -2230,7 +2239,7 @@ TEST_CASE("Published 3MF reloading does not compound values on the receiver's pr load(); // Each load re-applies the same values onto the (already mutated) preset: no accumulation. CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.9 }); } // A receiver with several slots aliasing the same preset (multi-extruder profile with one @@ -2293,7 +2302,7 @@ TEST_CASE("Published 3MF gives each published slot its own preset on an aliased for (size_t slot = 0; slot < 4; ++slot) { Preset *preset = bundle.filaments.find_preset(bundle.filament_presets[slot], false, true); REQUIRE(preset != nullptr); - CHECK(preset->config.opt("filament_retraction_length")->values == std::vector{ expected[slot] }); + check_double_vector(preset->config.opt("filament_retraction_length")->values, { expected[slot] }); } CHECK(pub.skipped_keys.empty()); } @@ -2345,7 +2354,7 @@ TEST_CASE("Published 3MF de-aliases an aliased slot by published identity withou // The published key was written onto the re-pointed slot's own preset. Preset *target = bundle.filaments.find_preset("Zzz PLA", false, true); REQUIRE(target != nullptr); - CHECK(target->config.opt("filament_retraction_length")->values == std::vector{ 0.9 }); + check_double_vector(target->config.opt("filament_retraction_length")->values, { 0.9 }); REQUIRE(pub.material_replacements.size() == 1); CHECK(pub.material_replacements[0].find("(de-aliased") != std::string::npos); CHECK(pub.skipped_keys.empty()); @@ -2375,7 +2384,7 @@ TEST_CASE("Published 3MF applies per-extruder printer keys across extruder-count DynamicPrintConfig config = make_file_config(); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 0.6 }); + check_double_vector(bundle.printers.get_edited_preset().config.opt("retraction_length")->values, { 0.6 }); CHECK(contains_key(pub.skipped_keys, "retraction_length#1")); CHECK(contains_key(pub.skipped_keys, "retraction_length#2")); CHECK(contains_key(pub.skipped_keys, "retraction_length#3")); @@ -2395,8 +2404,7 @@ TEST_CASE("Published 3MF applies per-extruder printer keys across extruder-count config.opt("retraction_length")->values = { 0.7 }; bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - CHECK(bundle.printers.get_edited_preset().config.opt("retraction_length")->values == std::vector{ 0.7, 0.8, 0.8, 0.8 }); + check_double_vector(bundle.printers.get_edited_preset().config.opt("retraction_length")->values, { 0.7, 0.8, 0.8, 0.8 }); CHECK(pub.skipped_keys.empty()); } } - From 036c4004f7537b930ab55bbf7fe6e26e5c74e314 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 26 Aug 2026 12:33:31 +0800 Subject: [PATCH 26/60] Fixes publish dialog filtering issue --- src/slic3r/GUI/PublishSettingsDialog.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 0ebb784b01..70a6cc96ae 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -120,12 +120,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) m_filter_ctrl->SetHint(_L("Type to filter...")); m_filter_ctrl->Bind(wxEVT_TEXT, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); }); m_filter_ctrl->Bind(wxEVT_TEXT_ENTER, [this](auto&) { apply_filter(m_filter_ctrl->GetValue()); }); - m_filter_ctrl->Bind(wxEVT_SET_FOCUS, [this](auto& e) { - apply_filter(m_filter_ctrl->GetValue()); - e.Skip(); - }); - m_filter_ctrl->Bind(wxEVT_KILL_FOCUS, [this](auto& e) { - apply_filter(m_filter_ctrl->GetValue()); + m_filter_ctrl->Bind(wxEVT_LEFT_DOWN, [this](auto& e) { + if (m_filter_mode != FilterMode::Text) + apply_filter(m_filter_ctrl->GetValue()); e.Skip(); }); f_sizer->Add(m_filter_box, 1, wxEXPAND); From feb0e479faaf4dc6cec635b60bf3f3b225697e4a Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 26 Aug 2026 13:06:24 +0800 Subject: [PATCH 27/60] Published flag hardening. Better error handling path for when publish fails (unlikely) --- src/libslic3r/Format/bbs_3mf.cpp | 8 +- src/libslic3r/Format/bbs_3mf.hpp | 4 + src/slic3r/GUI/Plater.cpp | 172 +++++++++++++++++-------------- 3 files changed, 108 insertions(+), 76 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 4a7cac7055..15f3f3c0f0 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -678,6 +678,11 @@ bool bbs_is_valid_object_type(const std::string& type) namespace Slic3r { +bool is_published_3mf_flag(const std::string &value) +{ + return value == "1"; +} + void PlateData::parse_filament_info(GCodeProcessorResult *result) { if (!result) return; @@ -1224,7 +1229,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // Reads the parse-time metadata: the model XML carries it before its resources, while // m_model->model_info is only filled in after the whole XML has been parsed. bool _is_published_3mf() const { - return this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG) != this->model_info.metadata_items.end(); + const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG); + return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second); } bool _is_svg_shape_file(const std::string &filename) const; diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 647488ad55..46d06199fa 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -163,6 +163,10 @@ inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys" inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys"; inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config"; +// Published files are produced with "1". The importer and the GUI loader both gate on this +// exact value, so a "0"/"false"/unknown value is rejected consistently. +bool is_published_3mf_flag(const std::string &value); + inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs) { using T = std::underlying_type_t ; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index dfeca24a19..c580f16c70 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6921,8 +6921,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // whether to import geometry only. if (model.model_info != nullptr) { auto published_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG); - if (published_it != model.model_info->metadata_items.end() && - (published_it->second == "true" || published_it->second == "1")) { + if (published_it != model.model_info->metadata_items.end() && is_published_3mf_flag(published_it->second)) { published_config.published = true; auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG); if (keys_it != model.model_info->metadata_items.end()) { @@ -6943,50 +6942,55 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto jm = nlohmann::json::parse(material_keys_it->second); if (jm.is_array()) for (const auto &m : jm) { - // Malformed entries are skipped individually. - if (!m.is_object()) - continue; - PublishedMaterialEntry entry; - const auto mat_it = m.find("material"); - if (mat_it != m.end() && mat_it->is_object()) { - const auto &mat = *mat_it; - if (mat.contains("filament_type") && mat["filament_type"].is_string()) - entry.filament_type = mat["filament_type"].get(); - if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) - entry.filament_vendor = mat["filament_vendor"].get(); - if (mat.contains("filament_id") && mat["filament_id"].is_string()) - entry.filament_id = mat["filament_id"].get(); - if (mat.contains("setting_id") && mat["setting_id"].is_string()) - entry.setting_id = mat["setting_id"].get(); - if (mat.contains("name") && mat["name"].is_string()) - entry.preset_name = mat["name"].get(); + try { + // Malformed entries are isolated so one bad item + // cannot discard valid entries that follow it. + if (!m.is_object()) + continue; + PublishedMaterialEntry entry; + const auto mat_it = m.find("material"); + if (mat_it != m.end() && mat_it->is_object()) { + const auto &mat = *mat_it; + if (mat.contains("filament_type") && mat["filament_type"].is_string()) + entry.filament_type = mat["filament_type"].get(); + if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string()) + entry.filament_vendor = mat["filament_vendor"].get(); + if (mat.contains("filament_id") && mat["filament_id"].is_string()) + entry.filament_id = mat["filament_id"].get(); + if (mat.contains("setting_id") && mat["setting_id"].is_string()) + entry.setting_id = mat["setting_id"].get(); + if (mat.contains("name") && mat["name"].is_string()) + entry.preset_name = mat["name"].get(); + } + if (m.contains("slot") && m["slot"].is_number_integer()) + entry.slot = m["slot"].get(); + const auto entry_keys_it = m.find("keys"); + if (entry_keys_it != m.end() && entry_keys_it->is_array()) + for (const auto &k : *entry_keys_it) + if (k.is_string()) + entry.keys.emplace_back(k.get()); + // Fields always written by the current exporter. + if (m.contains("full") && m["full"].is_boolean()) + entry.full = m["full"].get(); + 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()); + if (m.contains("publish_type") && m["publish_type"].is_boolean()) + entry.publish_type = m["publish_type"].get(); + if (m.contains("type") && m["type"].is_string()) + entry.publish_type_value = m["type"].get(); + if (m.contains("publish_color") && m["publish_color"].is_boolean()) + entry.publish_color = m["publish_color"].get(); + if (m.contains("color") && m["color"].is_string()) + entry.color = m["color"].get(); + published_config.material_keys.emplace_back(std::move(entry)); + } catch (const nlohmann::json::exception &) { + // Ignore only this malformed material entry. } - if (m.contains("slot") && m["slot"].is_number_integer()) - entry.slot = m["slot"].get(); - const auto entry_keys_it = m.find("keys"); - if (entry_keys_it != m.end() && entry_keys_it->is_array()) - for (const auto &k : *entry_keys_it) - if (k.is_string()) - entry.keys.emplace_back(k.get()); - // Fields always written by the current exporter. - if (m.contains("full") && m["full"].is_boolean()) - entry.full = m["full"].get(); - 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()); - if (m.contains("publish_type") && m["publish_type"].is_boolean()) - entry.publish_type = m["publish_type"].get(); - if (m.contains("type") && m["type"].is_string()) - entry.publish_type_value = m["type"].get(); - if (m.contains("publish_color") && m["publish_color"].is_boolean()) - entry.publish_color = m["publish_color"].get(); - if (m.contains("color") && m["color"].is_string()) - entry.color = m["color"].get(); - published_config.material_keys.emplace_back(std::move(entry)); } - } catch (...) { + } catch (const nlohmann::json::exception &) { // Ignore malformed published_material_keys; the project still loads normally. } } @@ -16297,37 +16301,21 @@ int Plater::export_published_3mf(const std::vector& published_keys, const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) : std::string(); const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) : std::string(); const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string(); - if (model.model_info == nullptr) - model.model_info = std::make_shared(); - model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; - model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump(); - model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump(); - // Minimal published export: filter full_config to the published keys, material keys, - // identity fields and plate geometry keys, and omit the project config file, the - // project-embedded preset dumps and the OrcaSlicer version tag from the archive. The - // filtered values are serialized into the published_config metadata payload instead, so - // OrcaSlicer versions without the publish feature fall back to importing the geometry only - // (keeping the receiver's presets) while new versions rebuild the config from the payload. - DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); - DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); - std::string payload; - for (const std::string &key : filtered_cfg.keys()) - payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; - model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); + // export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too, + // otherwise a publish changes what a later normal project save writes. + std::vector> previous_svg_paths; + for (ModelObject *object : model.objects) + for (ModelVolume *volume : object->volumes) + if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) { + std::string *path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf; + previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf); + } - // Same file layout as save_project(), plus Silence (so export_3mf does not set the project - // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; - bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); - if (full_pathnames) - save_strategy = save_strategy | SaveStrategy::FullPathSources; + auto restore_temporary_state = [&]() { + for (const auto &[path_in_3mf, previous_path] : previous_svg_paths) + *path_in_3mf = previous_path; - // Restore the previous metadata state (both on success and on failure): a thrown export - // must not leave the published metadata on the in-memory project, or a later Save Project - // would write a hybrid file (full config + slicer tags + published metadata) that receivers - // silently load in published mode, skipping the project's own presets. - auto restore_metadata = [&]() { if (!had_model_info) { model.model_info = nullptr; } else { @@ -16349,23 +16337,57 @@ int Plater::export_published_3mf(const std::vector& published_keys, model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG); } }; + bool state_restored = false; + auto restore_now = [&]() { + if (state_restored) + return; + restore_temporary_state(); + state_restored = true; + }; + ScopeGuard restore_guard(restore_now); - int ret; + if (model.model_info == nullptr) + model.model_info = std::make_shared(); + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump(); + model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump(); + + int ret = -1; try { + // Minimal published export: filter full_config to the published keys, material keys, + // identity fields and plate geometry keys, and omit the project config file, the + // project-embedded preset dumps and the OrcaSlicer version tag from the archive. The + // filtered values are serialized into the published_config metadata payload instead, so + // OrcaSlicer versions without the publish feature fall back to importing the geometry only + // (keeping the receiver's presets) while new versions rebuild the config from the payload. + DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); + std::string payload; + for (const std::string &key : filtered_cfg.keys()) + payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; + model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); + + // Same file layout as save_project(), plus Silence (so export_3mf does not set the project + // filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished. + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished; + bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); + if (full_pathnames) + save_strategy = save_strategy | SaveStrategy::FullPathSources; ret = export_3mf(into_path(path), save_strategy, -1, nullptr); } catch (...) { - restore_metadata(); + restore_now(); MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } - restore_metadata(); if (ret < 0) { + restore_now(); MessageDialog(this, _L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs have the file open."), _L("Publish"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } + restore_now(); return wxID_YES; } From 76d9b8bac045afb8f9b89464a1da730df48c703c Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 27 Aug 2026 13:17:19 +0800 Subject: [PATCH 28/60] 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 --- src/libslic3r/PresetBundle.cpp | 98 +- src/libslic3r/PublishSettings.cpp | 17 + src/libslic3r/PublishSettings.hpp | 7 + src/slic3r/GUI/FilamentBitmapUtils.cpp | 41 +- src/slic3r/GUI/FilamentBitmapUtils.hpp | 16 + src/slic3r/GUI/MixedFilamentDialog.cpp | 48 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 942 ++++++++++++++++-- src/slic3r/GUI/PublishSettingsDialog.hpp | 52 +- src/slic3r/GUI/Widgets/TabCtrl.cpp | 138 ++- src/slic3r/GUI/Widgets/TabCtrl.hpp | 50 +- tests/libslic3r/test_3mf.cpp | 52 + .../libslic3r/test_preset_bundle_loading.cpp | 249 +++++ 12 files changed, 1474 insertions(+), 236 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b66c0a8587..9157987b8a 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5291,6 +5291,11 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, const std::set printer_option_set(printer_options.begin(), printer_options.end()); std::set contract_excluded_keys; auto apply_published = [&](DynamicPrintConfig& target, const std::set* 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 collapsed_bases; for (const std::string& key : published_config->published_keys) { if (applied_keys.count(key) != 0) continue; // already applied @@ -5309,7 +5314,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, if (src_opt == nullptr) continue; // key not present in the loaded config; record later 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()) continue; // cannot apply; will be reported as skipped // 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()) continue; // 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 // indices are skipped (set_at would otherwise resize the receiver's vector). if (key.size() > base_key.size()) { @@ -5341,16 +5346,33 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, break; } } - if (!valid || idx >= static_cast(src_opt)->size() || - idx >= static_cast(dst_opt)->size()) - continue; // malformed or out-of-range variant: cannot apply; reported as skipped + const size_t src_size = static_cast(src_opt)->size(); + if (!valid || idx >= src_size) + continue; // malformed or out-of-range on the author's side: cannot apply; reported as skipped + const size_t dst_size = static_cast(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(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(src_opt)->size() != static_cast(dst_opt)->size()) { // Whole-vector base key: the receiver must have a matching vector size, // otherwise applying would overwrite a different number of elements. 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); } else { // 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); if (proj_volume_map && proj_volume_map->values.size() < target_slots) proj_volume_map->values.resize(target_slots, static_cast(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("filament_is_mixed")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, false); + if (auto* opt = this->project_config.opt("filament_mixed_components")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, std::string{}); + if (auto* opt = this->project_config.opt("filament_mixed_sublayer_ratios")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, std::string{}); + if (auto* opt = this->project_config.opt("filament_mixed_gradient")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, false); + if (auto* opt = this->project_config.opt("filament_mixed_gradient_range")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, std::string{}); + if (auto* opt = this->project_config.opt("filament_mixed_gradient_curve")) + if (opt->values.size() < target_slots) + opt->values.resize(target_slots, std::string{}); + if (auto* opt = this->project_config.opt("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) this->ams_multi_color_filment.resize(target_slots); 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 // into project_config for GUI rendering. 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 // requirement, not an override. if (ConfigOptionStrings* colour = write_config.opt("filament_colour", true)) { @@ -6016,8 +6068,36 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, } } - if (apply_slot && recv != nullptr) - apply_slot_keys(write_config, entry.keys, entry.slot, material_label); + if (apply_slot && recv != nullptr) { + // 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& mixed_keys = publish_mixed_keys(); + std::vector 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(static_cast(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(dst_opt)->size()) { + static_cast(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); + } } } } diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index 3a3d241e81..c4c71f9436 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -83,6 +83,23 @@ const std::set& publish_structural_keys() return structural_keys; } +const std::set& 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 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. // 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). diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index d10472ce71..038a6eca11 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -15,6 +15,13 @@ std::string publish_base_key(const std::string &key); // filter_published_config because 3MF validation needs it - exported, never applied. const std::set& 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& publish_mixed_keys(); + // 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. struct PublishablePrinterOption { diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index 1f51fc79b3..23b14c385b 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -11,6 +11,45 @@ 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) { if (rect.width <= 0 || rect.height <= 0) return; @@ -73,7 +112,7 @@ std::vector sample_gradient_ramp(const wxColour& first, // 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 // 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("filament_mixed_gradient_curve"); if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) { diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index 11696f3401..fc6eb1f9dd 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -13,6 +13,16 @@ namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; } 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. // Use instead of wxDC::GradientFillLinear, whose CoreGraphics (CGShading) backend // fails to render on some macOS builds; solid fills are unaffected. @@ -51,6 +61,12 @@ std::vector sample_gradient_ramp(const wxColour& first, // destination's height in pixels. std::vector 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. void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector& ramp); diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index 902c12d27b..c1cbcc7450 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -919,52 +919,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider() } // ---- Triangle (ternary) ratio picker ---- - -// Barycentric coordinate utilities -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}; -} +// The barycentric utilities (TriPoint, tri_contains, tri_barycentric, tri_clamp) live in +// FilamentBitmapUtils so the Publish dialog can mirror this picker read-only. wxBoxSizer* MixedFilamentDialog::create_triangle_picker() { diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 70a6cc96ae..262fdbe356 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -6,6 +6,7 @@ #include "I18N.hpp" #include "Tab.hpp" #include "ConfigValueFormatter.hpp" +#include "FilamentBitmapUtils.hpp" #include "Widgets/Label.hpp" #include "Widgets/TextInput.hpp" #include "Widgets/DialogButtons.hpp" @@ -15,11 +16,21 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Preset.hpp" #include "libslic3r/PublishSettings.hpp" +#include "libslic3r/FilamentMixer.hpp" #include #include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include namespace Slic3r { namespace GUI { namespace { @@ -90,8 +101,230 @@ wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPr return _L("Material"); } +// The component slots of a mixed filament ("1,2,3"), 1-based, or empty when out of range. +std::vector mixed_slot_components(const DynamicPrintConfig& full, size_t slot) +{ + const auto* comp_opt = full.opt("filament_mixed_components"); + if (comp_opt == nullptr || slot >= comp_opt->size()) + return {}; + return parse_mixed_components(comp_opt->values[slot]); +} + +// Human-readable label of a mixed filament, mirroring the sidebar's mixed filament rows: the +// 1-based component slot numbers with their blend percentages (or a "->" gradient arrow), +// e.g. "1 (60%) + 2 (40%)". Used as the slot's tab/header title in place of a preset name. +wxString mixed_filament_label(const DynamicPrintConfig& full, size_t slot) +{ + const std::vector comps = mixed_slot_components(full, slot); + if (comps.empty()) + return _L("Mixed filament"); + const auto* grad_opt = full.opt("filament_mixed_gradient"); + const bool is_gradient = grad_opt != nullptr && slot < grad_opt->size() && grad_opt->values[slot]; + const auto* ratios_opt = full.opt("filament_mixed_sublayer_ratios"); + wxString label; + for (size_t i = 0; i < comps.size(); ++i) { + if (i > 0) + label += is_gradient ? wxString::FromUTF8(" \u2192 ") : wxString::FromUTF8(" + "); + label += wxString::Format("%u", comps[i]); + if (!is_gradient) { + double ratio = 100.0 / comps.size(); + if (ratios_opt != nullptr && slot < ratios_opt->size()) { + const std::vector rs = parse_mixed_ratios(ratios_opt->values[slot], comps.size()); + if (i < rs.size()) + ratio = rs[i] * 100.0; + } + label += wxString::Format(" (%d%%)", int(ratio + 0.5)); + } + } + return label; +} + +// Blended representative colour of a mixed slot, computed exactly like the sidebar's swatches +// (recompute_mixed_slot_colors): sublayer slots blend by their ratios, gradient slots by their +// two end colours, broken references fall back to grey. +wxColour mixed_filament_blend_color(const DynamicPrintConfig& full, size_t slot) +{ + std::vector colors; + if (const auto* colours = full.opt("filament_colour")) { + colors.reserve(colours->values.size()); + for (const std::string& hex : colours->values) { + const wxColour c(hex); + colors.push_back(c.IsOk() ? c : wxColour(0, 0, 0)); + } + } + while (colors.size() <= slot) + colors.push_back(wxColour("#D9D9D9")); + recompute_mixed_slot_colors(colors, full); + return slot < colors.size() ? colors[slot] : wxColour("#D9D9D9"); +} + +// Tab-strip bitmap for a mixed slot: the mix's own chip, then its component swatches each +// followed by their percent share (or a "->" arrow for gradients), mirroring the main GUI's +// sidebar rows - e.g. "[3 purple]: [1 red] 50% + [2 blue] 50%". The whole composition is one +// bitmap because a TabCtrl item cannot interleave images into its text; the tab's text is +// therefore empty. Transparent background like the other chips. +wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, int swatch_sz) +{ + const std::vector comps = mixed_slot_components(full, slot); + if (comps.empty()) + return wxNullBitmap; + const auto* grad_opt = full.opt("filament_mixed_gradient"); + const bool is_gradient = grad_opt != nullptr && slot < grad_opt->size() && grad_opt->values[slot]; + const auto* ratios_opt = full.opt("filament_mixed_sublayer_ratios"); + std::vector ratio_pct(comps.size(), 100.0 / comps.size()); + if (!is_gradient && ratios_opt != nullptr && slot < ratios_opt->size()) { + const std::vector rs = parse_mixed_ratios(ratios_opt->values[slot], comps.size()); + for (size_t i = 0; i < rs.size() && i < comps.size(); ++i) + ratio_pct[i] = rs[i] * 100.0; + } + + const auto* colours = full.opt("filament_colour"); + const wxString lead_sep = wxString::FromUTF8(":"); + const wxString comp_sep = is_gradient ? wxString::FromUTF8("\u2192") : wxString::FromUTF8("+"); + + // Phase 1 (layout): render every swatch and measure every text piece so the composite + // width is known before drawing; the dummy bitmap keeps GetTextExtent reliable. + struct Piece + { + enum Kind { Swatch, Text } kind{Text}; + wxBitmap bmp; + wxString text; + }; + std::vector pieces; + bool has_lead = false; + wxBitmap dummy(1, 1); + wxMemoryDC measure_dc; + measure_dc.SelectObject(dummy); + measure_dc.SetFont(::Label::Body_12); + const int gap = wxWindow::FromDIP(4, nullptr); + + auto push_swatch = [&](const std::string& hex, const std::string& label) { + wxBitmap* icon = get_extruder_color_icon(hex, label, swatch_sz, swatch_sz); + if (icon == nullptr) + return; + pieces.push_back({Piece::Swatch, *icon, wxString()}); + }; + auto push_text = [&](const wxString& text) { pieces.push_back({Piece::Text, wxNullBitmap, text}); }; + + { + const wxColour blend = mixed_filament_blend_color(full, slot); + const std::string blend_hex = blend.IsOk() ? + std::string(wxString::Format("#%02X%02X%02X", blend.Red(), blend.Green(), blend.Blue()).ToUTF8()) : + std::string("#808080"); + const size_t before = pieces.size(); + push_swatch(blend_hex, std::to_string(slot + 1)); + has_lead = pieces.size() > before; + } + for (size_t ci = 0; ci < comps.size(); ++ci) { + if (pieces.empty()) + break; + push_text(has_lead && pieces.size() == 1 ? lead_sep : comp_sep); // lead chip may have failed to render + std::string hex = "#D9D9D9"; + if (colours != nullptr && comps[ci] >= 1 && comps[ci] <= colours->size()) + hex = colours->values[comps[ci] - 1]; + const size_t before = pieces.size(); + push_swatch(hex, std::to_string(comps[ci])); + if (pieces.size() == before) + break; // swatch failed: stop cleanly before an orphaned separator/percent pair + if (!is_gradient) { + push_text(wxString::Format("%d%%", int(ratio_pct[ci] + 0.5))); + } + } + if (pieces.empty()) + return wxNullBitmap; + + int width = 0; + for (const Piece& p : pieces) + width += (p.kind == Piece::Swatch ? swatch_sz : measure_dc.GetTextExtent(p.text).x + gap); + + // Phase 2 (draw): transparent background like the page-header chips. + wxBitmap composite(width, swatch_sz); + wxMemoryDC memdc; +#ifdef __WXOSX__ + composite.UseAlpha(); + memdc.SelectObject(composite); +#else + { + wxImage img(width, swatch_sz); + img.InitAlpha(); + memset(img.GetAlpha(), 0, width * swatch_sz); + composite = wxBitmap(std::move(img)); + } + memdc.SelectObject(composite); +#endif + { +#ifdef __WXMSW__ + wxGCDC dc(memdc); +#else + wxDC& dc = memdc; +#endif + dc.SetBackgroundMode(wxTRANSPARENT); + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#262E30"))); + int x = 0; + for (const Piece& p : pieces) { + if (p.kind == Piece::Swatch) { + dc.DrawBitmap(p.bmp, x, 0); + x += swatch_sz; + } else { + const wxSize tsz = measure_dc.GetTextExtent(p.text); + dc.DrawText(p.text, x + gap / 2, (swatch_sz - tsz.y) / 2); + x += tsz.x + gap; + } + } + } + memdc.SelectObject(wxNullBitmap); + return composite; +} + } // namespace +PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_spec(const DynamicPrintConfig& full, size_t slot) +{ + MixedVisualSpec spec; + const std::vector comps = mixed_slot_components(full, slot); + if (comps.empty()) + return spec; + + const auto* grad_opt = full.opt("filament_mixed_gradient"); + spec.is_gradient = grad_opt != nullptr && slot < grad_opt->size() && grad_opt->values[slot]; + + const auto* colours = full.opt("filament_colour"); + for (unsigned int cid : comps) { + std::string hex = "#D9D9D9"; + if (colours != nullptr && cid >= 1 && cid <= colours->size()) + hex = colours->values[cid - 1]; + const wxColour c(hex); + spec.component_colours.push_back(c.IsOk() ? c : wxColour("#D9D9D9")); + } + + if (!spec.is_gradient) { + // Sublayer shares; parse_mixed_ratios already falls back to equal shares and + // normalizes to sum 1. + spec.ratios.assign(comps.size(), 1.0 / comps.size()); + if (const auto* ratios_opt = full.opt("filament_mixed_sublayer_ratios")) + if (slot < ratios_opt->size()) { + const std::vector rs = parse_mixed_ratios(ratios_opt->values[slot], comps.size()); + if (rs.size() == comps.size()) + spec.ratios = rs; + } + if (comps.size() == 3) + spec.tri_weights = spec.ratios; // the picker's barycentric shares + } else { + const Slic3r::GradientCurve curve = mixed_gradient_curve(full, slot); + constexpr int kSamples = 64; + for (int i = 0; i <= kSamples; ++i) { + const double t = double(i) / kSamples; + spec.gradient_samples.emplace_back(t, sample_gradient_curve(curve, t)); + } + for (const Slic3r::GradientAnchor& anchor : curve.points) + spec.gradient_anchors.emplace_back(anchor.x, anchor.y); + } + + spec.valid = true; + return spec; +} + PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, @@ -206,9 +439,12 @@ void PublishSettingsDialog::fit_to_content() static const wxSize BASE{600, 500}; static const wxSize CAP{1300, 850}; - int strip = m_outer_tabs->buttons_best_width(); - for (const SectionGroup& section : m_sections) - strip = std::max(strip, section.tabs->buttons_best_width()); + int strip = m_outer_tabs->GetFullSize(); + for (const SectionGroup& section : m_sections) { + strip = std::max(strip, section.tabs->GetFullSize()); + if (section.mixed_tabs != nullptr) + strip = std::max(strip, section.mixed_tabs->GetFullSize()); + } // Minimum width: whatever keeps every tab visible (never below the base). strip is device // pixels (Button min sizes); BASE/CAP are DIP and converted over. @@ -233,9 +469,9 @@ void PublishSettingsDialog::build_option_model() { // Structural / non-publishable keys, shared with the published-3MF overlay path. const std::set& denylist = publish_structural_keys(); - // Base keys already added in the print/printer sections. Printer rows share this set: - // per-extruder "#N" variants collapse to the first occurrence (acceptable MVP; the - // per-extruder context is lost in the UI). + // Base keys already added in the print section (dedup across pages/optgroups). The printer + // section keeps its own printer_added set keyed by the full per-extruder "#N" opt_id, so + // every extruder gets its own row (see Phase 1 below). std::set added; PresetBundle* bundle = wxGetApp().preset_bundle; @@ -244,6 +480,7 @@ void PublishSettingsDialog::build_option_model() m_info_nonsel = _L("No selected items..."); m_info_allsel = _L("All items selected..."); m_info_empty = _L("No matching items..."); + m_info_mix = _L("Mixed filament - published as a whole when \"Enable\" above is selected"); // Tab order differs from Section's enum order (Print, Printer, Material): the dialog // presents Printer, Filament, Process. @@ -272,16 +509,33 @@ void PublishSettingsDialog::build_option_model() }; // --- Phase 1: printer per-extruder retraction settings (first, mirroring the sidebar's - // Printer group), from the printer tab's "Extruder"/"Extruder N" pages. + // Printer group), from the printer tab's "Extruder"/"Extruder N" pages. One inner tab per + // extruder (e.g. "Left Extruder"/"Right Extruder" via Tab::translate_category), each holding + // that extruder's Retraction and Z-Hop rows with per-extruder "#N" values. { size_t g = section_group_for(Section::Printer); - category_index_for(_L("Extruder"), Section::Printer, g, 0); + std::set printer_added; for (Tab* tab : wxGetApp().tabs_list) { if (tab->m_type != Preset::TYPE_PRINTER) continue; for (const PageShp& page : tab->m_pages) { if (!page->title().StartsWith("Extruder")) continue; + // The extruder index of this page: its options are appended with the same + // "#N" opt_index (opt.second.second), so derive the tab's index from the first + // allowlisted option; skip the page when none is found (defensive). + int extruder_idx = -1; + for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { + if (optgroup->title != "Retraction" && optgroup->title != "Z-Hop") + continue; + for (const auto& opt : optgroup->opt_map()) + if (extruder_idx < 0) + extruder_idx = opt.second.second; + if (extruder_idx >= 0) + break; + } + if (extruder_idx < 0) + continue; const wxString page_title = Tab::translate_category(page->title(), tab->m_type); for (const ConfigOptionsGroupShp& optgroup : page->m_optgroups) { // Allowlist on the untranslated optgroup title; the "Retraction when @@ -292,17 +546,17 @@ void PublishSettingsDialog::build_option_model() for (const auto& opt : optgroup->opt_map()) { const std::string& opt_id = opt.first; const std::string& pure_key = opt.second.first; - // Per-extruder "#N" variants collapse to the first base key. The row - // stores the base key; GetPublishedKeys() later expands it back to one - // "#N" entry per extruder so the load side can apply per-extruder values. - if (!added.insert(pure_key).second) + // Rows are keyed by the full per-extruder "#N" opt_id so each extruder + // tab publishes its own value; GetPublishedKeys() emits the checked rows + // as-is. + if (!printer_added.insert(opt_id).second) continue; wxString label, value, unit; if (!option_text(opt_id, pure_key, label, value, unit)) continue; - size_t cat_index = category_index_for(_L("Extruder"), Section::Printer, g, 0); + size_t cat_index = category_index_for(page_title, Section::Printer, g, size_t(extruder_idx)); size_t sub_index = subcategory_index_for(cat_index, subcategory, optgroup->icon); - add_row_ui(pure_key, label, value, unit, cat_index, sub_index); + add_row_ui(opt_id, label, value, unit, cat_index, sub_index); } } } @@ -328,12 +582,28 @@ void PublishSettingsDialog::build_option_model() } if (overrides_page != nullptr) { + // Mixed-color slots are virtual: they carry no per-key Material/Retraction + // settings; their "Enable" toggle always embeds the mix definition (components, + // ratios, gradient). Detect them via the project-level flag. + const ConfigOptionBools* is_mixed_opt = full.opt("filament_is_mixed"); // One section per filament slot (a 4-slot printer shows 4 pages), each // disambiguated by its colour chip and slot identity while showing the bare name. for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { + const bool is_mixed = is_mixed_opt != nullptr && slot < is_mixed_opt->size() && is_mixed_opt->values[slot]; 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, g, slot, identity); + // A mixed slot's title is its component composition (e.g. "1 (60%) + 2 + // (40%)"), not the cloned preset's name shown in the main GUI. + const wxString title = is_mixed ? mixed_filament_label(full, slot) : material_title(slot, bundle, full); + const size_t category_index = category_index_for(title, Section::Material, g, slot, identity, is_mixed); + + if (is_mixed) { + // A mixed slot publishes as one unit (its definition); nothing to select + // per-key. Its component filaments are auto-enabled + Full Published when + // "Enable" is checked (see on_enable_toggle). Its page still shows what + // would be published: a ratio bar, or the gradient graph. + add_mixed_visual(category_index, make_mixed_visual_spec(full, slot)); + continue; + } // Material requirement rows: an optional filament colour and/or a // vendor-agnostic material type for this slot, in their own optgroup so they @@ -414,25 +684,37 @@ void PublishSettingsDialog::build_option_model() } } - // Pre-check the dirty (modified) settings and mark them bold (base-key match, across all + // Pre-check the dirty (modified) settings and mark them bold (per-slot match, across all // sections; collect_dirty_settings_keys unions the prints, printers and filaments). - std::set dirty_base; + // Dirty keys carry a "#N" per-extruder/per-slot suffix (deep_diff), so the base key alone + // cannot distinguish which extruder/filament slot changed: match the row's exact key, and + // for material rows (base key + per-slot value) the base key plus the section's slot. + std::set dirty_keys; for (const std::string& key : collect_dirty_settings_keys(*wxGetApp().preset_bundle)) - dirty_base.insert(publish_base_key(key)); + dirty_keys.insert(key); for (Row& row : m_rows) { // The Color/Type requirement rows are not "dirty overrides": never auto-checked. if (row.kind != RowKind::Setting) continue; - std::string base = publish_base_key(row.key); - row.dirty = dirty_base.count(base) > 0; + bool dirty = dirty_keys.count(row.key) != 0; + if (!dirty && row.section == Section::Material) + dirty = dirty_keys.count(publish_base_key(row.key) + "#" + std::to_string(m_categories[row.inner_index].filament_slot)) != 0; + row.dirty = dirty; if (row.dirty) { row.check->SetValue(true); set_row_bold(row, true); } } - // 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. + // Wire the "Enable" checkboxes: toggling one reveals/hides the slot's settings below the + // header (and, for a mixed slot, auto-selects its components). 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].enable_check != nullptr) + m_categories[c].enable_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_enable_toggle(c); }); + + // Wire the "Full Publish" checkboxes (physical slots): toggling one disables/enables the + // material's rows. for (size_t c = 0; c < m_categories.size(); ++c) if (m_categories[c].full_check != nullptr) m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); }); @@ -487,6 +769,18 @@ size_t PublishSettingsDialog::section_group_for(Section kind) section.tabs->SetFont(Label::Body_14); section.tabs->SetBackgroundColour(GetBackgroundColour()); page_sizer->Add(section.tabs, 0, wxEXPAND); + // Mixed-color filament slots get a second tab strip below the physical filament tabs; only + // the Material section has them. + if (kind == Section::Material) { + section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); + section.mixed_tabs->SetFont(Label::Body_14); + section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); + // The mixed tabs carry full swatch compositions: give them extra room to breathe so + // neighbouring compositions do not read as one long row (must precede AppendItem). + section.mixed_tabs->SetItemSpace(FromDIP(5)); + page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); + section.mixed_tabs->Hide(); + } section.page_host = new wxPanel(section.page, wxID_ANY); section.page_host->SetBackgroundColour(GetBackgroundColour()); section.page_host_sizer = new wxBoxSizer(wxVERTICAL); @@ -505,14 +799,20 @@ size_t PublishSettingsDialog::section_group_for(Section kind) } size_t PublishSettingsDialog::category_index_for( - const wxString& title, Section section, size_t group, size_t source_index, const PublishMaterialIdentity& identity) + const wxString& title, Section section, size_t group, size_t source_index, const PublishMaterialIdentity& identity, bool is_mixed) { - for (size_t i : m_sections[group].categories) { - Category& existing = m_categories[i]; - if (existing.title == title && existing.section == section && existing.source_index == source_index && - existing.filament_id == identity.id && existing.filament_type == identity.type && existing.filament_vendor == identity.vendor) + // Dedup across both tab rows (physical + mixed); mixed slots are never duplicated anyway. + auto match = [&](const Category& existing) { + return existing.title == title && existing.section == section && existing.source_index == source_index && + existing.filament_id == identity.id && existing.filament_type == identity.type && + existing.filament_vendor == identity.vendor; + }; + for (size_t i : m_sections[group].categories) + if (match(m_categories[i])) + return i; + for (size_t i : m_sections[group].mixed_categories) + if (match(m_categories[i])) return i; - } Category category; category.title = title; @@ -523,29 +823,58 @@ size_t PublishSettingsDialog::category_index_for( category.filament_vendor = identity.vendor; category.filament_id = identity.id; category.filament_slot = source_index; + category.is_mixed = is_mixed; category.page = new wxPanel(m_sections[group].page_host, wxID_ANY); category.page->SetBackgroundColour(GetBackgroundColour()); auto* page_sizer = new wxBoxSizer(wxVERTICAL); - // The slot's colour chip decorates both the section header and the inner tab. + // The physical slot's colour chip decorates the page header and the inner tab; it carries + // the 1-based slot number, mirroring the main GUI's filament swatches. A mixed slot has no + // header at all: its page is just the Enable toggle above the (always visible) definition + // preview - the identification lives in the tab strip's composition bitmap. std::string hex; - if (section == Section::Material) - hex = filament_color_hex(wxGetApp().preset_bundle->full_config(), source_index); + std::string chip_label; + if (section == Section::Material && !is_mixed) { + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + hex = filament_color_hex(full, source_index); + chip_label = std::to_string(source_index + 1); + } if (section == Section::Material) { - auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); - if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) { - 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)); + if (is_mixed) { + // No chip/title: the lone "Enable" checkbox tops the page. + auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL); + category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable")); + category.enable_check->SetFont(Label::Body_13); + category.enable_check->SetToolTip(_L("Publish this mixed filament and enable + Full Publish its component filaments")); + enable_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL); + page_sizer->Add(enable_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); + } else { + // Line 1: [chip] [title] [Enable]. The Enable checkbox gates the whole slot: while + // it is unchecked nothing below the title is shown and nothing of it is published. + auto* header_sizer = new wxBoxSizer(wxHORIZONTAL); + if (wxBitmap* chip = get_extruder_color_icon(hex, chip_label, FromDIP(20), FromDIP(20))) { + 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.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.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable")); + category.enable_check->SetFont(Label::Body_13); + category.enable_check->SetToolTip(_L("Publish this filament slot in the 3MF file")); + header_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10)); + page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); + + // Line 2: the "Full Publish" toggle, on its own line below the title (hidden until + // the slot is enabled). + auto* full_sizer = new wxBoxSizer(wxHORIZONTAL); + 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")); + full_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL); + category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); } - 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)); } category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); @@ -555,28 +884,53 @@ size_t PublishSettingsDialog::category_index_for( category.scroll->SetSizer(category.list_sizer); category.scroll->DisableFocusFromKeyboard(); category.scroll->Bind(wxEVT_RIGHT_DOWN, &PublishSettingsDialog::show_menu, this); - category.info = new wxStaticText(category.scroll, wxID_ANY, m_info_empty); + category.info = new wxStaticText(category.scroll, wxID_ANY, is_mixed ? m_info_mix : m_info_empty); category.info->SetFont(Label::Body_13); category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10)); category.info->Hide(); page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4)); + // A material slot starts disabled: its rows (and its Full Publish line) stay hidden until + // "Enable" is checked. + if (section == Section::Material) + category.scroll->Hide(); category.page->SetSizer(page_sizer); category.page->Hide(); const size_t category_index = m_categories.size(); m_categories.push_back(std::move(category)); - m_sections[group].categories.push_back(category_index); + // Mixed slots live in a second tab row below the physical filament tabs. + if (is_mixed) + m_sections[group].mixed_categories.push_back(category_index); + else + m_sections[group].categories.push_back(category_index); if (section == Section::Material) { - if (wxBitmap* chip = get_extruder_color_icon(hex, "", FromDIP(12), FromDIP(12))) - m_sections[group].tabs->AppendItem(title, *chip); - else - m_sections[group].tabs->AppendItem(title); + TabCtrl* target = is_mixed ? m_sections[group].mixed_tabs : m_sections[group].tabs; + if (is_mixed) { + // The tab's whole composition (mix chip + components + percents) lives in one + // bitmap; the text is empty because a TabCtrl item cannot interleave images into + // its text. + const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); + const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full_cfg, source_index, FromDIP(20)); + if (tab_bmp.IsOk()) + target->AppendItem(wxString(), tab_bmp); + else + target->AppendItem(title); + } else if (wxBitmap* chip = get_extruder_color_icon(hex, chip_label, FromDIP(20), FromDIP(20))) { + target->AppendItem(title, *chip); + } else { + target->AppendItem(title); + } } else { m_sections[group].tabs->AppendItem(title); } m_sections[group].page_host_sizer->Add(m_categories[category_index].page, 1, wxEXPAND); - if (m_sections[group].selected_inner < 0) + if (is_mixed) { + if (m_sections[group].selected_mixed < 0) + m_sections[group].selected_mixed = 0; + m_sections[group].mixed_tabs->Show(); + } else if (m_sections[group].selected_inner < 0) { m_sections[group].selected_inner = 0; + } return category_index; } @@ -636,7 +990,9 @@ void PublishSettingsDialog::add_row_ui(const std::string& key, 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))) { + // The colour swatch carries this slot's 1-based number, like the main GUI swatches. + const std::string chip_label = std::to_string(category.filament_slot + 1); + if (wxBitmap* chip = get_extruder_color_icon(value.ToStdString(), chip_label, FromDIP(20), FromDIP(20))) { current.color_chip = new wxStaticBitmap(category.scroll, wxID_ANY, *chip); row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); } @@ -661,6 +1017,320 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index) m_rows[r].check->Enable(!full); } +void PublishSettingsDialog::on_enable_toggle(size_t category_index) +{ + Category& cat = m_categories[category_index]; + const bool enabled = cat.enable_check->GetValue(); + + // A published mixed filament needs its component filaments published too: mark the slots it + // uses as "Enable"d and Full Published so the mix's physical components always ship their + // identities. + if (cat.is_mixed && enabled) { + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + const std::vector components = mixed_slot_components(full, cat.filament_slot); + for (const unsigned int component : components) { + // Components are 1-based physical filament indices. + const size_t component_slot = size_t(component) - 1; + for (size_t c = 0; c < m_categories.size(); ++c) { + Category& comp_cat = m_categories[c]; + if (comp_cat.section != Section::Material || comp_cat.is_mixed || comp_cat.filament_slot != component_slot) + continue; + if (comp_cat.enable_check != nullptr) + comp_cat.enable_check->SetValue(true); + if (comp_cat.full_check != nullptr) + comp_cat.full_check->SetValue(true); + on_full_toggle(c); + } + } + } + + // Reveal/hide everything below the slot's header (rows, info, Full Publish line) and refresh + // the visibility of the auto-selected component slots. + apply_visibility(); + if (cat.page != nullptr) + cat.page->GetSizer()->Layout(); +} + +void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedVisualSpec& spec) +{ + if (category_index >= m_categories.size() || !spec.valid) + return; + Category& category = m_categories[category_index]; + if (category.page == nullptr || category.page->GetSizer() == nullptr || category.scroll == nullptr || spec.component_colours.empty()) + return; + + auto* viz = new wxPanel(category.page, wxID_ANY); + viz->SetBackgroundStyle(wxBG_STYLE_PAINT); + // Per-panel fill-bitmap cache for the ternary branch; rebuilt only when size or colours + // change (shared_ptr keeps the lifetime independent of this method's locals). + struct TriCache + { + wxBitmap bmp; + wxSize sz{0, 0}; + wxColour c0, c1, c2; + }; + auto tri_cache = std::make_shared(); + // Theme colours and DIP metrics are resolved inside the paint handler so dark-mode toggles + // and DPI changes are picked up on the next repaint without any explicit listener. + viz->Bind(wxEVT_PAINT, [this, panel = viz, spec, tri_cache](wxPaintEvent&) { + const wxColour bg = StateColor::darkModeColorFor(*wxWHITE); + wxBufferedPaintDC pdc(panel); + pdc.SetBackground(wxBrush(bg)); + pdc.Clear(); + const wxRect rc = panel->GetClientRect(); + if (rc.width <= 0 || rc.height <= 0) + return; + + const size_t n = spec.component_colours.size(); + + if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) { + // Ternary mix: a read-only miniature of the MixedFilamentDialog's triangle picker. + // Per-pixel barycentric fill is cached into a bitmap keyed on size + colours; the + // marker and labels are redrawn on top every paint. + const wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); + const wxColour outline = StateColor::darkModeColorFor(wxColour("#CECECE")); + const wxColour ring = StateColor::darkModeColorFor(wxColour("#262E30")); + const wxColour label_c = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 + const double margin_dip = 24.0; + auto& cache = *tri_cache; + + auto vertices_for = [&](const wxSize& sz) -> std::tuple { + const double pw = sz.GetWidth(), ph = sz.GetHeight(); + const int margin = FromDIP(int(margin_dip)); + const double avail = std::min(pw, ph) - 2.0 * margin; + const double side = avail; + const double tri_h = side * std::sqrt(3.0) / 2.0; + const double cx = pw / 2.0; + const double top_y = (ph - tri_h) / 2.0; + return {{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}; + }; + + pdc.SetFont(::Label::Body_12); + const wxColour& c0 = spec.component_colours[0]; + const wxColour& c1 = spec.component_colours[1]; + const wxColour& c2 = spec.component_colours[2]; + + if (!cache.bmp.IsOk() || cache.sz != rc.GetSize() || cache.c0 != c0 || cache.c1 != c1 || cache.c2 != c2) { + auto [v0, v1, v2] = vertices_for(rc.GetSize()); + cache.bmp = wxBitmap(rc.width, rc.height, 32); + wxMemoryDC mdc(cache.bmp); + mdc.SetBrush(wxBrush(tri_bg)); + mdc.SetPen(*wxTRANSPARENT_PEN); + mdc.DrawRectangle(0, 0, rc.width, rc.height); + + const int min_y = int(std::min({v0.y, v1.y, v2.y})); + const int max_y = int(std::max({v0.y, v1.y, v2.y})); + const int min_x = int(std::min({v0.x, v1.x, v2.x})); + const int max_x = int(std::max({v0.x, v1.x, v2.x})); + for (int py = min_y; py <= max_y; ++py) + for (int px = min_x; px <= max_x; ++px) { + const TriPoint p = {double(px), double(py)}; + if (!tri_contains(p, v0, v1, v2)) + continue; + double w0, w1, w2; + tri_barycentric(p, v0, v1, v2, w0, w1, w2); + unsigned char mr, mg, mb; + if (w0 + w1 > 1e-6) { + float t01 = static_cast(w1 / (w0 + w1)); + Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, + &mb); + Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), static_cast(w2), &mr, &mg, &mb); + } else { + mr = c2.Red(); + mg = c2.Green(); + mb = c2.Blue(); + } + mdc.SetPen(wxPen(wxColour(mr, mg, mb))); + mdc.DrawPoint(px, py); + } + + mdc.SetPen(wxPen(outline, 1)); + mdc.SetBrush(*wxTRANSPARENT_BRUSH); + const wxPoint pts[3] = {{int(v0.x), int(v0.y)}, {int(v1.x), int(v1.y)}, {int(v2.x), int(v2.y)}}; + mdc.DrawPolygon(3, pts); + mdc.SelectObject(wxNullBitmap); + + cache.sz = rc.GetSize(); + cache.c0 = c0; + cache.c1 = c1; + cache.c2 = c2; + } + pdc.DrawBitmap(cache.bmp, 0, 0); + + // Published-ratio marker (read-only twin of the editor's drag handle). + { + auto [v0, v1, v2] = vertices_for(rc.GetSize()); + const double w0 = spec.tri_weights[0], w1 = spec.tri_weights[1], w2 = spec.tri_weights[2]; + const int hx = int(w0 * v0.x + w1 * v1.x + w2 * v2.x); + const int hy = int(w0 * v0.y + w1 * v1.y + w2 * v2.y); + pdc.SetBrush(*wxWHITE_BRUSH); + pdc.SetPen(wxPen(ring, FromDIP(2))); + pdc.DrawCircle(hx, hy, FromDIP(5)); + + // Percent label beside each vertex. + for (int i = 0; i < 3; ++i) { + const wxString text = wxString::Format("%d%%", int(std::lround(spec.tri_weights[i] * 100.0))); + const wxSize tsz = pdc.GetTextExtent(text); + const TriPoint vtx = (i == 0) ? v0 : (i == 1) ? v1 : v2; + int lx = int(vtx.x - tsz.GetWidth() / 2.0); + int ly = (i == 0) ? int(vtx.y - tsz.GetHeight()) : int(vtx.y + FromDIP(3)); + ly = std::clamp(ly, 0, rc.height - tsz.GetHeight()); + lx = std::clamp(lx, 0, rc.width - tsz.GetWidth()); + pdc.SetTextForeground(label_c); + pdc.DrawText(text, lx, ly); + } + } + } else if (!spec.is_gradient) { + // Stacked ratio bar: one solid segment per component, widths proportional to the + // published shares. Integer widths accumulate left to right; the last segment takes + // the rounding remainder so the bar always fills exactly. + std::vector shares = spec.ratios; + double total = 0.0; + for (double r : shares) + total += r; + if (shares.size() != n || total <= 0.0) { + shares.assign(n, 1.0 / n); + total = 1.0; + } + auto share_to_px = [&](double share_sum) { return rc.x + int(std::lround(share_sum / total * double(rc.width))); }; + std::vector segs(n); + int x0 = rc.x; + for (size_t i = 0; i < n; ++i) { + int x1 = rc.x + rc.width; + if (i + 1 < n) + x1 = share_to_px(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0)); + segs[i] = wxRect(x0, rc.y, std::max(1, x1 - x0), rc.height); + x0 = segs[i].GetRight() + 1; + } + + for (size_t i = 0; i < n; ++i) { + pdc.SetPen(*wxTRANSPARENT_PEN); + pdc.SetBrush(wxBrush(spec.component_colours[i])); + pdc.DrawRectangle(segs[i]); + } + pdc.SetBrush(*wxTRANSPARENT_BRUSH); + pdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1)); + pdc.DrawRectangle(rc); + + // Percent label centred in each segment wide enough to hold it. + pdc.SetFont(::Label::Body_12); + for (size_t i = 0; i < n; ++i) { + const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); + const wxSize tsz = pdc.GetTextExtent(text); + if (tsz.GetWidth() + FromDIP(4) > segs[i].GetWidth()) + continue; + // Label contrast follows the swatch itself, not the theme. + const wxColour& c = spec.component_colours[i]; + const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue(); + pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE); + pdc.DrawText(text, segs[i].x + (segs[i].GetWidth() - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2); + } + } else { + // Gradient: compact "Material Ratio" over "Model Height" graph, a read-only + // miniature of the GradientCurveEditor plot. Component order matches the config; + // the second component's curve is the mirror of the first's. + const wxColour grid_color = StateColor::darkModeColorFor(wxColour(238, 238, 238)); // grey 300 + const wxColour axis_color = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 + const wxColour label_muted = StateColor::darkModeColorFor(wxColour(107, 107, 107)); + const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); + + const int pad_left = FromDIP(34); + const int pad_right = FromDIP(10); + const int pad_top = FromDIP(18); + const int pad_bottom = FromDIP(16); + const wxRect plot(rc.x + pad_left, rc.y + pad_top, std::max(1, rc.width - pad_left - pad_right), + std::max(1, rc.height - pad_top - pad_bottom)); + + constexpr int kGridDivisions = 5; + pdc.SetPen(wxPen(grid_color, 1)); + for (int i = 0; i <= kGridDivisions; ++i) { + const int gx = plot.x + plot.width * i / kGridDivisions; + const int gy = plot.y + plot.height * i / kGridDivisions; + pdc.DrawLine(gx, plot.y, gx, plot.y + plot.height); + pdc.DrawLine(plot.x, gy, plot.x + plot.width, gy); + } + + // Axes with small filled arrowheads along the plot's left and bottom edges. + const int arrow_len = FromDIP(7); + const int arrow_half = FromDIP(3); + pdc.SetPen(wxPen(axis_color, 1)); + pdc.SetBrush(wxBrush(axis_color)); + pdc.DrawLine(plot.x, plot.y + plot.height, plot.x, plot.y); + { + wxPoint tri[3] = {wxPoint(plot.x, plot.y - arrow_len), wxPoint(plot.x - arrow_half, plot.y), + wxPoint(plot.x + arrow_half, plot.y)}; + pdc.DrawPolygon(3, tri); + } + pdc.DrawLine(plot.x, plot.y + plot.height, plot.x + plot.width, plot.y + plot.height); + { + wxPoint tri[3] = {wxPoint(plot.x + plot.width + arrow_len, plot.y + plot.height), + wxPoint(plot.x + plot.width, plot.y + plot.height - arrow_half), + wxPoint(plot.x + plot.width, plot.y + plot.height + arrow_half)}; + pdc.DrawPolygon(3, tri); + } + + wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); + pdc.SetFont(label_font); + pdc.SetTextForeground(label_muted); + pdc.DrawText(_L("Material Ratio"), plot.x + FromDIP(4), plot.y - pdc.GetTextExtent(_L("Material Ratio")).GetHeight()); + const wxString height_title = _L("Model Height"); + pdc.DrawText(height_title, plot.x + plot.width - pdc.GetTextExtent(height_title).GetWidth(), plot.y + plot.height + FromDIP(2)); + + if (spec.gradient_samples.size() >= 2 && n >= 2) { + auto curve_point = [&](double t, double ratio) { + return wxPoint(plot.x + int(std::lround(t * plot.width)), plot.y + int(std::lround((1.0 - ratio) * plot.height))); + }; + // First component's ratio solid, its mirror dashed-free twin for the other. + const wxColour& col_a = spec.component_colours[0]; + const wxColour& col_b = spec.component_colours[1]; + std::vector pts_a, pts_b; + pts_a.reserve(spec.gradient_samples.size()); + pts_b.reserve(spec.gradient_samples.size()); + for (const auto& [t, r] : spec.gradient_samples) { + pts_a.push_back(curve_point(t, r)); + pts_b.push_back(curve_point(t, 1.0 - r)); + } + pdc.SetPen(wxPen(col_b, 2)); + for (size_t i = 0; i + 1 < pts_b.size(); ++i) + pdc.DrawLine(pts_b[i], pts_b[i + 1]); + pdc.SetPen(wxPen(col_a, 2)); + for (size_t i = 0; i + 1 < pts_a.size(); ++i) + pdc.DrawLine(pts_a[i], pts_a[i + 1]); + + // Control-point anchors of the stored curve on the first component's line. + pdc.SetBrush(wxBrush(point_fill)); + pdc.SetPen(wxPen(col_a, 1)); + for (const auto& [t, r] : spec.gradient_anchors) { + const wxPoint c = curve_point(t, r); + pdc.DrawCircle(c, FromDIP(3)); + } + } + } + }); + + // Fixed DIP size, left-aligned: the visualization keeps its proportions no matter how the + // dialog is resized (the paint handler draws into whatever client rect the panel ends up + // with, so nothing else has to change). + const int viz_h = spec.is_gradient ? 150 : (spec.tri_weights.size() == 3 ? 180 : 30); + const wxSize viz_sz(FromDIP(240), FromDIP(viz_h)); + viz->SetMinSize(viz_sz); + viz->SetMaxSize(viz_sz); + // Parented to the page right above the scroll area, so it is always shown with the tab: + // the "Enable" toggle keeps gating only the rows/info below, never this preview. + wxSizer* page_sizer = category.page->GetSizer(); + int scroll_idx = -1; + for (size_t i = 0; i < page_sizer->GetChildren().size(); ++i) + if (page_sizer->GetChildren()[i]->GetWindow() == category.scroll) { + scroll_idx = int(i); + break; + } + if (scroll_idx < 0) + page_sizer->Add(viz, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(10)); // defensive: no scroll item + else + page_sizer->Insert(scroll_idx, viz, 0, wxLEFT | wxRIGHT | wxTOP, FromDIP(10)); +} + void PublishSettingsDialog::set_row_bold(Row& row, bool bold) { // Rebase on the dialog's body font so clearing bold restores the exact original font. @@ -681,13 +1351,18 @@ void PublishSettingsDialog::show_outer_page(size_t section_index) SectionGroup& old_section = m_sections[m_selected_outer]; if (old_section.selected_inner >= 0 && old_section.selected_inner < static_cast(old_section.categories.size())) save_scroll_position(m_categories[old_section.categories[old_section.selected_inner]]); + if (old_section.selected_mixed >= 0 && old_section.selected_mixed < static_cast(old_section.mixed_categories.size())) + save_scroll_position(m_categories[old_section.mixed_categories[old_section.selected_mixed]]); m_sections[m_selected_outer].page->Hide(); } m_selected_outer = static_cast(section_index); SectionGroup& section = m_sections[section_index]; section.page->Show(); + // Restore whichever tab row (physical or mixed) was active. if (section.selected_inner >= 0) show_inner_page(section_index, section.selected_inner); + else if (section.selected_mixed >= 0) + show_mixed_page(section_index, section.selected_mixed); m_outer_host_sizer->Layout(); } @@ -702,11 +1377,43 @@ void PublishSettingsDialog::show_inner_page(size_t section_index, int inner_inde save_scroll_position(m_categories[section.categories[section.selected_inner]]); m_categories[section.categories[section.selected_inner]].page->Hide(); } + if (section.selected_mixed >= 0 && section.selected_mixed < static_cast(section.mixed_categories.size())) { + save_scroll_position(m_categories[section.mixed_categories[section.selected_mixed]]); + m_categories[section.mixed_categories[section.selected_mixed]].page->Hide(); + section.selected_mixed = -1; + } section.selected_inner = inner_index; Category& category = m_categories[section.categories[inner_index]]; category.page->Show(); category.scroll->FitInside(); category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y); + if (section.mixed_tabs != nullptr) + section.mixed_tabs->Unselect(); + section.page_host_sizer->Layout(); +} + +void PublishSettingsDialog::show_mixed_page(size_t section_index, int mixed_index) +{ + if (section_index >= m_sections.size()) + return; + SectionGroup& section = m_sections[section_index]; + if (mixed_index < 0 || mixed_index >= static_cast(section.mixed_categories.size())) + return; + if (section.selected_mixed >= 0 && section.selected_mixed < static_cast(section.mixed_categories.size())) { + save_scroll_position(m_categories[section.mixed_categories[section.selected_mixed]]); + m_categories[section.mixed_categories[section.selected_mixed]].page->Hide(); + } + if (section.selected_inner >= 0 && section.selected_inner < static_cast(section.categories.size())) { + save_scroll_position(m_categories[section.categories[section.selected_inner]]); + m_categories[section.categories[section.selected_inner]].page->Hide(); + section.selected_inner = -1; + } + section.selected_mixed = mixed_index; + Category& category = m_categories[section.mixed_categories[mixed_index]]; + category.page->Show(); + category.scroll->FitInside(); + category.scroll->Scroll(category.scroll_pos.x, category.scroll_pos.y); + section.tabs->Unselect(); section.page_host_sizer->Layout(); } @@ -724,12 +1431,25 @@ void PublishSettingsDialog::on_inner_tab_changed(size_t section_index, wxCommand show_inner_page(section_index, selection); } +void PublishSettingsDialog::on_mixed_tab_changed(size_t section_index, wxCommandEvent& event) +{ + const int selection = event.GetInt(); + if (section_index < m_sections.size() && selection >= 0 && + selection < static_cast(m_sections[section_index].mixed_categories.size())) + show_mixed_page(section_index, selection); +} + void PublishSettingsDialog::bind_tab_events() { m_outer_tabs->Bind(wxEVT_TAB_SEL_CHANGED, &PublishSettingsDialog::on_outer_tab_changed, this); - for (size_t section_index = 0; section_index < m_sections.size(); ++section_index) + for (size_t section_index = 0; section_index < m_sections.size(); ++section_index) { m_sections[section_index].tabs->Bind(wxEVT_TAB_SEL_CHANGED, [this, section_index](wxCommandEvent& event) { on_inner_tab_changed(section_index, event); }); + if (m_sections[section_index].mixed_tabs != nullptr) + m_sections[section_index].mixed_tabs->Bind(wxEVT_TAB_SEL_CHANGED, [this, section_index](wxCommandEvent& event) { + on_mixed_tab_changed(section_index, event); + }); + } } void PublishSettingsDialog::apply_filter(const wxString& filter_text) @@ -781,7 +1501,9 @@ void PublishSettingsDialog::refresh_filter(const wxString& filter) has_match = has_match || m_rows[r].matches_filter; category.info->Show(!has_match); if (!has_match) - category.info->SetLabel(pseudo ? (want_checked ? m_info_nonsel : m_info_allsel) : m_info_empty); + // A mixed slot has no selectable rows: always explain it is published whole. + category.info->SetLabel(category.is_mixed ? m_info_mix : + (pseudo ? (want_checked ? m_info_nonsel : m_info_allsel) : m_info_empty)); if (has_match && first_inner < 0) { first_outer = s; first_inner = static_cast(inner); @@ -789,6 +1511,13 @@ void PublishSettingsDialog::refresh_filter(const wxString& filter) if (static_cast(s) == m_selected_outer && static_cast(inner) == m_sections[s].selected_inner) active_has_match = has_match; } + // Mixed slots have no rows: they can never match a filter, so they always fall back to + // their explanatory hint (shown once the slot is enabled). + for (size_t mixed : m_sections[s].mixed_categories) { + Category& category = m_categories[mixed]; + category.info->Show(true); + category.info->SetLabel(m_info_mix); + } } if (!active_has_match && first_inner >= 0 && (m_selected_outer != static_cast(first_outer) || m_sections[first_outer].selected_inner != first_inner)) { @@ -815,18 +1544,26 @@ void PublishSettingsDialog::apply_visibility() { Freeze(); for (Category& category : m_categories) { + // A disabled material slot hides everything below its header (rows, info and the Full + // Publish line); enable it first to reveal its settings. + const bool enabled = category.section != Section::Material || + (category.enable_check != nullptr && category.enable_check->GetValue()); + if (category.scroll != nullptr) + category.scroll->Show(enabled); + if (category.full_line_item != nullptr) + category.full_line_item->Show(enabled); bool category_any = false; for (size_t r : category.rows) category_any = category_any || m_rows[r].matches_filter; - category.info->Show(!category_any); + category.info->Show(enabled && !category_any); for (Subcategory& sub : category.subs) { bool sub_any = false; for (size_t r : sub.rows) sub_any = sub_any || m_rows[r].matches_filter; if (sub.header != nullptr) - sub.item->Show(sub_any); + sub.item->Show(enabled && sub_any); for (size_t r : sub.rows) - m_rows[r].item->Show(m_rows[r].matches_filter); + m_rows[r].item->Show(enabled && m_rows[r].matches_filter); } category.list_sizer->Layout(); category.scroll->FitInside(); @@ -840,6 +1577,17 @@ void PublishSettingsDialog::select_all(bool value) for (Row& row : m_rows) if (row.check->IsEnabled()) row.check->SetValue(value); + // "All" also enables every material slot (so its rows/Full Publish become visible and the + // selection is actually exported); "None" disables them all again. + for (Category& cat : m_categories) + if (cat.section == Section::Material && cat.enable_check != nullptr) + cat.enable_check->SetValue(value); + // wxCheckBox::SetValue does not emit wxEVT_CHECKBOX, so re-run the enable handlers to + // propagate mixed-slot components and refresh visibility as if the user had clicked. + for (size_t c = 0; c < m_categories.size(); ++c) + if (m_categories[c].section == Section::Material) + on_enable_toggle(c); + apply_visibility(); } bool PublishSettingsDialog::row_is_visible(const Row& row) const @@ -936,28 +1684,13 @@ std::vector PublishSettingsDialog::GetPublishedKeys() const std::vector out; // Process and printer sections both travel through published_keys (the load-side overlay // applies process keys to the prints edited preset and the allowlisted printer keys to the - // printers edited preset); material keys use a separate API. - const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + // printers edited preset); material keys use a separate API. Printer rows carry the full + // per-extruder "#N" opt_id (built in Phase 1), so a checked row publishes exactly that + // extruder's value - per-extruder selection is independent. for (const Row& row : m_rows) { if ((row.section != Section::Print && row.section != Section::Printer) || !row.check->GetValue()) continue; - if (row.section == Section::Printer) { - // Printer rows store the base key (per-extruder "#N" variants collapsed during - // build). Publish every extruder element so the load side can apply per-extruder - // values even when the receiver has a different extruder count; a scalar printer - // key is published as-is. - const std::string base_key = publish_base_key(row.key); - if (const ConfigOption* opt = full.option(base_key)) { - if (const auto* vec = dynamic_cast(opt)) { - for (size_t i = 0; i < vec->size(); ++i) - out.push_back(base_key + "#" + std::to_string(i)); - } else { - out.push_back(base_key); - } - } - } else { - out.push_back(row.key); - } + out.push_back(row.key); } return out; } @@ -968,6 +1701,9 @@ std::vector PublishSettingsDialog::GetPublishedM for (const Category& cat : m_categories) { if (cat.section != Section::Material) continue; + // A slot that is not "Enable"d publishes nothing at all. + if (cat.enable_check != nullptr && !cat.enable_check->GetValue()) + continue; Slic3r::PublishedMaterialEntry entry; entry.filament_type = cat.filament_type; entry.filament_vendor = cat.filament_vendor; @@ -983,6 +1719,28 @@ std::vector PublishSettingsDialog::GetPublishedM entry.preset_name = preset->name; } } + // A mixed slot publishes as one unit: its definition (components, ratios, gradient) + // always travels (its Enable implies this), and the component filaments are enabled + + // Full Published by on_enable_toggle into their own entries. + if (cat.is_mixed) { + Slic3r::PublishedMaterialEntry mixed_entry; + mixed_entry.filament_type = cat.filament_type; + mixed_entry.filament_vendor = cat.filament_vendor; + mixed_entry.filament_id = cat.filament_id; + mixed_entry.slot = static_cast(cat.filament_slot); + // The mix's own colour (blended) is a property of the definition, not a + // requirement row; carry it so the receiver renders the swatch. + const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); + const std::string mix_color = filament_color_hex(full_cfg, cat.filament_slot); + if (!mix_color.empty()) { + mixed_entry.publish_color = true; + mixed_entry.color = mix_color; + } + for (const std::string& key : publish_mixed_keys()) + mixed_entry.keys.emplace_back(key); + out.push_back(std::move(mixed_entry)); + continue; + } // "Full Publish": the whole filament preset is embedded; type and colour are implicitly // published, and the per-key rows are disabled / their state ignored. if (cat.full_check != nullptr && cat.full_check->GetValue()) { @@ -1050,11 +1808,16 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) for (Category& cat : m_categories) { if (cat.full_check != nullptr) cat.full_check->Refresh(); + if (cat.enable_check != nullptr) + cat.enable_check->Refresh(); if (cat.title_label != nullptr) cat.title_label->Refresh(); if (cat.filament_color_chip != nullptr) { - if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, cat.filament_slot), "", FromDIP(12), FromDIP(12))) + // Mixed pages have no header chip; this only ever fires for physical slots. + if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, cat.filament_slot), std::to_string(cat.filament_slot + 1), + FromDIP(20), FromDIP(20))) { cat.filament_color_chip->SetBitmap(*chip); + } } cat.scroll->FitInside(); cat.list_sizer->Layout(); @@ -1066,12 +1829,17 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) if (section.icon_bmp.bmp().IsOk()) m_outer_tabs->SetItemBitmap(s, section.icon_bmp.bmp()); section.tabs->Rescale(); + if (section.mixed_tabs != nullptr) + section.mixed_tabs->Rescale(); } - // Refresh the per-row Color chips at the new DPI. + // Refresh the per-row Color chips at the new DPI (they carry the slot number too). 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))) + const std::string chip_label = (row.inner_index < m_categories.size()) ? + std::to_string(m_categories[row.inner_index].filament_slot + 1) : + ""; + if (wxBitmap* chip = get_extruder_color_icon(row.value.ToStdString(), chip_label, FromDIP(20), FromDIP(20))) row.color_chip->SetBitmap(*chip); } } @@ -1080,9 +1848,19 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) const Category& category = m_categories[category_index]; if (category.section != Section::Material) continue; - if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), "", FromDIP(12), FromDIP(12))) { - const SectionGroup& section = m_sections[category.group]; - const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); + const SectionGroup& section = m_sections[category.group]; + if (category.is_mixed) { + // The tab carries the full composition bitmap (mix chip + components + percents); + // the page header has no chip/title to refresh. + const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full, category.filament_slot, FromDIP(20)); + if (tab_bmp.IsOk()) { + const auto iter = std::find(section.mixed_categories.begin(), section.mixed_categories.end(), category_index); + if (iter != section.mixed_categories.end() && section.mixed_tabs != nullptr) + section.mixed_tabs->SetItemBitmap(static_cast(iter - section.mixed_categories.begin()), tab_bmp); + } + } else if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), + std::to_string(category.filament_slot + 1), FromDIP(20), FromDIP(20))) { + const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); if (iter != section.categories.end()) m_sections[category.group].tabs->SetItemBitmap(static_cast(iter - section.categories.begin()), *chip); } diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 2cf49e7eb9..e00bec37fd 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -7,8 +7,10 @@ #include "libslic3r/PublishSettings.hpp" #include +#include #include #include +#include #include #include @@ -105,9 +107,18 @@ private: wxPoint scroll_pos{0, 0}; wxStaticBitmap* filament_color_chip{nullptr}; 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 // (incl. Color/Type) are disabled. 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. std::string filament_type; std::string filament_vendor; @@ -118,6 +129,21 @@ private: std::vector 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 component_colours; // colour per component, in config order + std::vector ratios; // sublayer shares summing to ~1 (non-gradient) + std::vector tri_weights; // 3-component mixes: barycentric shares + std::vector> gradient_samples; + std::vector> gradient_anchors; + }; + // One outer TabCtrl page. Category entries are its inner tabs. struct SectionGroup { @@ -127,13 +153,24 @@ private: ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change wxPanel* page{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}; wxBoxSizer* page_host_sizer{nullptr}; int selected_inner{-1}; - std::vector 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 categories; // indices into m_categories (physical slots) + std::vector mixed_categories; // indices into m_categories (mixed slots) }; 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); // 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. @@ -147,13 +184,21 @@ private: void set_row_bold(Row& row, bool bold); // "Full Publish" toggled: disables/enables the material's rows. 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. size_t section_group_for(Section kind); size_t category_index_for(const wxString& title, Section section, size_t group, 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); void add_row_ui(const std::string& key, const wxString& label, @@ -167,8 +212,10 @@ private: 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 show_mixed_page(size_t section_index, int mixed_index); void on_outer_tab_changed(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; void apply_visibility(); void bind_tab_events(); @@ -190,6 +237,7 @@ private: wxString m_info_nonsel; wxString m_info_allsel; wxString m_info_empty; + wxString m_info_mix; // body hint shown for a mixed slot (published as a whole) ScalableBitmap m_search; ScalableBitmap m_menu; diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 7817c9111a..8fc432771e 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -2,8 +2,8 @@ #include -wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent ); -wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent ); +wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent); +wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent); BEGIN_EVENT_TABLE(TabCtrl, StaticBox) @@ -22,11 +22,7 @@ END_EVENT_TABLE() #define TAB_BUTTON_PADDING_Y 2 #define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y -TabCtrl::TabCtrl(wxWindow * parent, - wxWindowID id, - const wxPoint & pos, - const wxSize & size, - long style) +TabCtrl::TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style) : StaticBox(parent, id, pos, size, style) { #if 0 @@ -42,14 +38,11 @@ TabCtrl::TabCtrl(wxWindow * parent, hsizer->Add(sizer, 0, wxEXPAND | wxBOTTOM, border_width * 4); SetSizer(hsizer); Bind(wxEVT_COMMAND_BUTTON_CLICKED, &TabCtrl::buttonClicked, this); - //wxString reason; - //IsTransparentBackgroundSupported(&reason); + // wxString reason; + // IsTransparentBackgroundSupported(&reason); } -TabCtrl::~TabCtrl() -{ - delete images; -} +TabCtrl::~TabCtrl() { delete images; } int TabCtrl::GetSelection() const { return sel; } @@ -75,14 +68,11 @@ void TabCtrl::SelectItem(int item) Refresh(); } -void TabCtrl::Unselect() -{ - SelectItem(-1); -} +void TabCtrl::Unselect() { SelectItem(-1); } void TabCtrl::Rescale() { - for (auto & b : btns) + for (auto& b : btns) b->Rescale(); relayout(); } @@ -96,23 +86,20 @@ bool TabCtrl::SetFont(wxFont const& font) return true; } -int TabCtrl::AppendItem(const wxString &item, - int image, int selImage, - void * clientData) +int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* clientData) { - Button * btn = new Button(); + Button* btn = new Button(); btn->Create(this, item, "", wxBORDER_NONE); btn->SetFont(GetFont()); - btn->SetTextColor(StateColor( - std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), - std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal))); + btn->SetTextColor( + StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal))); btn->SetBackgroundColor(StateColor()); btn->SetCornerRadius(0); btn->SetPaddingSize({TAB_BUTTON_PADDING}); btns.push_back(btn); if (btns.size() > 1) 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); relayout(); return btns.size() - 1; @@ -144,7 +131,7 @@ bool TabCtrl::DeleteItem(int item) sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); 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(); if (selection_changed) { @@ -167,14 +154,12 @@ void TabCtrl::DeleteAllItems() unsigned int TabCtrl::GetCount() const { return btns.size(); } -wxString TabCtrl::GetItemText(unsigned int item) const -{ - return item < btns.size() ? btns[item]->GetLabel() : wxString{}; -} +wxString TabCtrl::GetItemText(unsigned int item) const { 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); } @@ -188,61 +173,59 @@ void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap) bool TabCtrl::GetItemBold(unsigned int item) const { - if (item >= btns.size()) return false; + if (item >= btns.size()) + return false; return btns[item]->GetFont() == 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]->Rescale(); } void* TabCtrl::GetItemData(unsigned int item) const { - if (item >= btns.size()) return nullptr; + if (item >= btns.size()) + return nullptr; return btns[item]->GetClientData(); } void TabCtrl::SetItemData(unsigned int item, void* clientData) { - if (item >= btns.size()) return; + if (item >= btns.size()) + return; btns[item]->SetClientData(clientData); } void TabCtrl::AssignImageList(wxImageList* imageList) { - if (images == imageList) return; + if (images == imageList) + return; delete images; 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); } -int TabCtrl::GetFirstVisibleItem() const -{ - return btns.size() == 0 ? -1 : 0; -} +int TabCtrl::GetFirstVisibleItem() const { return btns.size() == 0 ? -1 : 0; } -int TabCtrl::GetNextVisible(int item) const -{ - return ++item < btns.size() ? item : -1; -} +int TabCtrl::GetNextVisible(int item) const { return ++item < btns.size() ? item : -1; } -bool TabCtrl::IsVisible(unsigned int item) const -{ - return true; -} +bool TabCtrl::IsVisible(unsigned int item) const { return true; } void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags) { wxWindow::DoSetSize(x, y, width, height, sizeFlags); - if (sizeFlags & wxSIZE_USE_EXISTING) return; + if (sizeFlags & wxSIZE_USE_EXISTING) + return; 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) { - if (nMsg == WM_GETDLGCODE) { return DLGC_WANTARROWS; } + if (nMsg == WM_GETDLGCODE) { + return DLGC_WANTARROWS; + } return wxWindow::MSWWindowProc(nMsg, wParam, lParam); } @@ -259,15 +244,15 @@ WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) void TabCtrl::relayout() { int offset = 10; - int item = sel + 1; - int first = 0; + int item = sel + 1; + int first = 0; 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()) - offset += btns[item]->GetMinSize().x + TAB_BUTTON_SPACE * 2; - int width = GetSize().x; + offset += btns[item]->GetMinSize().x + item_space * 2; + int width = GetSize().x; 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) { sizer->Show(i * 2 + 1, false); sizer->Show(i * 2 + 2, false); @@ -288,23 +273,32 @@ void TabCtrl::relayout() sizer->GetItem(i * 2 + 2)->SetMinSize({0, 0}); } if (item >= btns.size()) - -- item; + --item; // 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}); 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. int width = 10; - for (const Button *btn : btns) - width += btn->GetMinSize().x + TAB_BUTTON_SPACE * 2; + for (const Button* btn : btns) + width += btn->GetMinSize().x + item_space * 2; return width; } -void TabCtrl::buttonClicked(wxCommandEvent &event) +void TabCtrl::buttonClicked(wxCommandEvent& event) { SetFocus(); auto btn = event.GetEventObject(); @@ -312,7 +306,7 @@ void TabCtrl::buttonClicked(wxCommandEvent &event) SelectItem(iter == btns.end() ? -1 : iter - btns.begin()); } -void TabCtrl::keyDown(wxKeyEvent &event) +void TabCtrl::keyDown(wxKeyEvent& event) { switch (event.GetKeyCode()) { case WXK_UP: @@ -331,11 +325,13 @@ void TabCtrl::keyDown(wxKeyEvent &event) void TabCtrl::doRender(wxDC& dc) { wxSize size = GetSize(); - int states = state_handler.states(); - if (sel < 0) { return; } + int states = state_handler.states(); + if (sel < 0) { + return; + } - auto x1 = btns[sel]->GetPosition().x; - auto x2 = x1 + btns[sel]->GetSize().x; + auto x1 = btns[sel]->GetPosition().x; + auto x2 = x1 + btns[sel]->GetSize().x; const int BS2 = (1 + border_width) / 2; #if 0 const int BS = border_width / 2; diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index e6a990243e..0d3606aca7 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -3,33 +3,30 @@ #include "Button.hpp" -wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent ); -wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent ); +wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent); +wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent); class TabCtrl : public StaticBox { std::vector btns; wxImageList* images = nullptr; - wxBoxSizer * sizer = nullptr; + wxBoxSizer* sizer = nullptr; int sel = -1; wxFont bold; + int item_space = 2; // space around each button, both sides (SetItemSpace) public: - TabCtrl(wxWindow * parent, - wxWindowID id, - const wxPoint & pos = wxDefaultPosition, - const wxSize & size = wxDefaultSize, - long style = 0); + TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0); ~TabCtrl(); public: - virtual bool SetFont(wxFont const & font) override; + virtual bool SetFont(wxFont const& font) override; public: - 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, int image = -1, int selImage = -1, void* clientData = nullptr); + int AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData = nullptr); bool DeleteItem(int item); @@ -37,7 +34,7 @@ public: unsigned int GetCount() const; - int GetSelection() const; + int GetSelection() const; void SelectItem(int item); @@ -46,16 +43,16 @@ public: virtual void Rescale(); wxString GetItemText(unsigned int item) const; - void SetItemText(unsigned int item, wxString const &value); - void SetItemBitmap(unsigned int item, const wxBitmap& bitmap); + void SetItemText(unsigned int item, wxString const& value); + void SetItemBitmap(unsigned int item, const wxBitmap& bitmap); - bool GetItemBold(unsigned int item) const; - void SetItemBold(unsigned int item, bool bold); + bool GetItemBold(unsigned int item) const; + void SetItemBold(unsigned int item, bool bold); - void* GetItemData(unsigned int item) const; - void SetItemData(unsigned int item, void *clientData); - - void AssignImageList(wxImageList *imageList); + void* GetItemData(unsigned int item) const; + void SetItemData(unsigned int item, void* clientData); + + void AssignImageList(wxImageList* imageList); void SetItemTextColour(unsigned int item, const StateColor& col); @@ -64,8 +61,11 @@ public: int GetNextVisible(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). - int buttons_best_width() const; + // Extra space around each tab button (in px on both sides). Defaults to the control-wide + // standard; call before appending items so every button picks it up. + void SetItemSpace(int space); + + int GetFullSize() const; private: virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override; @@ -76,10 +76,10 @@ private: void relayout(); - void buttonClicked(wxCommandEvent & event); - void keyDown(wxKeyEvent &event); + void buttonClicked(wxCommandEvent& event); + void keyDown(wxKeyEvent& event); - void doRender(wxDC & dc) override; + void doRender(wxDC& dc) override; // some useful events bool sendTabCtrlEvent(bool changing = false); diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index a459bfa62d..37147b93b8 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -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("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + full_cfg.opt("filament_colour")->values = { "#111111", "#222222", "#333333" }; + full_cfg.opt("filament_is_mixed")->values = { 0, 0, 1 }; + full_cfg.opt("filament_mixed_components")->values = { "", "", "1,2" }; + full_cfg.opt("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" }; + full_cfg.opt("filament_mixed_gradient")->values = { 0, 0, 1 }; + full_cfg.opt("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" }; + full_cfg.opt("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" }; + full_cfg.opt("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("filament_is_mixed")->values == std::vector{ 0, 0, 1 }); + const auto& components = filtered_cfg.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 3); + CHECK(components[2] == "1,2"); + CHECK(filtered_cfg.opt("filament_mixed_sublayer_ratios")->values[2] == "0.6,0.4"); + CHECK(filtered_cfg.opt("filament_mixed_gradient_curve")->values[2] == "0,0.1|1,0.9"); + CHECK(filtered_cfg.opt("filament_mixed_gradient")->values[2]); + CHECK(filtered_cfg.opt("filament_mixed_gradient_per_part")->values[2]); + } + THEN("the non-published slots are masked to their defaults") { + CHECK(filtered_cfg.opt("filament_mixed_components")->values[0] == ""); + CHECK(filtered_cfg.opt("filament_mixed_components")->values[1] == ""); + CHECK(filtered_cfg.opt("filament_is_mixed")->values[0] == 0); + CHECK(filtered_cfg.opt("filament_is_mixed")->values[1] == 0); + } + THEN("the identity keys stay present") { + REQUIRE(filtered_cfg.option("filament_colour") != nullptr); + } + } + } +} + diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 01abba75bf..d4a1e04e0c 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -2545,3 +2545,252 @@ TEST_CASE("Published 3MF applies per-extruder printer keys across extruder-count 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("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#0000FF", "#800080" }; + config.opt("filament_type")->values = { "PLA", "PETG", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("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("filament_is_mixed")->values = { 0, 0, 1 }; + config.opt("filament_mixed_components")->values = { "", "", "1,2" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" }; + config.opt("filament_mixed_gradient")->values = { 0, 0, 1 }; + config.opt("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" }; + config.opt("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" }; + config.opt("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("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 3); + CHECK(is_mixed[2]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 3); + CHECK(components[2] == "1,2"); + const auto &ratios = bundle.project_config.opt("filament_mixed_sublayer_ratios")->values; + REQUIRE(ratios.size() == 3); + CHECK(ratios[2] == "0.6,0.4"); + const auto &gradient = bundle.project_config.opt("filament_mixed_gradient")->values; + CHECK(gradient[2]); + const auto &range = bundle.project_config.opt("filament_mixed_gradient_range")->values; + CHECK(range[2] == "0.9,0.1"); + const auto &curve = bundle.project_config.opt("filament_mixed_gradient_curve")->values; + CHECK(curve[2] == "0,0.1|1,0.9"); + const auto &per_part = bundle.project_config.opt("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("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("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 3); + CHECK(is_mixed[2]); + const auto &components = bundle.project_config.opt("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("filament_diameter")->values = { 1.75, 1.75 }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00" }; + config.opt("filament_type")->values = { "PLA", "PLA" }; + config.opt("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("filament_colour")->values = { "#FF0000" }; + // Author has two extruders. + config.opt("retraction_length")->values = { 0.6, 0.9 }; + config.opt("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("retraction_length")->values = { 0.8 }; + bundle.printers.get_edited_preset().config.opt("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("retraction_length")->values, { 0.6 }); + check_double_vector(bundle.printers.get_edited_preset().config.opt("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("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("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("filament_colour")->values = { "#FF0000", "#00FF00" }; + // Author has two extruders. + config.opt("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("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("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("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("retraction_length")->values, { 0.6, 0.9, 0.7 }); + CHECK(pub.skipped_keys.empty()); + } +} + From 23b98e2ca56ccd0adb0bca2d12a5a5ee503a5c16 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 27 Aug 2026 17:04:27 +0800 Subject: [PATCH 29/60] Initial commit for warning popup when required filaments are not selected for mixed filaments --- src/slic3r/GUI/PublishSettingsDialog.cpp | 66 +++++++++++++++++++++++- src/slic3r/GUI/PublishSettingsDialog.hpp | 5 ++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 262fdbe356..be28ae7499 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -416,7 +416,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - // Publish is always allowed: no settings selected means no settings override. + // Publish is always allowed: no settings selected means no settings override. Warn only + // when an enabled mixed filament would ship without the identity of one of its + // components; "Proceed" accepts that and publishes anyway. + if (const std::vector missing = unpublished_mixed_components(); !missing.empty()) { + wxString missing_list; + for (size_t i = 0; i < missing.size(); ++i) { + if (i > 0) + missing_list += ", "; + missing_list += wxString::Format(_L("Filament %d"), int(missing[i] + 1)); + } + const wxString msg = _L("The following filaments are used by published mixed filaments but will not carry their material identity:") + + wxString(" ") + missing_list; + MessageDialog warn(this, msg, _L("Warning"), wxICON_WARNING); + warn.AddButton(wxID_CANCEL, _L("Cancel"), true); // safe choice gets the focus + warn.AddButton(wxID_OK, _L("Proceed"), false); + if (warn.ShowModal() != wxID_OK) + return; // Cancel: dismiss the warning and stay in this dialog + } EndModal(wxID_OK); }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); @@ -1695,6 +1712,53 @@ std::vector PublishSettingsDialog::GetPublishedKeys() const return out; } +std::vector PublishSettingsDialog::unpublished_mixed_components() const +{ + std::set missing; + for (const Category& cat : m_categories) { + if (!cat.is_mixed || cat.section != Section::Material) + continue; + // Only enabled mixed slots depend on their components being published. + if (cat.enable_check == nullptr || !cat.enable_check->GetValue()) + continue; + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + // Components are 1-based physical filament indices. + for (const unsigned int component : mixed_slot_components(full, cat.filament_slot)) { + const size_t component_slot = size_t(component) - 1; + // Find the component slot's material category (one exists per physical slot). + const Category* comp_cat = nullptr; + for (const Category& other : m_categories) { + if (other.section == Section::Material && !other.is_mixed && other.filament_slot == component_slot) { + comp_cat = &other; + break; + } + } + if (comp_cat == nullptr || comp_cat->enable_check == nullptr) + continue; + // Missing when the component's "Enable" is off, or it is enabled with neither + // "Full Publish" nor the "Type" requirement row checked. Colour never counts: + // the receiver renders the mix from its own components' colours. + if (!comp_cat->enable_check->GetValue()) { + missing.insert(component_slot); + continue; + } + if (comp_cat->full_check != nullptr && comp_cat->full_check->GetValue()) + continue; + bool type_checked = false; + for (const size_t r : comp_cat->rows) { + const Row& row = m_rows[r]; + if (row.kind == RowKind::Type && row.check->GetValue()) { + type_checked = true; + break; + } + } + if (!type_checked) + missing.insert(component_slot); + } + } + return std::vector(missing.begin(), missing.end()); +} + std::vector PublishSettingsDialog::GetPublishedMaterialKeys() const { std::vector out; diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index e00bec37fd..60c33e5a2d 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -187,6 +187,11 @@ private: // "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); + // 0-based material slots required by enabled mixed-filament slots that would ship without + // their identity: "Enable" not checked, or enabled with neither "Full Publish" nor the + // "Type" requirement row checked. Colour is deliberately ignored (the receiver renders the + // mix from its own components' colours). Sorted, deduplicated. + std::vector unpublished_mixed_components() const; // 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. From 28b325805bf21c9a494040adeccbc10053a121d6 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 28 Aug 2026 10:37:54 +0800 Subject: [PATCH 30/60] Fixed issues with remapping mixed filaments when importing published 3MF --- src/libslic3r/Model.cpp | 46 +++ src/libslic3r/Model.hpp | 11 + src/libslic3r/PresetBundle.cpp | 127 +++++++- src/libslic3r/PresetBundle.hpp | 6 + src/slic3r/GUI/Plater.cpp | 11 + .../libslic3r/test_preset_bundle_loading.cpp | 305 +++++++++++++++++- 6 files changed, 498 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 600c46e7f5..995302e220 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -3598,6 +3598,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker this->set(selector); } +void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map) +{ + if (empty()) return; + TriangleSelector selector(mv.mesh()); + selector.deserialize(m_data, false); + selector.remap_triangle_state(state_map); + this->set(selector); +} + void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv, EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament, @@ -3862,6 +3871,43 @@ bool model_has_advanced_features(const Model &model) return false; } +void remap_model_filament_slots(Model &model, const std::map &slot_relocations) +{ + if (slot_relocations.empty()) + return; + + // Paint states and the object/volume "extruder" configs store one-based slot numbers + // (see Sidebar::on_action_add_filament's insertion remap for the same encoding). + std::map one_based_slots; + for (const auto &[from, to] : slot_relocations) + one_based_slots.emplace(from + 1, to + 1); + + EnforcerBlockerStateMap paint_state_map; + for (size_t state = 0; state < paint_state_map.size(); ++state) + paint_state_map[state] = EnforcerBlockerType(state); + for (const auto &[one_based_from, one_based_to] : one_based_slots) { + assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size()); + assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size()); + paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to); + } + + auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool { + const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end(); + if (it == one_based_slots.end()) + return false; + config.set("extruder", it->second); + return true; + }; + + for (ModelObject *object : model.objects) { + remap_extruder_config(object->config); + for (ModelVolume *volume : object->volumes) { + remap_extruder_config(volume->config); + volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map); + } + } +} + #ifndef NDEBUG // Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. void check_model_ids_validity(const Model &model) diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 6834c7a59b..8da1340fa9 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -745,6 +745,10 @@ public: // Shift painted filament indices >= threshold by delta. Used when a physical filament is // inserted ahead of existing slots (mixed-color slots are kept at the end of the list). void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta); + // Relabel painted filament indices according to state_map (old state value -> new state + // value; untouched states keep their identity). Used when published-3MF import relocates + // mixed-filament definitions onto new slot numbers. + void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map); indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const; bool empty() const { return m_data.triangles_to_split.empty(); } @@ -1790,6 +1794,13 @@ bool model_has_multi_part_objects(const Model &model); // If the model has advanced features, then it cannot be processed in simple mode. bool model_has_advanced_features(const Model &model); +// Remap the model's filament-slot references after a published-3MF import relocated +// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and +// multi-material color-painting states (paint state stores the one-based slot number). +// slot_relocations maps the author's zero-based slot number to its final zero-based slot; +// entries are applied simultaneously (no chained lookups), untouched slots keep everything. +void remap_model_filament_slots(Model &model, const std::map &slot_relocations); + #ifndef NDEBUG // Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique. void check_model_ids_validity(const Model &model); diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 855aea9e0c..b6b816b587 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -14,6 +14,7 @@ #include "Utils.hpp" #include "LocalesUtils.hpp" #include "Model.hpp" +#include "TriangleSelector.hpp" #include "libslic3r_version.h" #include @@ -4871,6 +4872,49 @@ static std::map> filament_preset {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}}}; +// Relocate the per-slot cells of one mixed-filament project vector inside the imported +// config, applying every authored-slot -> destination move at once. Each move reads its +// source cell from a frozen snapshot of the config's current cells, so relocations never +// cross-contaminate when an earlier destination overlaps a later source (adjacent published +// tail mixes relocate onto consecutive slots). Cells the snapshot lacks degrade to +// empty/false defaults - the missing-data handling in the material pass reports them +// downstream. Left-behind source cells stay as-is: nothing else consumes the imported config +// at those indices in published mode. +static void apply_mixed_config_relocations(DynamicPrintConfig& config, + const std::string& key, + const std::vector>& moves) +{ + ConfigOption* opt = config.optptr(key); + if (opt == nullptr || moves.empty()) + return; + std::unique_ptr snapshot(opt->clone()); + switch (opt->type()) { + case coBools: { + auto* live = static_cast(opt); + const auto* frozen = static_cast(snapshot.get()); + for (const auto [from, to] : moves) { + const unsigned char cell = from < frozen->values.size() ? frozen->values[from] : 0; + if (live->values.size() <= to) + live->values.resize(to + 1, 0); + live->values[to] = cell; + } + break; + } + case coStrings: { + auto* live = static_cast(opt); + const auto* frozen = static_cast(snapshot.get()); + for (const auto [from, to] : moves) { + const std::string cell = from < frozen->values.size() ? frozen->values[from] : std::string(); + if (live->values.size() <= to) + live->values.resize(to + 1, std::string{}); + live->values[to] = cell; + } + break; + } + default: break; + } +} + // convert the old filament preset to new one after split static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name) { @@ -5379,12 +5423,87 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, if (entry.slot >= 0) grow_target = std::max(grow_target, size_t(entry.slot) + 1); } + // Mixed-filament definitions live in project-level virtual slots, so applying one + // positionally onto a receiver slot that holds a real, physical filament would + // silently convert hardware-backed state into a virtual mix. Compute each mixed + // entry's destination before anything consumes entry.slot (growth, seeding, + // de-aliasing, the overlay below): + // - a definition landing on a receiver slot that already carries a mixed + // definition keeps its place (a like-for-like override of a virtual slot); + // - everything else goes through one monotone append counter preserving author + // order: dest = max(authored, next_free). With a receiver shorter than the + // publish this keeps the authored positions intact; past them (or around a + // collision with a real filament) the mixes pack onto consecutive fresh slots + // AFTER every positional (real-filament) territory. The definition's cells are + // shifted inside the file-side per-slot mixed arrays so they stay readable + // from the new index. No existing slot changes meaning. + // - destinations are also capped: appends past the extruder limit are dropped + // and reported instead of being forced onto a physical filament. + const std::set& mixed_definitions = publish_mixed_keys(); + auto is_mixed_definition = [&mixed_definitions](const PublishedMaterialEntry& entry) { + return std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) { + return mixed_definitions.count(publish_base_key(key)) != 0; + }); + }; + size_t next_free_slot = this->filament_presets.size(); + bool any_mixed_relocated = false; + // All authored-slot -> destination moves decided by this pass, applied to the + // incoming config in one batched snapshot step below (an earlier move's + // destination can overlap a later move's source: adjacent tail mixes relocate + // onto consecutive slots, so incremental in-place shifts would overwrite a + // definition that has not been moved yet). + std::vector> mixed_moves; + for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) { + PublishedMaterialEntry& entry = *entry_it; + if (entry.slot < 0 || !is_mixed_definition(entry) || + // Like-for-like override of a virtual receiver slot (bounds-checked). + this->is_mixed_filament(size_t(entry.slot))) { + ++entry_it; + continue; + } + if (std::max(size_t(entry.slot), next_free_slot) >= size_t(EnforcerBlockerType::ExtruderMax)) { + // No free virtual slot left: report instead of destroying a real filament. + const std::string material_label = !entry.filament_id.empty() ? entry.filament_id : + !entry.publish_type_value.empty() ? entry.publish_type_value : + entry.filament_type; + published_config->skipped_keys.emplace_back("material:" + material_label + + " (mixed filament definition: filament slot limit reached)"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot + << " could not be placed: all " << next_free_slot << " slots exhausted"; + entry_it = published_config->material_keys.erase(entry_it); + continue; + } + const int authored_slot = entry.slot; + const int dest_slot = int(std::max(size_t(authored_slot), next_free_slot)); + next_free_slot = size_t(dest_slot) + 1; + if (dest_slot == authored_slot) + // Uncontended fresh tail slot: the definition is already readable there. + ++entry_it; + else { + entry.slot = dest_slot; + any_mixed_relocated = true; + mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot)); + published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot); + published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " + + std::to_string(entry.slot) + + ": mixed filament relocated (would have replaced a physical filament)"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot + << " -> " << entry.slot; + ++entry_it; + } + } + if (!mixed_moves.empty()) + for (const std::string& mixed_key : mixed_definitions) + apply_mixed_config_relocations(config, mixed_key, mixed_moves); if (has_published_entries) { // Defensive cap: growth never exceeds the file's own filament count. The // receiver's current slot count is a floor: neither the preset list nor the // project vectors are ever shrunk, even when the file carries fewer filaments - // than the receiver has slots. - const size_t target_slots = std::max(this->filament_presets.size(), std::min(grow_target, num_filaments)); + // than the receiver has slots. Relocated mixed entries legitimately land past + // the file's own slot count (virtual slots consume no nozzle or tray), so + // their destinations lift the ceiling explicitly. + const size_t target_slots = std::max({this->filament_presets.size(), std::min(grow_target, num_filaments), + any_mixed_relocated ? next_free_slot : size_t(0)}); // Slots carrying published content, steering the initial preset selection of // newly grown slots. std::set published_slots; @@ -5999,9 +6118,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // 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; - }); + const bool is_mixed_entry = is_mixed_definition(entry); if (!is_mixed_entry && recv != nullptr) { // Create the key when the target preset lacks it: the colour is a // requirement, not an override. diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 67ff28b81e..c85b96a92a 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -8,6 +8,7 @@ #include "enum_bitmask.hpp" #include +#include #include #include #include @@ -186,6 +187,11 @@ struct PublishedConfig // Human-readable notices of the slot material replacements performed while loading a // published project, for the load notification. std::vector material_replacements; + // Mixed-filament entries that had to be moved off their authored slot on load (a real, + // physical filament occupied it): maps the author's zero-based slot number to its final + // zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to + // keep geometry references pointing at the relocated definitions. + std::map mixed_slot_relocations; }; // Bundle of Print + Filament + Printer presets. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index fee75dee67..3cadcae4b9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9053,6 +9053,17 @@ std::vector Plater::priv::load_files(const std::vector& input_ preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config); + // Mixed-filament definitions that collided with one of the + // receiver's real slots were relocated during the preset load. + // Re-point the freshly parsed model's extruder references and + // color painting from the author's slot numbers to where each + // definition landed, so volumes colored with a mix follow it. + // Runs before the objects are handed over to the plater below. + if (load_model && !published_config.mixed_slot_relocations.empty()) + Slic3r::remap_model_filament_slots(model, published_config.mixed_slot_relocations); + + // BBS: notify the user about published settings that could not be applied. + // BBS: notify the user about published settings that could not be applied. if (!published_config.skipped_keys.empty()) { NotificationManager* notify_manager = q->get_notification_manager(); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 53a40764d5..931f9e96c3 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -5,6 +5,8 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/AppConfig.hpp" +#include "libslic3r/Model.hpp" +#include "libslic3r/TriangleMesh.hpp" #include "test_utils.hpp" @@ -2577,8 +2579,9 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's 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). + // A receiver that already carries the mix slot at index 2 as an actual mixed slot (e.g. a + // two-physical-plus-one-mix project with the same layout): the incoming definition is a + // like-for-like override of the virtual slot and applies in place without relocation. { PresetBundle bundle; Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); @@ -2587,8 +2590,12 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's 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. + // Grow the receiver's project arrays to 3 slots first, as set_num_filaments would, + // then mark the third slot as the receiver's own mixed filament. bundle.set_num_filaments(3); + bundle.project_config.opt("filament_is_mixed")->values[2] = 1; + bundle.project_config.opt("filament_mixed_components")->values[2] = "1,1"; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2] = "0.5,0.5"; PublishedMaterialEntry mix; mix.filament_type = "PLA"; @@ -2636,6 +2643,8 @@ TEST_CASE("Published 3MF applies a mixed filament definition onto the receiver's CHECK_FALSE(is_mixed[1]); // Nothing skipped: every serialized mixed key was applied. CHECK(pub.skipped_keys.empty()); + // Like-for-like override: no slot was relocated. + CHECK(pub.material_replacements.empty()); } // A receiver with fewer slots: the slot is grown and seeded before the definition applies. @@ -2701,6 +2710,228 @@ TEST_CASE("Published 3MF reports an unappliable mixed filament definition as ski CHECK(contains_key(pub.skipped_keys, "material: (filament_mixed_sublayer_ratios)")); } +// A published mixed filament must never convert one of the receiver's real, physical slots +// into a virtual mix: definitions that collide with a physical slot are relocated past every +// positional (real-filament) destination, while ones colliding with an existing mixed slot +// override it in place. +TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a physical slot", "[Preset][Bundle][Published]") +{ + // An author project with slots whose last slot is a mixed filament. + auto make_file_config = [](size_t num_author_slots, size_t num_tail_mixes = 1) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + std::vector diameters(num_author_slots, 1.75); + std::vector self_index; + std::vector variants; + std::vector types; + for (size_t i = 0; i < num_author_slots; ++i) { + self_index.push_back(int(i + 1)); + variants.emplace_back("Direct Drive Standard"); + types.push_back(i % 2 == 0 ? "PLA" : "PETG"); + } + config.opt("filament_diameter")->values = diameters; + config.opt("filament_self_index")->values = self_index; + config.opt("filament_extruder_variant")->values = variants; + config.opt("filament_colour")->values = { "#FF0000", "#00AA00", "#0000FF", "#FFFF00", "#800080" }; + config.opt("filament_colour")->values.resize(num_author_slots, "#808080"); + config.opt("filament_type")->values = types; + config.opt("filament_vendor")->values.assign(num_author_slots, "Generic"); + config.opt("filament_ids")->values.resize(num_author_slots); + // The last author slots are mixed ones (components differ per slot so + // the definitions are distinguishable after relocation). + const size_t first_mix_slot = num_author_slots - num_tail_mixes; + config.opt("filament_is_mixed")->values.assign(num_author_slots, 0); + config.opt("filament_mixed_components")->values.assign(num_author_slots, ""); + config.opt("filament_mixed_sublayer_ratios")->values.assign(num_author_slots, ""); + for (size_t i = first_mix_slot; i < num_author_slots; ++i) { + config.opt("filament_is_mixed")->values[i] = 1; + config.opt("filament_mixed_components")->values[i] = + i % 2 == 0 ? std::string("1,2") : std::string("1,3"); + config.opt("filament_mixed_sublayer_ratios")->values[i] = + i % 2 == 0 ? std::string("0.6,0.4") : std::string("0.3,0.7"); + } + return config; + }; + + // The reported bug: an author publishes with physical filaments on slots 1-2 and a mixed + // filament on slot 5; the receiver runs five real filaments of his own. Slot 5 must stay + // untouched and the mix lands as a newly appended virtual slot 6. + { + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(5, "#123456"); + const std::vector receiver_colours = + bundle.project_config.opt("filament_colour")->values; + + PublishedMaterialEntry mix; + mix.filament_type = "PLA"; + mix.filament_vendor = "Generic"; + mix.slot = 4; + 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(5); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The receiver grew by exactly one extra virtual slot. + REQUIRE(bundle.filament_presets.size() == 6); + // All five physical slots kept their meaning: no mixed flag, untouched names/colours. + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 6); + CHECK_FALSE(is_mixed[0]); + CHECK_FALSE(is_mixed[1]); + CHECK_FALSE(is_mixed[2]); + CHECK_FALSE(is_mixed[3]); + CHECK_FALSE(is_mixed[4]); + CHECK(is_mixed[5]); + CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), + bundle.project_config.opt("filament_colour")->values.begin())); + CHECK(bundle.filament_presets[0] == "My PLA"); + CHECK(bundle.filament_presets[4] == "My PLA"); + // The definition itself is readable at the new index. + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 6); + CHECK(components[5] == "1,2"); + const auto &ratios = bundle.project_config.opt("filament_mixed_sublayer_ratios")->values; + REQUIRE(ratios.size() == 6); + CHECK(ratios[5] == "0.6,0.4"); + // The blended colour seeds the swatch of the new slot only. + const auto &colour = bundle.project_config.opt("filament_colour")->values; + REQUIRE(colour.size() == 6); + CHECK(colour[5] == "#800080"); + // The relocation is surfaced to the user through the post-import notice (the de-alias + // pass may contribute further messages, so presence is asserted, not the count). + bool relocated_reported = false; + for (const std::string &message : pub.material_replacements) + if (message.find("slot 4 -> slot 5") != std::string::npos) + relocated_reported = true; + CHECK(relocated_reported); + CHECK(pub.skipped_keys.empty()); + } + + // A definition colliding with the receiver's own mixed filament is overridden in place: + // nothing grows, nothing is reported as moved. + { + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(3); + bundle.project_config.opt("filament_is_mixed")->values[2] = 1; + bundle.project_config.opt("filament_mixed_components")->values[2] = "1,1"; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2] = "0.9,0.1"; + + PublishedMaterialEntry mix; + mix.slot = 2; + mix.publish_color = true; + mix.color = "#800080"; + 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(3); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets.size() == 3); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 3); + CHECK_FALSE(is_mixed[0]); + CHECK_FALSE(is_mixed[1]); + CHECK(is_mixed[2]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 3); + CHECK(components[2] == "1,2"); + const auto &ratios = bundle.project_config.opt("filament_mixed_sublayer_ratios")->values; + REQUIRE(ratios.size() == 3); + CHECK(ratios[2] == "0.6,0.4"); + CHECK(pub.skipped_keys.empty()); + CHECK(pub.material_replacements.empty()); + } + + // Author publishes four physical filaments plus two mixed ones on slots 5 and 6; the + // receiver runs five real filaments. Both mixes relocate onto consecutive fresh slots, + // preserving their author order (slot 5 -> slot 6, slot 6 -> slot 7); no receiver slot is + // converted into a virtual mix. + { + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(5, "#123456"); + const std::vector receiver_colours = + bundle.project_config.opt("filament_colour")->values; + + auto make_mix_entry = [](int authored_slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = authored_slot; + entry.publish_color = true; + entry.color = color; + entry.keys = { "filament_is_mixed", "filament_mixed_components", "filament_mixed_sublayer_ratios" }; + return entry; + }; + PublishedMaterialEntry mix_a = make_mix_entry(4, "#800080"); + PublishedMaterialEntry mix_b = make_mix_entry(5, "#FF69B4"); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { mix_a, mix_b }; + DynamicPrintConfig config = make_file_config(6, 2); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Two fresh virtual slots were appended. + REQUIRE(bundle.filament_presets.size() == 7); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 7); + for (size_t i = 0; i < 5; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[5]); + CHECK(is_mixed[6]); + // The definitions follow their author order. + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 7); + CHECK(components[5] == "1,2"); + CHECK(components[6] == "1,3"); + const auto &ratios = bundle.project_config.opt("filament_mixed_sublayer_ratios")->values; + REQUIRE(ratios.size() == 7); + CHECK(ratios[5] == "0.6,0.4"); + CHECK(ratios[6] == "0.3,0.7"); + // The five real slots kept their colours; each mix's blended colour seeded its new slot. + const auto &colour = bundle.project_config.opt("filament_colour")->values; + REQUIRE(colour.size() == 7); + CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), colour.begin())); + CHECK(colour[5] == "#800080"); + CHECK(colour[6] == "#FF69B4"); + // Both relocations are reported with the correct mapping. + bool a_reported = false, b_reported = false; + for (const std::string &message : pub.material_replacements) { + if (message.find("slot 4 -> slot 5") != std::string::npos) + a_reported = true; + if (message.find("slot 5 -> slot 6") != std::string::npos) + b_reported = true; + } + CHECK(a_reported); + CHECK(b_reported); + CHECK(pub.skipped_keys.empty()); + // The relocation table is exposed for the model-reference remapping. + REQUIRE(pub.mixed_slot_relocations.size() == 2); + CHECK(pub.mixed_slot_relocations.at(4) == 5); + CHECK(pub.mixed_slot_relocations.at(5) == 6); + } +} + // 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. @@ -2980,3 +3211,71 @@ TEST_CASE("Sizing down to the nozzle count plus mixes is what eats the mixed tai CHECK(bundle.project_config.option("filament_mixed_components")->values[5] == "1,2"); } } + +// After a published-3MF import relocated mixed-filament definitions, the freshly loaded +// model's slot references must follow: object/volume "extruder" configs and multi-material +// color-painting states (which store the one-based slot number) are re-pointed to where each +// definition landed; everything else keeps its state. +TEST_CASE("remap_model_filament_slots repoints extruder configs and color painting", "[Preset][Bundle][Published]") +{ + auto make_model = [] { + Model model; + ModelObject *object_a = model.add_object(); + object_a->name = "relocated mix"; + ModelVolume *vol_a = object_a->add_volume(make_cube(10., 10., 10.)); + vol_a->config.set_key_value("extruder", new ConfigOptionInt(5)); // author slot 5 (0-based 4) + // Author painted one facet with the mix (slot 5) and another with a physical (slot 2). + { + TriangleSelector selector(vol_a->mesh()); + selector.set_facet(0, EnforcerBlockerType(5)); + selector.set_facet(1, EnforcerBlockerType(2)); + vol_a->mmu_segmentation_facets.set_data(selector.serialize()); + } + // A second object that does not reference the relocated slot at all. Painted with a + // real, non-relocated state (NONE is never serialized: an unsplit triangle without a + // state is the unpainted default and is skipped by TriangleSelector::serialize()). + ModelObject *object_b = model.add_object(); + object_b->name = "untouched"; + object_b->config.set_key_value("extruder", new ConfigOptionInt(1)); + ModelVolume *vol_b = object_b->add_volume(make_cube(5., 5., 5.)); + vol_b->config.set_key_value("extruder", new ConfigOptionInt(2)); + { + TriangleSelector selector(vol_b->mesh()); + selector.set_facet(0, EnforcerBlockerType(2)); + vol_b->mmu_segmentation_facets.set_data(selector.serialize()); + } + return model; + }; + + const std::map relocations = {{4, 5}}; + + Model model = make_model(); + Slic3r::remap_model_filament_slots(model, relocations); + + const ModelVolume *vol_a = model.objects[0]->volumes.front(); + CHECK(vol_a->config.extruder() == 6); // author slot 5 -> final slot 6 + // Painted states follow: the mix facet moved 5 -> 6, the physical one is untouched. + REQUIRE(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(6))); + REQUIRE_FALSE(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(5))); + CHECK(TriangleSelector::has_facets(vol_a->mmu_segmentation_facets.get_data(), EnforcerBlockerType(2))); + + const ModelVolume *vol_b = model.objects[1]->volumes.front(); + CHECK(vol_b->config.extruder() == 2); + // The untouched volume's paint (a non-relocated state) survives as-is. + CHECK(TriangleSelector::has_facets(vol_b->mmu_segmentation_facets.get_data(), EnforcerBlockerType(2))); + + // The mapping is applied simultaneously: each entry reads the original slot number, so + // relocating onto another relocated-from slot number must not chase chains. With the + // 0-based relocations {3->4, 4->6} the 1-based config map is {4->5, 5->7}: a volume on + // 1-based slot 4 lands on 5 and does NOT continue to 7. + Model chained = make_model(); + chained.objects[0]->volumes.front()->config.set_key_value("extruder", new ConfigOptionInt(4)); + Slic3r::remap_model_filament_slots(chained, std::map{{3, 4}, {4, 6}}); + CHECK(chained.objects[0]->volumes.front()->config.extruder() == 5); + // The chained model's paint follows its own single-step mapping: painted state 5 -> 7, + // and nothing lands back on 5. + CHECK(TriangleSelector::has_facets(chained.objects[0]->volumes.front()->mmu_segmentation_facets.get_data(), + EnforcerBlockerType(7))); + CHECK_FALSE(TriangleSelector::has_facets(chained.objects[0]->volumes.front()->mmu_segmentation_facets.get_data(), + EnforcerBlockerType(5))); +} From 1c09ef14a55c60abd996e5ce3c269f75dbbdd6d6 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 28 Aug 2026 12:57:00 +0800 Subject: [PATCH 31/60] Update translations. Update warning messages to be more user friendly. Update gradient color chips in publish dialog --- localization/i18n/OrcaSlicer.pot | 153 ++++++++++++++++++- localization/i18n/ca/OrcaSlicer_ca.po | 153 ++++++++++++++++++- localization/i18n/cs/OrcaSlicer_cs.po | 153 ++++++++++++++++++- localization/i18n/de/OrcaSlicer_de.po | 153 ++++++++++++++++++- localization/i18n/en/OrcaSlicer_en.po | 153 ++++++++++++++++++- localization/i18n/es/OrcaSlicer_es.po | 153 ++++++++++++++++++- localization/i18n/eu/OrcaSlicer_eu.po | 153 ++++++++++++++++++- localization/i18n/fr/OrcaSlicer_fr.po | 153 ++++++++++++++++++- localization/i18n/hu/OrcaSlicer_hu.po | 153 ++++++++++++++++++- localization/i18n/it/OrcaSlicer_it.po | 153 ++++++++++++++++++- localization/i18n/ja/OrcaSlicer_ja.po | 153 ++++++++++++++++++- localization/i18n/ko/OrcaSlicer_ko.po | 153 ++++++++++++++++++- localization/i18n/lt/OrcaSlicer_lt.po | 153 ++++++++++++++++++- localization/i18n/nl/OrcaSlicer_nl.po | 153 ++++++++++++++++++- localization/i18n/pl/OrcaSlicer_pl.po | 153 ++++++++++++++++++- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 153 ++++++++++++++++++- localization/i18n/ru/OrcaSlicer_ru.po | 153 ++++++++++++++++++- localization/i18n/sv/OrcaSlicer_sv.po | 153 ++++++++++++++++++- localization/i18n/th/OrcaSlicer_th.po | 153 ++++++++++++++++++- localization/i18n/tr/OrcaSlicer_tr.po | 153 ++++++++++++++++++- localization/i18n/uk/OrcaSlicer_uk.po | 153 ++++++++++++++++++- localization/i18n/vi/OrcaSlicer_vi.po | 153 ++++++++++++++++++- localization/i18n/zh_CN/OrcaSlicer_zh_CN.po | 153 ++++++++++++++++++- localization/i18n/zh_TW/OrcaSlicer_zh_TW.po | 153 ++++++++++++++++++- src/slic3r/GUI/PublishSettingsDialog.cpp | 159 ++++++++++++++++---- src/slic3r/GUI/PublishSettingsDialog.hpp | 19 ++- 26 files changed, 3796 insertions(+), 54 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index c1dc798ff3..b9da59451f 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -2762,6 +2762,9 @@ msgstr "" msgid "Merge with" msgstr "" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "" @@ -3039,6 +3042,9 @@ msgstr "" msgid "Merge parts to an object" msgstr "" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "" @@ -7409,6 +7415,9 @@ msgstr "" msgid "The %s nozzle can not print %s." msgstr "" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, possible-boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7529,12 +7538,36 @@ msgstr "" msgid "Set filaments to use" msgstr "" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "" msgid "Pellets" msgstr "" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, possible-c-format, possible-boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "" @@ -7712,6 +7745,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "" @@ -7906,6 +7945,18 @@ msgstr "" msgid "Sync now" msgstr "" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "" @@ -9066,6 +9117,9 @@ msgstr "" msgid "First layer filament sequence" msgstr "" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "" @@ -9111,18 +9165,58 @@ msgstr "" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, possible-c-format, possible-boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -11548,6 +11642,9 @@ msgstr "" msgid "No extrusions under current settings." msgstr "" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "" @@ -11584,6 +11681,9 @@ msgstr "" msgid "Variable layer height is not supported with Organic supports." msgstr "" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "" @@ -13289,6 +13389,48 @@ msgstr "" msgid "Support material is commonly used to print supports and support interfaces." msgstr "" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "" @@ -15872,6 +16014,12 @@ msgid "" "Setting a value in the retract amount before wipe setting below will perform any excess retraction before the wipe, else it will be performed after." msgstr "" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "" @@ -16899,6 +17047,9 @@ msgstr "" msgid "The supplied file couldn't be read because it's empty." msgstr "" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "" diff --git a/localization/i18n/ca/OrcaSlicer_ca.po b/localization/i18n/ca/OrcaSlicer_ca.po index 7070c885be..74ed04532c 100644 --- a/localization/i18n/ca/OrcaSlicer_ca.po +++ b/localization/i18n/ca/OrcaSlicer_ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2025-03-15 10:55+0100\n" "Last-Translator: \n" "Language-Team: \n" @@ -3015,6 +3015,9 @@ msgstr "Editar" msgid "Merge with" msgstr "Fusiona amb" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Elimina aquest filament" @@ -3316,6 +3319,9 @@ msgstr "Muntatge" msgid "Merge parts to an object" msgstr "Fusionar les peces en un objecte" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Afegir capes" @@ -7962,6 +7968,9 @@ msgstr "Personalitzar la placa actual" msgid "The %s nozzle can not print %s." msgstr "El broquet %s no pot imprimir %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "No es recomana barrejar %1% amb %2% en la impressió.\n" @@ -8087,6 +8096,21 @@ msgstr "Sincronitzar la llista de filaments des d'AMS" msgid "Set filaments to use" msgstr "Configurar els filaments a utilitzar" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Cercar placa, objecte i peça." @@ -8094,6 +8118,15 @@ msgstr "Cercar placa, objecte i peça." msgid "Pellets" msgstr "Pèl·lets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "En completar l'operació, el projecte %s es tancarà i es crearà un nou projecte." @@ -8287,6 +8320,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objecte de múltiples peces detectat" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Carregar aquests fitxers com un sol objecte amb diverses peces?\n" @@ -8500,6 +8539,18 @@ msgstr "" msgid "Sync now" msgstr "Sincronitza ara" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Podeu mantenir els perfils modificats al projecte nou o descartar-los" @@ -9803,6 +9854,9 @@ msgstr "Gerro en Espiral" msgid "First layer filament sequence" msgstr "Seqüència d'impressió de la primera capa" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Per Capa" @@ -9849,18 +9903,58 @@ msgstr "Anar a la pàgina web" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12503,6 +12597,9 @@ msgstr "Si tot i així voleu imprimir, podeu activar l'opció a Preferències / msgid "No extrusions under current settings." msgstr "No hi ha extrusions a la configuració actual." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "El mode suau de timelapse no està permès quan la seqüència \"Per objecte\" està habilitada." @@ -12539,6 +12636,9 @@ msgstr "Potser voleu reduir la mida del model o canviar la configuració d'impre msgid "Variable layer height is not supported with Organic supports." msgstr "Alçada de Capa Variable no és compatible amb suports Orgànics." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "És possible que els diferents diàmetres de broquet i els diferents diàmetres de filament no funcionin bé quan la torre principal està activada. És molt experimental, així que si us plau, procediu amb precaució." @@ -14610,6 +14710,48 @@ msgstr "Material de suport" msgid "Support material is commonly used to print supports and support interfaces." msgstr "El material de suport s'utilitza habitualment per imprimir interfície de suport i suport" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament imprimible" @@ -17520,6 +17662,12 @@ msgstr "" "\n" "L'establiment d'un valor en la quantitat de retractació abans de l'esborrat es realitzarà qualsevol retracció en excés abans de la neteja, sinó es realitzarà després." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La Torre de Purga es pot utilitzar per netejar els residus al broquet i estabilitzar la pressió de la cambra dins del broquet, per tal d'evitar defectes d'aparença en imprimir objectes." @@ -18611,6 +18759,9 @@ msgstr "La generació de malla del fitxer del model ha fallat o la forma no és msgid "The supplied file couldn't be read because it's empty." msgstr "El fitxer subministrat no s'ha pogut llegir perquè està buit" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Format de fitxer desconegut. El fitxer d'entrada ha de tenir extensió .stl, .obj, .amf( .xml )." diff --git a/localization/i18n/cs/OrcaSlicer_cs.po b/localization/i18n/cs/OrcaSlicer_cs.po index 889b659a0b..28396811c5 100644 --- a/localization/i18n/cs/OrcaSlicer_cs.po +++ b/localization/i18n/cs/OrcaSlicer_cs.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Jakub Hencl\n" "Language-Team: \n" @@ -2977,6 +2977,9 @@ msgstr "Upravit" msgid "Merge with" msgstr "Sloučit s" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Smazat tento filament" @@ -3279,6 +3282,9 @@ msgstr "Sestava" msgid "Merge parts to an object" msgstr "Sloučit části do objektu" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Přidat vrstvy" @@ -7927,6 +7933,9 @@ msgstr "Přizpůsobit aktuální desku" msgid "The %s nozzle can not print %s." msgstr "Tryska %s nemůže tisknout %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Míchání %1% s %2% při tisku není doporučeno.\n" @@ -8056,12 +8065,36 @@ msgstr "Synchronizovat seznam filamentů z AMS" msgid "Set filaments to use" msgstr "Nastavit používané filamenty" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Hledat desku, objekt a díl." msgid "Pellets" msgstr "Pelety" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Po dokončení operace bude projekt %s uzavřen a bude vytvořen nový projekt." @@ -8249,6 +8282,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Detekován vícedílný objekt" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Načíst tyto soubory jako jeden objekt s více částmi?\n" @@ -8459,6 +8498,18 @@ msgstr "" msgid "Sync now" msgstr "Synchronizovat nyní" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Upravené předvolby můžete ponechat v novém projektu nebo je zahodit" @@ -9751,6 +9802,9 @@ msgstr "Spirálová váza" msgid "First layer filament sequence" msgstr "Pořadí filamentů v první vrstvě" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Podle vrstvy" @@ -9797,18 +9851,58 @@ msgstr "Přejít na webovou stránku" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12484,6 +12578,9 @@ msgstr "Pokud chcete přesto tisknout, můžete povolit možnost v Nastavení / msgid "No extrusions under current settings." msgstr "Při aktuálním nastavení nejsou žádné extruze." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Plynulý režim časosběru není podporován, pokud je povoleno pořadí tisku „podle objektu“." @@ -12520,6 +12617,9 @@ msgstr "Možná budete chtít zmenšit velikost modelu nebo změnit aktuální n msgid "Variable layer height is not supported with Organic supports." msgstr "Proměnná výška vrstvy není podporována s organickými podporami." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Různé průměry trysek a filamentu nemusí správně fungovat, pokud je povolena základní věž. Jedná se o velmi experimentální funkci, proto pokračujte opatrně." @@ -14578,6 +14678,48 @@ msgstr "Podpůrný materiál" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Podpůrný materiál se běžně používá pro tisk podpěr a rozhraní podpěr." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Tisknutelný filament" @@ -17465,6 +17607,12 @@ msgstr "" "\n" "Nastavení hodnoty v parametru množství retrakce před očištěním níže provede případnou dodatečnou retrakci před očištěním, jinak bude provedena po něm." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Čistící věž lze použít k odstranění zbytků materiálu na trysce a ke stabilizaci tlaku v komoře trysky, aby se předešlo vizuálním vadám při tisku objektů." @@ -18538,6 +18686,9 @@ msgstr "Síťování modelového souboru selhalo nebo nebyl nalezen platný tvar msgid "The supplied file couldn't be read because it's empty." msgstr "Zadaný soubor nelze načíst, protože je prázdný." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Neznámý formát souboru. Vstupní soubor musí mít příponu .stl, .obj nebo .amf(.xml)." diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index fe17251f32..0e3fc99a39 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Heiko Liebscher \n" "Language-Team: \n" @@ -2917,6 +2917,9 @@ msgstr "Bearbeiten" msgid "Merge with" msgstr "Zusammenführen mit" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Diesen Filament löschen" @@ -3216,6 +3219,9 @@ msgstr "Zusammenbau" msgid "Merge parts to an object" msgstr "Teile zu einem Objekt zusammenführen" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Schichten hinzufügen" @@ -7797,6 +7803,9 @@ msgstr "Aktuelle Platte anpassen" msgid "The %s nozzle can not print %s." msgstr "Die %s Düse kann %s nicht drucken." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Mischen von %1% mit %2% im Druck wird nicht empfohlen.\n" @@ -7922,6 +7931,21 @@ msgstr "Filamentliste von AMS synchronisieren" msgid "Set filaments to use" msgstr "Zu verwendende Filamente einstellen" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Suche Platte, Objekt und Teil." @@ -7929,6 +7953,15 @@ msgstr "Suche Platte, Objekt und Teil." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Nach Abschluss Ihrer Operation wird das %s-Projekt geschlossen und ein neues Projekt erstellt." @@ -8118,6 +8151,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Mehrteiliges Objekt erkannt" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Diese Dateien als ein einziges Objekt mit mehreren Teilen laden?\n" @@ -8331,6 +8370,18 @@ msgstr "" msgid "Sync now" msgstr "Jetzt synchronisieren" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + # AI Translated msgid "You can keep the modified presets for the new project or discard them" msgstr "Sie können die geänderten Profile für das neue Projekt beibehalten oder sie verwerfen" @@ -9586,6 +9637,9 @@ msgstr "Vasenmodus" msgid "First layer filament sequence" msgstr "Erste Filament-Schichtsequenz" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Nach Schicht" @@ -9631,18 +9685,58 @@ msgstr "Zu einer Website springen" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12223,6 +12317,9 @@ msgstr "Wenn Sie trotzdem drucken möchten, können Sie die Option in Einstellun msgid "No extrusions under current settings." msgstr "Keine Extrusion unter den aktuellen Einstellungen." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Der gewählte Zeitraffermodus wird nicht unterstützt, wenn die Sequenz \"nach Objekt\" aktiviert ist." @@ -12259,6 +12356,9 @@ msgstr "Sie möchten möglicherweise die Größe Ihres Modells reduzieren oder d msgid "Variable layer height is not supported with Organic supports." msgstr "Variable Schichthöhe wird nicht mit organischen Stützstrukturen unterstützt." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Unterschiedliche Düsendurchmesser und unterschiedliche Filamentdurchmesser funktionieren möglicherweise nicht gut, wenn der Reinigungsturm aktiviert ist. Es ist sehr experimentell, also gehen Sie bitte vorsichtig vor." @@ -14276,6 +14376,48 @@ msgstr "Stützmaterial" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Stützmaterial wird üblicherweise zum Drucken von Stützen und Stütz-Schnittstellen verwendet." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament druckbar" @@ -17108,6 +17250,12 @@ msgstr "" "\n" "Wenn ein Wert in der Einstellung \"Rückzugsmenge vor dem Wischen\" unten angegeben ist, wird ein überschüssiger Rückzug vor dem Wischen ausgeführt, ansonsten wird er danach ausgeführt." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Der Reinigungsturm kann verwendet werden, um Rückstände auf der Düse zu entfernen und den Kammerdruck im Inneren der Düse zu stabilisieren, um Erscheinungsdefekte beim Drucken von Objekten zu vermeiden." @@ -18178,6 +18326,9 @@ msgstr "Das Erstellen eines Netzes aus der Modelldatei ist fehlgeschlagen oder e msgid "The supplied file couldn't be read because it's empty." msgstr "Die angegebene Datei konnte nicht gelesen werden, weil sie leer ist." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Unbekanntes Dateiformat: Die Eingabedatei muss die Endung .stl, .obj oder .amf(.xml) haben." diff --git a/localization/i18n/en/OrcaSlicer_en.po b/localization/i18n/en/OrcaSlicer_en.po index 0da5e9d2b5..8a679894fa 100644 --- a/localization/i18n/en/OrcaSlicer_en.po +++ b/localization/i18n/en/OrcaSlicer_en.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-06-17 15:44-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: \n" @@ -2758,6 +2758,9 @@ msgstr "" msgid "Merge with" msgstr "" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "" @@ -3035,6 +3038,9 @@ msgstr "" msgid "Merge parts to an object" msgstr "" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "" @@ -7405,6 +7411,9 @@ msgstr "" msgid "The %s nozzle can not print %s." msgstr "" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7525,12 +7534,36 @@ msgstr "" msgid "Set filaments to use" msgstr "" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "" msgid "Pellets" msgstr "" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "" @@ -7708,6 +7741,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "" @@ -7902,6 +7941,18 @@ msgstr "" msgid "Sync now" msgstr "" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "" @@ -9062,6 +9113,9 @@ msgstr "" msgid "First layer filament sequence" msgstr "" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "" @@ -9107,18 +9161,58 @@ msgstr "" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -11544,6 +11638,9 @@ msgstr "" msgid "No extrusions under current settings." msgstr "" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "" @@ -11580,6 +11677,9 @@ msgstr "" msgid "Variable layer height is not supported with Organic supports." msgstr "" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "" @@ -13285,6 +13385,48 @@ msgstr "" msgid "Support material is commonly used to print supports and support interfaces." msgstr "" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "" @@ -15868,6 +16010,12 @@ msgid "" "Setting a value in the retract amount before wipe setting below will perform any excess retraction before the wipe, else it will be performed after." msgstr "" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "" @@ -16895,6 +17043,9 @@ msgstr "" msgid "The supplied file couldn't be read because it's empty." msgstr "" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "" diff --git a/localization/i18n/es/OrcaSlicer_es.po b/localization/i18n/es/OrcaSlicer_es.po index ded492a284..8a435af73e 100644 --- a/localization/i18n/es/OrcaSlicer_es.po +++ b/localization/i18n/es/OrcaSlicer_es.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Ian A. Bassi <>\n" "Language-Team: \n" @@ -2832,6 +2832,9 @@ msgstr "Editar" msgid "Merge with" msgstr "Fusionar con" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Eliminar este filamento" @@ -3113,6 +3116,9 @@ msgstr "Ensamblaje" msgid "Merge parts to an object" msgstr "Fusionar piezas en un objeto" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "Añadir capas" @@ -7619,6 +7625,9 @@ msgstr "Personalizar cama actual" msgid "The %s nozzle can not print %s." msgstr "La boquilla %s no puede imprimir %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "No se recomienda mezclar %1% con %2% en la impresión.\n" @@ -7741,12 +7750,36 @@ msgstr "Sicronizar filamentos de la lista AMS" msgid "Set filaments to use" msgstr "Elegir filamentos para usar" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Buscar cama, objeto y parte." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Al completar la operación, el proyecto %s se cerrará y se creará un nuevo proyecto." @@ -7931,6 +7964,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objeto multipieza detectado" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "¿Cargar estos archivos como un objeto único con múltiples piezas?\n" @@ -8135,6 +8174,18 @@ msgstr "" msgid "Sync now" msgstr "Sincronizar ahora" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Puedes mantener los perfiles modificados en el nuevo proyecto o descartarlos" @@ -9361,6 +9412,9 @@ msgstr "Vaso en espiral" msgid "First layer filament sequence" msgstr "Secuencia de primera capa de filamento" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Por Capa" @@ -9406,18 +9460,58 @@ msgstr "Ir a la página web" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -11929,6 +12023,9 @@ msgstr "Si aún así quieres imprimir, puedes activar la opción en Preferencias msgid "No extrusions under current settings." msgstr "No hay extrusiones con los ajustes actuales." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Modo de timelapse suave no está soportado cuando la secuencia \"por objeto\" está activada." @@ -11965,6 +12062,9 @@ msgstr "Es posible que desee reducir el tamaño de su modelo o cambiar la config msgid "Variable layer height is not supported with Organic supports." msgstr "La altura de capa adaptativa no es compatible con los soportes orgánicos." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Diámetros de boquillas y diámetros de filamento diferentes pueden no funcionar correctamente cuando la torre de purga está activada. Esta función es experimental, así que proceda con cautela." @@ -13954,6 +14054,48 @@ msgstr "Material de soporte" msgid "Support material is commonly used to print supports and support interfaces." msgstr "El material de soporte se utiliza habitualmente para imprimir soportes y la interfaz de los soportes." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filamento imprimible" @@ -16753,6 +16895,12 @@ msgstr "" "\n" "Fijando un valor en la cantidad de retracción antes del purgado se realizará cualquier exceso de retracción antes del purgado, de lo contrario se realizará después." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La torre de purga puede utilizarse para limpiar los residuos de la boquilla y estabilizar la presión de la recámara en el interior de la boquilla, con el fin de evitar defectos visuales al imprimir objetos." @@ -17809,6 +17957,9 @@ msgstr "La generación de la malla del archivo del modelo falló o no hay una fo msgid "The supplied file couldn't be read because it's empty." msgstr "No se ha podido leer el archivo proporcionado porque está vacío." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato de archivo desconocido: el archivo de entrada debe tener extensión .STL, .obj o .amf (.xml)." diff --git a/localization/i18n/eu/OrcaSlicer_eu.po b/localization/i18n/eu/OrcaSlicer_eu.po index e950ea3d2e..b0aadc66e4 100644 --- a/localization/i18n/eu/OrcaSlicer_eu.po +++ b/localization/i18n/eu/OrcaSlicer_eu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-07-20 13:33+0200\n" "Last-Translator: Manu Goiogana \n" "Language-Team: \n" @@ -2869,6 +2869,9 @@ msgstr "Editatu" msgid "Merge with" msgstr "Batu honekin" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Ezabatu filamentu hau" @@ -3150,6 +3153,9 @@ msgstr "Multzoa" msgid "Merge parts to an object" msgstr "Batu piezak objektu batean" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "Geruzak gehitu" @@ -7686,6 +7692,9 @@ msgstr "Pertsonalizatu uneko plaka" msgid "The %s nozzle can not print %s." msgstr "%s pitak ezin du %s inprimatu." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Ez da gomendatzen inprimatzean %1% eta %2% nahastea.\n" @@ -7808,12 +7817,36 @@ msgstr "Sinkronizatu filamentuen zerrenda AMSarekin" msgid "Set filaments to use" msgstr "Ezarri erabili beharreko filamentuak" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Bilatu plaka, objektua eta pieza." msgid "Pellets" msgstr "Pelletak" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Eragiketa amaitzean, %s proiektua itxi eta proiektu berri bat sortuko da." @@ -7998,6 +8031,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Pieza anitzeko objektua detektatu da" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Kargatu fitxategi hauek pieza anitzeko objektu bakar gisa?\n" @@ -8202,6 +8241,18 @@ msgstr "" msgid "Sync now" msgstr "Sinkronizatu orain" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Aldatutako aurrezarpenak proiektu berrirako gorde edo baztertu ditzakezu" @@ -9430,6 +9481,9 @@ msgstr "Espiral formako loreontzia" msgid "First layer filament sequence" msgstr "Lehen geruzako filamentuen sekuentzia" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Geruzaren arabera" @@ -9475,18 +9529,58 @@ msgstr "Joan web-orrira" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12032,6 +12126,9 @@ msgstr "Hala ere inprimatu nahi baduzu, aukera hau gaitu dezakezu: Hobespenak / msgid "No extrusions under current settings." msgstr "Uneko ezarpenekin ez dago estrusiorik." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Timelapsearen modu leuna ez da onartzen \"objektuka\" sekuentzia gaituta dagoenean." @@ -12068,6 +12165,9 @@ msgstr "Modeloaren tamaina txikitu edo uneko inprimatze-ezarpenak aldatu eta ber msgid "Variable layer height is not supported with Organic supports." msgstr "Geruza-altuera aldakorra ez da onartzen euskarri organikoekin." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Pitaren diametro eta filamentu-diametro desberdinek agian ez dute ondo funtzionatuko purgatze-dorrea gaituta dagoenean. Oso esperimentala da; jarraitu kontuz." @@ -14073,6 +14173,48 @@ msgstr "Euskarri-materiala" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Euskarri-materiala euskarriak eta euskarri-interfazeak inprimatzeko erabiltzen da normalean." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filamentua inprimagarria" @@ -16898,6 +17040,12 @@ msgstr "" "\n" "Beheko garbitu aurreko atzera-egite kantitatearen ezarpenean balio batezarriz gero, gehiegizko atzera-egitea garbiketa baino lehen egingo da; bestela, ondoren egingo da." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Purgatze-dorrea pitaren hondarrak garbitzeko eta pitaren barruko ganbera-presioa egonkortzeko erabil daiteke, objektuetan itxura-akatsak saihesteko." @@ -17956,6 +18104,9 @@ msgstr "Modelo-fitxategiaren sareztatzeak huts egin du edo ez dago baliozko form msgid "The supplied file couldn't be read because it's empty." msgstr "Emandako fitxategia ezin izan da irakurri hutsik dagoelako." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Fitxategi-formatu ezezaguna: sarrerako fitxategiak .stl, .obj edo .amf(.xml) luzapena izan behar du." diff --git a/localization/i18n/fr/OrcaSlicer_fr.po b/localization/i18n/fr/OrcaSlicer_fr.po index 5bd15b57cd..f0085ff16d 100644 --- a/localization/i18n/fr/OrcaSlicer_fr.po +++ b/localization/i18n/fr/OrcaSlicer_fr.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Guislain Cyril, Thomas Lété\n" @@ -2896,6 +2896,9 @@ msgstr "Éditer" msgid "Merge with" msgstr "Fusionner avec" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Supprimer ce filament" @@ -3178,6 +3181,9 @@ msgstr "Assemblé" msgid "Merge parts to an object" msgstr "Fusionner les pièces en un objet" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "Ajouter des couches" @@ -7739,6 +7745,9 @@ msgstr "Personnaliser le plateau actuel" msgid "The %s nozzle can not print %s." msgstr "La buse %s ne peut pas imprimer %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Le mélange de %1% avec %2% lors de l'impression n'est pas recommandé.\n" @@ -7864,12 +7873,36 @@ msgstr "Synchroniser la liste des filaments depuis l'AMS" msgid "Set filaments to use" msgstr "Définir les filaments à utiliser" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Recherche de plaque, d'objet et de pièce." msgid "Pellets" msgstr "Granulés" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Une fois votre opération terminée, le projet %s sera fermé et un nouveau projet sera créé." @@ -8054,6 +8087,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objet en plusieurs pièces détecté" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Charger ces fichiers en tant qu'objet unique avec plusieurs parties ?\n" @@ -8258,6 +8297,18 @@ msgstr "" msgid "Sync now" msgstr "Synchroniser maintenant" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Vous pouvez conserver les préréglages modifiés dans le nouveau projet ou les supprimer" @@ -9500,6 +9551,9 @@ msgstr "Vase spirale" msgid "First layer filament sequence" msgstr "Séquence d’impression de la première couche" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Par Couche" @@ -9545,18 +9599,58 @@ msgstr "Ouvrir la page internet" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12130,6 +12224,9 @@ msgstr "Si vous souhaitez tout de même imprimer, vous pouvez activer l’option msgid "No extrusions under current settings." msgstr "Aucune extrusion dans les paramètres actuels." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Le mode fluide du timelapse n'est pas pris en charge lorsque le mode d'impression « par objet » est activé." @@ -12166,6 +12263,9 @@ msgstr "Vous devez réduire la taille de votre modèle ou modifier les paramètr msgid "Variable layer height is not supported with Organic supports." msgstr "La hauteur de couche variable n’est pas prise en charge avec les supports organiques." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Différents diamètres de buses et de filaments peuvent ne pas fonctionner correctement lorsque la tour d’amorçage est activée. Il s’agit d’un projet très expérimental, il convient donc de procéder avec prudence." @@ -14175,6 +14275,48 @@ msgstr "Supports" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Le matériau de support est généralement utilisé pour imprimer le support et les interfaces de support" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament imprimable" @@ -17003,6 +17145,12 @@ msgstr "" "\n" "Le réglage d’une valeur dans le paramètre de quantité de rétraction avant essuyage ci-dessous permet d’effectuer toute rétraction excédentaire avant l’essuyage, sinon elle sera effectuée après l’essuyage." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La tour de purge peut être utilisée pour nettoyer les résidus sur la buse et stabiliser la pression du caisson à l'intérieur de la buse afin d'éviter les défauts d'apparence lors de l'impression d'objets." @@ -18069,6 +18217,9 @@ msgstr "Le maillage d'un fichier modèle a échoué ou la forme n'est pas valide msgid "The supplied file couldn't be read because it's empty." msgstr "Le fichier fourni n'a pas pu être lu car il est vide" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Format de fichier inconnu : le fichier d'entrée doit porter l'extension .stl, .obj ou .amf (.xml)." diff --git a/localization/i18n/hu/OrcaSlicer_hu.po b/localization/i18n/hu/OrcaSlicer_hu.po index 1f8c1022bb..e6dc44afcb 100644 --- a/localization/i18n/hu/OrcaSlicer_hu.po +++ b/localization/i18n/hu/OrcaSlicer_hu.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2948,6 +2948,9 @@ msgstr "Szerkesztés" msgid "Merge with" msgstr "Egyesítés ezzel" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Filament törlése" @@ -3247,6 +3250,9 @@ msgstr "Összeállítás" msgid "Merge parts to an object" msgstr "Tárgyak egyesítése egy objektummá" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Rétegek hozzáadása" @@ -7853,6 +7859,9 @@ msgstr "Aktuális tálca testreszabása" msgid "The %s nozzle can not print %s." msgstr "A(z) %s fúvóka nem tudja nyomtatni ezt: %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "A(z) %1% és %2% keverése nyomtatás közben nem ajánlott.\n" @@ -7978,12 +7987,36 @@ msgstr "Filamentlista szinkronizálása az AMS-ből" msgid "Set filaments to use" msgstr "Használni kívánt filament beállítása" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Tálca, objektum és tárgy keresése." msgid "Pellets" msgstr "Pelletek" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "A művelet befejezésekor a(z) %s projekt bezárul, majd új projekt jön létre." @@ -8172,6 +8205,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Több részből álló objektum észlelve" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Betöltöd ezeket a fájlokat több részből álló egyetlen objektumként?\n" @@ -8383,6 +8422,18 @@ msgstr "" msgid "Sync now" msgstr "Szinkronizálás most" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Megtarthatod az új projekt módosított beállításait, vagy elvetheted őket" @@ -9656,6 +9707,9 @@ msgstr "Spirál (váza)" msgid "First layer filament sequence" msgstr "Kezdőréteg filament sorrendje" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + # AI Translated msgid "By Layer" msgstr "Rétegenként" @@ -9705,18 +9759,58 @@ msgstr "Ugrás a weboldalra" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12320,6 +12414,9 @@ msgstr "Ha továbbra is szeretnél nyomtatni, engedélyezheted az opciót itt: B msgid "No extrusions under current settings." msgstr "A jelenlegi beállításokkal nincsenek extrudálások." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "A sima Timelapse nem használható, ha a nyomtatási sorrend \"Tárgyanként\"." @@ -12356,6 +12453,9 @@ msgstr "Próbáld meg csökkenteni a modell méretét vagy módosítani a jelenl msgid "Variable layer height is not supported with Organic supports." msgstr "A változó rétegmagasság nem működik az organikus támaszokkal." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Eltérő fúvókaátmérők és eltérő filamentátmérők mellett a törlőtorony nem biztos, hogy megfelelően működik. Ez nagyon kísérleti funkció, ezért körültekintően használd." @@ -14408,6 +14508,48 @@ msgstr "Támaszanyag" msgid "Support material is commonly used to print supports and support interfaces." msgstr "A támaszanyagot általában a támaszok és azok érintkező felületeinek nyomtatására használják." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament nyomtatható" @@ -17271,6 +17413,12 @@ msgstr "" "\n" "Ha az alábbi \"visszahúzási mennyiség törlés előtt\" beállításban értéket adsz meg, akkor az esetleges többletvisszahúzás a törlés előtt történik meg, különben utána." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "A törlőtorony a fúvókán lévő maradék eltávolítására és a fúvóka belsejében lévő nyomás stabilizálására szolgál, hogy elkerülhetők legyenek a megjelenésbeli hibák az objektumok nyomtatásakor." @@ -18344,6 +18492,9 @@ msgstr "A modellfájl hálósítása sikertelen volt, vagy nincs érvényes alak msgid "The supplied file couldn't be read because it's empty." msgstr "A megadott fájl nem olvasható be, mert üres" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Ismeretlen fájlformátum. A bemeneti fájlnak .stl, .obj vagy .amf(.xml) kiterjesztésűnek kell lennie." diff --git a/localization/i18n/it/OrcaSlicer_it.po b/localization/i18n/it/OrcaSlicer_it.po index ac9d5531e5..9c6edebbb7 100644 --- a/localization/i18n/it/OrcaSlicer_it.po +++ b/localization/i18n/it/OrcaSlicer_it.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2953,6 +2953,9 @@ msgstr "Modifica" msgid "Merge with" msgstr "Unisci con" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Elimina questo filamento" @@ -3252,6 +3255,9 @@ msgstr "Assemblaggio" msgid "Merge parts to an object" msgstr "Unisci parti in un oggetto" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Aggiungi strati" @@ -7857,6 +7863,9 @@ msgstr "Personalizza il piatto corrente" msgid "The %s nozzle can not print %s." msgstr "L'ugello %s non può stampare %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Non è consigliato miscelare %1% con %2% nella stampa.\n" @@ -7982,12 +7991,36 @@ msgstr "Sincronizza l'elenco filamenti dall'AMS" msgid "Set filaments to use" msgstr "Imposta filamenti da usare" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Cerca piatto, oggetto e parte." msgid "Pellets" msgstr "Granuli" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Al completamento dell'operazione, il progetto %s verrà chiuso e ne verrà creato uno nuovo." @@ -8173,6 +8206,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Rilevato oggetto in più parti" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Caricare questi file come un singolo oggetto con più parti?\n" @@ -8383,6 +8422,18 @@ msgstr "" msgid "Sync now" msgstr "Sincronizza ora" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "È possibile conservare i profili modificati per il nuovo progetto o scartarli" @@ -9677,6 +9728,9 @@ msgstr "Vaso a spirale" msgid "First layer filament sequence" msgstr "Sequenza filamenti primo strato" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Per strato" @@ -9723,18 +9777,58 @@ msgstr "Vai alla pagina web" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12341,6 +12435,9 @@ msgstr "Se desideri comunque stampare, puoi abilitare l'opzione in Preferenze / msgid "No extrusions under current settings." msgstr "Nessuna estrusione con le impostazioni attuali." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "La modalità fluida del timelapse non è supportata quando è abilitata la sequenza \"Per oggetto\"." @@ -12377,6 +12474,9 @@ msgstr "È possibile ridurre le dimensioni del modello o modificare le impostazi msgid "Variable layer height is not supported with Organic supports." msgstr "Altezza strato adattiva non è compatibile con i Supporti organici." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Ugelli e filamenti di diverso diametro potrebbero non funzionare correttamente quando è abilitata la torre di spurgo. Questa funzione è sperimentale, quindi procedere con cautela." @@ -14426,6 +14526,48 @@ msgstr "Materiale di supporto" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Il materiale di supporto viene comunemente utilizzato per stampare supporti e interfacce di supporto." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filamento stampabile" @@ -17296,6 +17438,12 @@ msgstr "" "\n" "Impostando un valore di quantità di retrazione prima dell'impostazione di spurgo di seguito, verra eseguita qualsiasi retrazione in eccesso prima dello spurgo. Altrimenti verrà eseguita dopo." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "La torre di spurgo può essere utilizzata per pulire i residui presenti sull'ugello e stabilizzare la pressione della camera all'interno dell'ugello, al fine di evitare difetti estetici durante la stampa." @@ -18367,6 +18515,9 @@ msgstr "La generazione della mesh del file del modello è fallita o la forma non msgid "The supplied file couldn't be read because it's empty." msgstr "Impossibile leggere il file fornito perché è vuoto" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato file sconosciuto: il file di input deve avere un'estensione .stl, .obj o .amf(.xml)." diff --git a/localization/i18n/ja/OrcaSlicer_ja.po b/localization/i18n/ja/OrcaSlicer_ja.po index 55ad80089b..25d81e9ccc 100644 --- a/localization/i18n/ja/OrcaSlicer_ja.po +++ b/localization/i18n/ja/OrcaSlicer_ja.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -2969,6 +2969,9 @@ msgstr "編集" msgid "Merge with" msgstr "結合" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "このフィラメントを削除" @@ -3266,6 +3269,9 @@ msgstr "アセンブリ" msgid "Merge parts to an object" msgstr "パーツをオブジェクトに結合" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "積層を追加" @@ -7865,6 +7871,9 @@ msgstr "現在のプレートをカスタマイズ" msgid "The %s nozzle can not print %s." msgstr "%sノズルは%sを印刷できません。" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "%1%と%2%を混合して印刷することは推奨されません。\n" @@ -7990,12 +7999,36 @@ msgstr "AMSと素材を同期" msgid "Set filaments to use" msgstr "フィラメントを選択" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "プレート、オブジェクト、パーツを検索。" msgid "Pellets" msgstr "ペレット" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "操作完了後、%sプロジェクトが閉じられ、新しいプロジェクトが作成されます。" @@ -8188,6 +8221,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "マルチパーツ検出" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "これらのファイルを一つのオブジェクトとしてロードしますか?\n" @@ -8401,6 +8440,18 @@ msgstr "" msgid "Sync now" msgstr "今すぐ同期" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "変更したプリセットをプロジェクト内に保存するか、破棄もできます" @@ -9699,6 +9750,9 @@ msgstr "スパイラル" msgid "First layer filament sequence" msgstr "初期レイヤーフィラメント順序" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "レイヤー別" @@ -9745,18 +9799,58 @@ msgstr "ウェブページに移動" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12381,6 +12475,9 @@ msgstr "それでも印刷する場合は、環境設定 / 制御 / スライス msgid "No extrusions under current settings." msgstr "現在の設定では造形しません" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "オブジェクト順で造形するでは、この機能を使用できません。" @@ -12417,6 +12514,9 @@ msgstr "モデルのサイズを小さくするか、現在のプリント設定 msgid "Variable layer height is not supported with Organic supports." msgstr "可変レイヤー高さはオーガニックサポートではサポートされていません。" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "ノズル径やフィラメント径が異なる場合、プライムタワーを有効にすると正しく動作しないことがあります。非常に実験的な機能ですので、慎重にお進みください。" @@ -14556,6 +14656,48 @@ msgstr "サポート材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "サポート素材は、サポート又はサポート接触面の造形によく使われます。" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "フィラメント印刷可能" @@ -17671,6 +17813,12 @@ msgstr "" "\n" "下の「ワイプ前のリトラクション量」設定に値を設定すると、超過分のリトラクションはワイプの前に行われます。それ以外の場合はワイプの後に行われます。" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "ワイプタワーは、ノズルに残った樹脂を除去し、ノズル内のチャンバー圧力を安定させることで、オブジェクト印刷時の外観不良を防ぐために使用できます。" @@ -18842,6 +18990,9 @@ msgstr "モデルファイルのメッシュ処理に失敗したか、有効な msgid "The supplied file couldn't be read because it's empty." msgstr "提供されたファイルは空であるため読み込めませんでした。" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "ファイル形式が不明です。入力ファイルの拡張子は .stl、.obj、.amf(.xml)である必要があります。" diff --git a/localization/i18n/ko/OrcaSlicer_ko.po b/localization/i18n/ko/OrcaSlicer_ko.po index 66ce7b49a1..3c2a65c4e7 100644 --- a/localization/i18n/ko/OrcaSlicer_ko.po +++ b/localization/i18n/ko/OrcaSlicer_ko.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2025-06-02 17:12+0900\n" "Last-Translator: crwusiz \n" "Language-Team: \n" @@ -2970,6 +2970,9 @@ msgstr "편집" msgid "Merge with" msgstr "병합" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "이 필라멘트 삭제" @@ -3266,6 +3269,9 @@ msgstr "조립" msgid "Merge parts to an object" msgstr "부품을 하나의 객체로 병합" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "레이어 추가" @@ -7877,6 +7883,9 @@ msgstr "사용자 정의 플레이트" msgid "The %s nozzle can not print %s." msgstr "%s 노즐은 %s 을 인쇄할 수 없습니다." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "%1%와 %2% 혼합 출력을 권장하지 않습니다.\n" @@ -8003,12 +8012,36 @@ msgstr "AMS에서 필라멘트 목록 동기화" msgid "Set filaments to use" msgstr "사용할 필라멘트 설정" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "플레이트, 객체 및 부품을 검색합니다." msgid "Pellets" msgstr "펠릿" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "작업을 완료하면 %s 프로젝트가 닫히고 새 프로젝트가 만들어집니다." @@ -8207,6 +8240,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "여러 부품으로 구성된 객체 감지됨" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "이 파일을 여러 부품이 있는 단일 객체로 로드하시겠습니까?\n" @@ -8435,6 +8474,18 @@ msgstr "" msgid "Sync now" msgstr "지금 동기화" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "수정된 사전 설정을 새 프로젝트에 유지하거나 삭제할 수 있습니다" @@ -9789,6 +9840,9 @@ msgstr "나선형 꽃병 모드" msgid "First layer filament sequence" msgstr "첫 번째 레이어 필라멘트 순서" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "레이어별" @@ -9835,18 +9889,58 @@ msgstr "웹 페이지로 이동" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12513,6 +12607,9 @@ msgstr "그래도 출력하려면 기본 설정 / 제어 / 슬라이싱 / 혼합 msgid "No extrusions under current settings." msgstr "현재 설정에 압출기가 없습니다." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "타임랩스의 유연 모드는 \"객체별\" 출력순서가 활성화된 경우 지원되지 않습니다." @@ -12551,6 +12648,9 @@ msgstr "모델 크기를 줄이거나 현재 출력 설정을 변경하고 다 msgid "Variable layer height is not supported with Organic supports." msgstr "유기체 서포트에서는 가변 레이어 높이가 지원되지 않습니다." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "프라임 타워를 활성화하면 노즐 직경과 필라멘트 직경이 다르면 제대로 작동하지 않을 수 있습니다. 매우 실험적인 기능이므로 주의해서 사용하시기 바랍니다." @@ -14658,6 +14758,48 @@ msgstr "서포트 재료" msgid "Support material is commonly used to print supports and support interfaces." msgstr "서포트 재료는 일반적으로 서포트 및 서포트 접점을 출력하는 데 사용됩니다" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "필라멘트 출력 가능" @@ -17617,6 +17759,12 @@ msgstr "" "\n" "아래의 와이프 전 후퇴량 설정에서 값을 설정하면 와이프 전에 초과 후퇴가 수행되고, 그렇지 않으면 와이프 후에 수행됩니다." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "프라임 타워는 객체를 출력할 때 외관 결함을 방지하기 위해 노즐의 잔류물을 청소하고 노즐 내부의 압력을 안정화하는 데 사용할 수 있습니다." @@ -18720,6 +18868,9 @@ msgstr "모델 파일의 메싱이 실패했거나 유효한 형태가 없습니 msgid "The supplied file couldn't be read because it's empty." msgstr "제공된 파일이 비어 있어 읽을 수 없습니다" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "알 수 없는 파일 형식: 입력 파일의 확장자는 .stl, .obj 또는 .amf(.xml)여야 합니다." diff --git a/localization/i18n/lt/OrcaSlicer_lt.po b/localization/i18n/lt/OrcaSlicer_lt.po index 866f5fa3f6..e6fdc28d40 100644 --- a/localization/i18n/lt/OrcaSlicer_lt.po +++ b/localization/i18n/lt/OrcaSlicer_lt.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-07-02 14:13+0300\n" "Last-Translator: Gintaras Kučinskas \n" "Language-Team: \n" @@ -2937,6 +2937,9 @@ msgstr "Redaguoti" msgid "Merge with" msgstr "Sujungti su" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Ištrinti šią giją" @@ -3238,6 +3241,9 @@ msgstr "Surinkimas" msgid "Merge parts to an object" msgstr "Sujungti dalis į objektą" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Pridėti sluoksnių" @@ -7841,6 +7847,9 @@ msgstr "Individualizuoti esamą plokštę" msgid "The %s nozzle can not print %s." msgstr "%s purkštukas negali spausdinti %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "" @@ -7974,12 +7983,36 @@ msgstr "Sinchronizuoti gijų sąrašą iš AMS" msgid "Set filaments to use" msgstr "Nustatyti gijas naudojimui" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Plokštės, objekto ir dalies paieška." msgid "Pellets" msgstr "Granulės" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Baigus šią operaciją, projektas „%s“ bus uždarytas ir bus sukurtas naujas projektas." @@ -8164,6 +8197,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Aptiktas kelių dalių objektas" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Ar įkelti šiuos failus kaip vieną objektą su keliomis detalėmis?\n" @@ -8378,6 +8417,18 @@ msgstr "" msgid "Sync now" msgstr "Sinchronizuoti dabar" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Pakeistus profilius galite išsaugoti naujam projektui arba juos atmesti" @@ -9618,6 +9669,9 @@ msgstr "Spiralinė vaza" msgid "First layer filament sequence" msgstr "Pirmojo sluoksnio gijos eiga" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Pagal sluoksnį" @@ -9663,18 +9717,58 @@ msgstr "Pereiti į interneto puslapį" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12264,6 +12358,9 @@ msgstr "Jei vis tiek norite spausdinti, galite įjungti šią parinktį skiltyje msgid "No extrusions under current settings." msgstr "Pagal dabartinius nustatymus nėra išspaudimų." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Sklandus pakadrinio filmavimo (timelapse) režimas nepalaikomas, kai įjungta spausdinimo seka „pagal objektą“." @@ -12300,6 +12397,9 @@ msgstr "Galbūt norėsite sumažinti modelio dydį arba pakeisti esamus spausdin msgid "Variable layer height is not supported with Organic supports." msgstr "Kintamas sluoksnio aukštis nepalaikomas su \" Organinėmis atramomis\"." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Skirtingo skersmens purkštukai ir skirtingo skersmens gijos gali neveikti gerai, kai įjungtas valymo bokštas. Tai labai eksperimentinė priemonė, todėl elkitės atsargiai." @@ -14307,6 +14407,48 @@ msgstr "Atramų gija" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Atramų gija paprastai naudojama atramoms ir atramų skiriamiesiems sluoksniams spausdinti." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Gija tinkama spausdinti" @@ -17137,6 +17279,12 @@ msgstr "" "\n" "Jei žemiau esančiame nustatyme „Įtraukimo kiekis prieš nuvalymą“ nurodysite reikšmę, perteklinis gijos įtraukimas bus atliktas prieš nuvalymą, kitu atveju – po jo." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Valymo bokštelis (angl. \"wiping tower\") gali būti naudojamas likučiams nuo purkštuko nuvalyti ir purkštuko viduje esančiam slėgiui stabilizuoti, siekiant išvengti išvaizdos defektų spausdinant objektus." @@ -18220,6 +18368,9 @@ msgstr "Nepavyko suformuoti modelio failo poligonažo arba jame nėra tinkamos g msgid "The supplied file couldn't be read because it's empty." msgstr "Pateikto failo nepavyko nuskaityti, nes jis yra tuščias." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Nežinomas failo formatas: įvesties failas privalo turėti .stl, .obj arba .amf(.xml) plėtinį." diff --git a/localization/i18n/nl/OrcaSlicer_nl.po b/localization/i18n/nl/OrcaSlicer_nl.po index 470aa9fa86..fd1aec7c3f 100644 --- a/localization/i18n/nl/OrcaSlicer_nl.po +++ b/localization/i18n/nl/OrcaSlicer_nl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -3197,6 +3197,9 @@ msgstr "Bewerken" msgid "Merge with" msgstr "Samenvoegen met" +msgid "Decompose Color" +msgstr "" + # AI Translated msgid "Delete this filament" msgstr "Dit filament verwijderen" @@ -3519,6 +3522,9 @@ msgstr "Montage" msgid "Merge parts to an object" msgstr "Onderdelen samenvoegen tot een object" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Lagen toevoegen" @@ -8558,6 +8564,9 @@ msgstr "Huidig printbed aanpassen" msgid "The %s nozzle can not print %s." msgstr "Het %s-mondstuk kan %s niet printen." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8701,6 +8710,21 @@ msgstr "Synchroniseer filamentlijst vanuit AMS" msgid "Set filaments to use" msgstr "Stel filamenten in om te gebruiken" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Zoek plaat, object en onderdeel." @@ -8708,6 +8732,15 @@ msgstr "Zoek plaat, object en onderdeel." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8915,6 +8948,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Object met meerdere onderdelen gedetecteerd" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Wilt u deze bestanden laden als een enkel object bestaande uit meerdere onderdelen?\n" @@ -9146,6 +9185,18 @@ msgstr "" msgid "Sync now" msgstr "Nu synchroniseren" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Je kunt de aangepaste voorinstellingen bewaren voor het nieuwe project of ze laten vervallen" @@ -10550,6 +10601,9 @@ msgstr "Spiraalvaas" msgid "First layer filament sequence" msgstr "Eerste laag filamentvolgorde" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Op laag" @@ -10596,18 +10650,58 @@ msgstr "Ga naar de website" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -13448,6 +13542,9 @@ msgstr "Als u toch wilt printen, kunt u de optie inschakelen via Voorkeuren / Be msgid "No extrusions under current settings." msgstr "Geen extrusion onder de huidige instellingen" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Vloeiende modus van timelapse wordt niet ondersteund wanneer \"per object\" sequentie is ingeschakeld." @@ -13490,6 +13587,9 @@ msgstr "Verklein eventueel uw model of wijzig de huidige printinstellingen en pr msgid "Variable layer height is not supported with Organic supports." msgstr "Variabele laaghoogte wordt niet ondersteund met organische steunen." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Verschillende mondstukdiameters en verschillende filamentdiameters werken mogelijk niet goed wanneer de prime toren is ingeschakeld. Dit is zeer experimenteel; ga daarom voorzichtig te werk." @@ -15762,6 +15862,48 @@ msgstr "Support materiaal" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Support materiaal wordt vaak gebruikt om support en support interfaces af te drukken." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + # AI Translated msgid "Filament printable" msgstr "Filament printbaar" @@ -18985,6 +19127,12 @@ msgstr "" "\n" "Als u hieronder een waarde instelt bij de terugtrekhoeveelheid vóór het vegen, wordt de overtollige terugtrekking vóór het vegen uitgevoerd; anders gebeurt dat erna." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "De veegtoren kan worden gebruikt om resten op het mondstuk te verwijderen en de druk in het mondstuk te stabiliseren om uiterlijke gebreken bij het printen van objecten te voorkomen." @@ -20251,6 +20399,9 @@ msgstr "Het meshen van een modelbestand is mislukt of er is geen geldige vorm." msgid "The supplied file couldn't be read because it's empty." msgstr "Het opgegeven bestand kon niet worden gelezen omdat het leeg is." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + # AI Translated msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Onbekende bestandsindeling: het invoerbestand moet de extensie .stl, .obj of .amf(.xml) hebben." diff --git a/localization/i18n/pl/OrcaSlicer_pl.po b/localization/i18n/pl/OrcaSlicer_pl.po index 4adcf037ca..7fa43e8993 100644 --- a/localization/i18n/pl/OrcaSlicer_pl.po +++ b/localization/i18n/pl/OrcaSlicer_pl.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer 2.3.0-rc\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: \n" "Last-Translator: Krzysztof Morga <>\n" "Language-Team: \n" @@ -3012,6 +3012,9 @@ msgstr "Edytuj" msgid "Merge with" msgstr "Scal z" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Usuń ten filament" @@ -3319,6 +3322,9 @@ msgstr "Złożenie" msgid "Merge parts to an object" msgstr "Scal części w obiekt" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Dodaj warstwy" @@ -8036,6 +8042,9 @@ msgstr "Dostosuj bieżący stół" msgid "The %s nozzle can not print %s." msgstr "Dysza %s nie może drukować %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Mieszanie %1% z %2% podczas druku nie jest zalecane.\n" @@ -8162,12 +8171,36 @@ msgstr "Synchronizuj listę filamentów z AMS" msgid "Set filaments to use" msgstr "Wybierz filamenty do użycia" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Szukaj stołu, obiektu i części." msgid "Pellets" msgstr "Granulat" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Po zakończeniu operacji projekt %s zostanie zamknięty i zostanie utworzony nowy projekt." @@ -8363,6 +8396,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Wykryto obiekt składający się z wielu części" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Czy wczytać te pliki jako pojedynczy obiekt składający się z wielu części?\n" @@ -8591,6 +8630,18 @@ msgstr "" msgid "Sync now" msgstr "Synchronizuj teraz" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Można zachować zmodyfikowane profile w nowym projekcie lub je odrzucić." @@ -9945,6 +9996,9 @@ msgstr "Tryb wazy" msgid "First layer filament sequence" msgstr "Sekwencja koloru pierwszej warstwy" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Wg warstwy" @@ -9991,18 +10045,58 @@ msgstr "Przejdź na stronę" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12684,6 +12778,9 @@ msgstr "Jeśli mimo to chcesz drukować, możesz włączyć opcję w Preferencje msgid "No extrusions under current settings." msgstr "Brak ekstruzji przy obecnych ustawieniach." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Tryb „Wygładzony” timelapse nie jest obsługiwany, gdy włączona jest sekwencja druku „według obiektu”." @@ -12722,6 +12819,9 @@ msgstr "Może być konieczne zmniejszenie rozmiaru modelu lub zmiana bieżących msgid "Variable layer height is not supported with Organic supports." msgstr "Zmienna wysokość warstwy nie jest dostępna w przypadku podpór organicznych." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Różne średnice dysz i filamentu mogą nie działać poprawnie, gdy włączona jest wieża czyszcząca. Jest to mocno eksperymentalna funkcja, więc zaleca się ostrożność." @@ -14825,6 +14925,48 @@ msgstr "Materiał podporowy" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Materiał podporowy jest powszechnie używany do drukowania podpór i warstw łączących podpory z modelem" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament do druku" @@ -17795,6 +17937,12 @@ msgstr "" "\n" "Ustawienie wartości w ilości cofania przed ustawieniem czyszczenia poniżej spowoduje wykonanie nadmiernego cofania przed czyszczeniem, w przeciwnym razie zostanie wykonane po nim." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Wieża czyszcząca może być używana do czyszczenia resztek na dyszy i stabilizacji ciśnienia w komorze wewnątrz dyszy, aby uniknąć defektów wyglądu podczas drukowania obiektów." @@ -18896,6 +19044,9 @@ msgstr "Siatkowanie pliku modelu nie powiodło się lub nie ma prawidłowego ksz msgid "The supplied file couldn't be read because it's empty." msgstr "Dostarczony plik nie mógł być odczytany, ponieważ jest pusty" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Nieznany format pliku. Plik wejściowy musi mieć rozszerzenie .stl, .obj, .amf(.xml)." diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index 0e1e34c7d7..ceb095306d 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-07-26 11:14-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" @@ -2836,6 +2836,9 @@ msgstr "Editar" msgid "Merge with" msgstr "Mesclar com" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Apagar este filamento" @@ -3118,6 +3121,9 @@ msgstr "Montagem" msgid "Merge parts to an object" msgstr "Mesclar peças com um objeto" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "Adicionar camadas" @@ -7637,6 +7643,9 @@ msgstr "Personalizar a placa atual" msgid "The %s nozzle can not print %s." msgstr "O bico %s não pode imprimir %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Misturar %1% com %2% na impressão não é recomendado.\n" @@ -7761,12 +7770,36 @@ msgstr "Sincronizar lista de filamentos do AMS" msgid "Set filaments to use" msgstr "Definir filamentos para usar" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Pesquisar placa, objeto e peça." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "Após a conclusão da sua operação, o projeto %s será encerrado e um novo projeto será criado." @@ -7951,6 +7984,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objeto multi-peça detectado" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Carregar esses arquivos como um único objeto com múltiplas peças?\n" @@ -8158,6 +8197,18 @@ msgstr "" msgid "Sync now" msgstr "Sincronizar agora" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Você pode manter as predefinições modificadas no novo projeto ou descartá-las" @@ -9402,6 +9453,9 @@ msgstr "Vaso espiral" msgid "First layer filament sequence" msgstr "Sequência de filamento da primeira camada" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Por Camada" @@ -9447,18 +9501,58 @@ msgstr "Ir para a página web" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12011,6 +12105,9 @@ msgstr "Se ainda assim desejar imprimir, você pode ativar a opção em Preferê msgid "No extrusions under current settings." msgstr "Nenhuma extrusão com as configurações atuais." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "O modo suave do timelapse não é suportado quando a sequência \"por objeto\" está ativada." @@ -12047,6 +12144,9 @@ msgstr "Você pode querer reduzir o tamanho do seu modelo ou alterar as configur msgid "Variable layer height is not supported with Organic supports." msgstr "A altura de camada variável não é suportada com suportes Orgânicos." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Diferentes diâmetros de bico e diferentes diâmetros de filamento podem não funcionar bem quando a torre de purga estiver habilitada. É muito experimental, então prossiga com cautela." @@ -14045,6 +14145,48 @@ msgstr "Material de suporte" msgid "Support material is commonly used to print supports and support interfaces." msgstr "O material de suporte é comumente usado para imprimir suportes e interfaces de suporte." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filamento imprimível" @@ -16846,6 +16988,12 @@ msgstr "" "\n" "Definir um valor na configuração de quantidade de retração antes da limpeza abaixo executará qualquer retração em excesso antes da limpeza, caso contrário, será realizada após." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "A torre de purga pode ser usada para limpar o resíduo no bico e estabilizar a pressão na câmara dentro do bico, a fim de evitar defeitos de aparência ao imprimir objetos." @@ -17902,6 +18050,9 @@ msgstr "A geração da malha do arquivo do modelo falhou ou não há forma váli msgid "The supplied file couldn't be read because it's empty." msgstr "O arquivo fornecido não pôde ser lido porque está vazio." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Formato de arquivo desconhecido: o arquivo de entrada deve ter extensão .stl, .obj, .amf(.xml)." diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po index debec4f832..fc26e99615 100644 --- a/localization/i18n/ru/OrcaSlicer_ru.po +++ b/localization/i18n/ru/OrcaSlicer_ru.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: OrcaSlicer V2.5.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-02-25 13:38+0300\n" "Last-Translator: Felix14_v2\n" "Language-Team: Felix14_v2 (ДС/ТГ: @felix14_v2, почта: aleks111001@list.ru), Andylg \n" @@ -2911,6 +2911,9 @@ msgstr "Правка" msgid "Merge with" msgstr "Объединить с" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Удалить материал" @@ -3222,6 +3225,9 @@ msgstr "Сборка" msgid "Merge parts to an object" msgstr "Сборка моделей" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # Запись в истории действий msgid "Add layers" msgstr "Добавление слоёв" @@ -7897,6 +7903,9 @@ msgstr "Настроить стол" msgid "The %s nozzle can not print %s." msgstr "Внимание: «%s» экструдер не может печатать %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Совмещение %1% с %2% при печати не рекомендуется.\n" @@ -8027,12 +8036,36 @@ msgstr "Синхронизировать материалы" msgid "Set filaments to use" msgstr "Выбрать материал" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Поиск стола, модели или части..." msgid "Pellets" msgstr "Гранулы" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + # Порядок слов сильно зависит от контекста; не могу воспроизвести в интерфейсе. По идее, выводится при bool Sidebar::is_new_project_in_gcode3mf(), но что-либо менять в нарезанном .gcode.3mf вообще нельзя #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8223,6 +8256,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Обнаружена модель, состоящая из нескольких частей" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Загрузить эти файлы как единую модель, состоящую из нескольких частей?\n" @@ -8437,6 +8476,18 @@ msgstr "" msgid "Sync now" msgstr "Синхронизировать" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Вы можете перенести сделанные изменения в новый проект или отказаться от их сохранения" @@ -9694,6 +9745,9 @@ msgstr "Режим вазы" msgid "First layer filament sequence" msgstr "Очерёдность материалов на первом слое" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Послойно" @@ -9739,18 +9793,58 @@ msgstr "Перейти на страницу" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12341,6 +12435,9 @@ msgstr "Если вас это не пугает, эту проверку мож msgid "No extrusions under current settings." msgstr "При текущих настройках экструзия отсутствует." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Плавный режим таймлапса не поддерживается, когда включена последовательность печати моделей по очереди." @@ -12379,6 +12476,9 @@ msgstr "Попробуйте уменьшить размер модели или msgid "Variable layer height is not supported with Organic supports." msgstr "Функция переменной высоты слоя несовместима с органическим стилем древовидных поддержек." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Совместное использование черновой башни с разными диаметрами сопел и прутков может привести к некорректной нарезке при включённой черновой башне. Этот метод работы экспериментальный, поэтому будьте осторожны при использовании." @@ -14526,6 +14626,48 @@ msgstr "Материал поддержки" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Обычно используется для печати поддержки и связующего слоя (интерфейса)." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + # ??? Настройка в режиме разработчика. Должна отображаться где-то в настройках # профиля, но поиском не ищется. msgid "Filament printable" @@ -17698,6 +17840,12 @@ msgstr "" "\n" "Внимание: процент первичного отката будет увеличен, если длины очистки окажется недостаточно при текущей скорости отката (или из-за иных ограничений)." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Черновая башня – специальная структура, которая используется для прочистки сопла от остатков материала и стабилизации давления внутри сопла при смене экструдера, чтобы избежать дефектов на поверхности печатаемой модели." @@ -18856,6 +19004,9 @@ msgstr "Не удалось обнаружить форму или создат msgid "The supplied file couldn't be read because it's empty." msgstr "Невозможно прочитать файл, так как он пуст." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Неизвестный формат файла: входной файл должен иметь расширение *.stl, *.obj или *.amf(.xml)." diff --git a/localization/i18n/sv/OrcaSlicer_sv.po b/localization/i18n/sv/OrcaSlicer_sv.po index 7add432a60..0e42c18f84 100644 --- a/localization/i18n/sv/OrcaSlicer_sv.po +++ b/localization/i18n/sv/OrcaSlicer_sv.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -3289,6 +3289,9 @@ msgstr "Redigera" msgid "Merge with" msgstr "Slå ihop med" +msgid "Decompose Color" +msgstr "" + # AI Translated msgid "Delete this filament" msgstr "Radera detta filament" @@ -3610,6 +3613,9 @@ msgstr "Montering" msgid "Merge parts to an object" msgstr "Slå ihop delar till ett objekt" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Lägg till lager" @@ -8650,6 +8656,9 @@ msgstr "Anpassa aktuell platta" msgid "The %s nozzle can not print %s." msgstr "Nozzeln %s kan inte skriva ut %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8793,6 +8802,21 @@ msgstr "Synkronisera filament listan från AMS" msgid "Set filaments to use" msgstr "Ställ in filament som ska användas" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Sök platta, objekt och del." @@ -8800,6 +8824,15 @@ msgstr "Sök platta, objekt och del." msgid "Pellets" msgstr "Pellets" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -9006,6 +9039,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Objekt i flera delar har upptäckts" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Ladda dessa filer som ett enkelt objekt med multipla delar?\n" @@ -9235,6 +9274,18 @@ msgstr "" msgid "Sync now" msgstr "Synkronisera nu" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Fortsätt med redigerings inställningarna till nytt projekt eller avfärda dem" @@ -10664,6 +10715,9 @@ msgstr "Spiral vas" msgid "First layer filament sequence" msgstr "Första lagrets filament sekvens" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Per lager" @@ -10710,18 +10764,58 @@ msgstr "Växla till hemsidan" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -13610,6 +13704,9 @@ msgstr "Om du ändå vill skriva ut kan du aktivera alternativet i Inställninga msgid "No extrusions under current settings." msgstr "Nuvarande inställning har ingen extrudering." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Smooth läge för timelapse stöds inte när ”per objekt” -sekvens är aktiverad." @@ -13652,6 +13749,9 @@ msgstr "Du kan behöva minska modellens storlek eller ändra de aktuella utskrif msgid "Variable layer height is not supported with Organic supports." msgstr "Variabel lagerhöjd stöds inte med organiska support." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + # AI Translated msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Olika nozzeldiametrar och olika filamentdiametrar kanske inte fungerar bra när prime tornet är aktiverat. Det är mycket experimentellt, så var försiktig." @@ -15950,6 +16050,48 @@ msgstr "Supportmaterial" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Support material används ofta för att skriva ut support och stödja gränssnittet" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + # AI Translated msgid "Filament printable" msgstr "Filament utskrivbart" @@ -19206,6 +19348,12 @@ msgstr "" "\n" "Om du anger ett värde i inställningen reduktionsmängd före avtorkning nedan utförs eventuell överskjutande reduktion före avtorkningen, annars utförs den efteråt." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Avstryknings tornet kan användas för att avlägsna rester på munstycket och stabilisera kammartrycket inuti munstycket för att undvika utseendefel vid utskrift av objekt." @@ -20479,6 +20627,9 @@ msgstr "Det gick inte att skapa mesh från modellfilen, eller så saknas en gilt msgid "The supplied file couldn't be read because it's empty." msgstr "Den medföljande filen kunde inte läsas eftersom den är tom." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Okänt filformat: indata filen måste ha tillägget .stl, .obj eller .amf(.xml)." diff --git a/localization/i18n/th/OrcaSlicer_th.po b/localization/i18n/th/OrcaSlicer_th.po index 60163fe29b..ec74d28213 100644 --- a/localization/i18n/th/OrcaSlicer_th.po +++ b/localization/i18n/th/OrcaSlicer_th.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-06-19 13:40+0700\n" "Last-Translator: Icezaza\n" "Language-Team: Thai\n" @@ -2942,6 +2942,9 @@ msgstr "แก้ไข" msgid "Merge with" msgstr "รวมกับ" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "ลบเส้นพลาสติกนี้" @@ -3237,6 +3240,9 @@ msgstr "การประกอบ" msgid "Merge parts to an object" msgstr "รวมชิ้นส่วนเป็นวัตถุเดียว" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "เพิ่มเลเยอร์" @@ -7813,6 +7819,9 @@ msgstr "ปรับแต่งแผ่นปัจจุบัน" msgid "The %s nozzle can not print %s." msgstr "หัวฉีด %s ไม่สามารถพิมพ์ %s ได้" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "ไม่แนะนำให้ผสม %1% กับ %2% ในการพิมพ์\n" @@ -7938,12 +7947,36 @@ msgstr "ประสานรายการเส้นพลาสติกจ msgid "Set filaments to use" msgstr "ตั้งค่าเส้นพลาสติกที่จะใช้" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "ค้นหาจาน วัตถุ และชิ้นส่วน" msgid "Pellets" msgstr "เม็ด" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "หลังจากเสร็จสิ้นการดำเนินการของคุณ โครงการ %s จะถูกปิดและสร้างโครงการใหม่" @@ -8128,6 +8161,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "ตรวจพบวัตถุที่มีหลายส่วน" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "โหลดไฟล์เหล่านี้เป็นออบเจ็กต์เดียวที่มีหลายส่วนใช่ไหม\n" @@ -8338,6 +8377,18 @@ msgstr "" msgid "Sync now" msgstr "ซิงค์เลย" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "คุณสามารถเก็บค่าที่ตั้งไว้ล่วงหน้าที่แก้ไขแล้วไว้ในโปรเจ็กต์ใหม่หรือทิ้งก็ได้" @@ -9591,6 +9642,9 @@ msgstr "แจกันเกลียว" msgid "First layer filament sequence" msgstr "ลำดับเส้นพลาสติกชั้นแรก" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "โดยเลเยอร์" @@ -9636,18 +9690,58 @@ msgstr "ข้ามไปที่หน้าเว็บ" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12231,6 +12325,9 @@ msgstr "หากคุณยังต้องการพิมพ์ คุ msgid "No extrusions under current settings." msgstr "ไม่มีการอัดขึ้นรูปภายใต้การตั้งค่าปัจจุบัน" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "ไม่รองรับโหมดไทม์แลปส์แบบราบรื่นเมื่อเปิดใช้งานลำดับ \"ตามวัตถุ\"" @@ -12267,6 +12364,9 @@ msgstr "คุณอาจต้องการลดขนาดแบบจำ msgid "Variable layer height is not supported with Organic supports." msgstr "ไม่รองรับความสูงของเลเยอร์ที่แปรผันได้ด้วยการรองรับแบบออร์แกนิก" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "เส้นผ่านศูนย์กลางของหัวฉีดที่แตกต่างกันและเส้นผ่านศูนย์กลางของเส้นพลาสติกที่แตกต่างกันอาจทำงานได้ไม่ดีนักเมื่อเปิดใช้งาน Prime Tower ยังเป็นการทดลองอยู่มาก ดังนั้นโปรดดำเนินการด้วยความระมัดระวัง" @@ -14286,6 +14386,48 @@ msgstr "วัสดุส่วนรองรับ" msgid "Support material is commonly used to print supports and support interfaces." msgstr "วัสดุรองรับมักใช้ในการพิมพ์ส่วนรองรับและอินเทอร์เฟซรองรับ" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "พิมพ์เส้นพลาสติกได้" @@ -17126,6 +17268,12 @@ msgstr "" "\n" "การตั้งค่าในจำนวนการถอนก่อนการล้างการตั้งค่าด้านล่างจะทำการถอนส่วนที่เกินก่อนการล้าง มิฉะนั้นจะดำเนินการหลังจากนั้น" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Wipe Tower สามารถใช้เพื่อทำความสะอาดสิ่งตกค้างบนหัวฉีด และทำให้แรงดันในห้องภายในหัวฉีดคงที่ เพื่อหลีกเลี่ยงข้อบกพร่องในลักษณะที่ปรากฏเมื่อพิมพ์วัตถุ" @@ -18209,6 +18357,9 @@ msgstr "การประสานไฟล์โมเดลล้มเหล msgid "The supplied file couldn't be read because it's empty." msgstr "ไม่สามารถอ่านไฟล์ที่ให้มาได้เนื่องจากไฟล์ว่างเปล่า" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "รูปแบบไฟล์ที่ไม่รู้จัก ไฟล์อินพุตต้องมีนามสกุล .stl, .obj, .amf(.xml)" diff --git a/localization/i18n/tr/OrcaSlicer_tr.po b/localization/i18n/tr/OrcaSlicer_tr.po index c0126ec8fd..c3812dd93f 100644 --- a/localization/i18n/tr/OrcaSlicer_tr.po +++ b/localization/i18n/tr/OrcaSlicer_tr.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-08-21 23:18+0300\n" "Last-Translator: GlauTech\n" "Language-Team: \n" @@ -2969,6 +2969,9 @@ msgstr "Düzenle" msgid "Merge with" msgstr "Şununla birleştir" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Bu filamenti sil" @@ -3269,6 +3272,9 @@ msgstr "Birleştir" msgid "Merge parts to an object" msgstr "Parçaları bir nesnede birleştir" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Katman ekle" @@ -7903,6 +7909,9 @@ msgstr "Mevcut plakayı özelleştir" msgid "The %s nozzle can not print %s." msgstr "%s püskürtme ucu %s'yi yazdıramıyor." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Yazdırmada %1% ile %2%'nin karıştırılması önerilmez.\n" @@ -8028,12 +8037,36 @@ msgstr "Filament listesini AMS'den senkronize edin" msgid "Set filaments to use" msgstr "Kullanılacak filamentleri ayarla" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Arama plakası, nesne ve parça." msgid "Pellets" msgstr "Peletler" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "İşleminizi tamamladıktan sonra %s projesi kapatılacak ve yeni bir proje oluşturulacak." @@ -8226,6 +8259,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Çok parçalı nesne algılandı" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Bu dosyalar birden fazla parçadan oluşan tek bir nesne olarak mı yüklensin?\n" @@ -8437,6 +8476,18 @@ msgstr "" msgid "Sync now" msgstr "Şimdi senkronize et" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Değiştirilen ön ayarları yeni projede tutabilir veya silebilirsiniz" @@ -9735,6 +9786,9 @@ msgstr "Spiral vazo" msgid "First layer filament sequence" msgstr "İlk katman filament dizisi" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Katmana göre" @@ -9780,18 +9834,58 @@ msgstr "Web sayfasına atla" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12439,6 +12533,9 @@ msgstr "Yine de yazdırmak istiyorsanız Tercihler / Denetim / Dilimleme / Karı msgid "No extrusions under current settings." msgstr "Mevcut ayarlarda ekstrüzyon yok." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "\"Nesneye göre\" dizisi etkinleştirildiğinde, hızlandırılmış çekimin yumuşak modu desteklenmez." @@ -12475,6 +12572,9 @@ msgstr "Modelinizin boyutunu küçültmek veya mevcut yazdırma ayarlarını de msgid "Variable layer height is not supported with Organic supports." msgstr "Değişken katman yüksekliği Organik desteklerle desteklenmez." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Farklı püskürtme ucu çapları ve farklı filament çapları, ana kule etkinleştirildiğinde iyi çalışmayabilir. Oldukça deneysel olduğundan lütfen dikkatli ilerleyin." @@ -14540,6 +14640,48 @@ msgstr "Destek malzemesi" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Destek malzemesi yaygın olarak destek ve destek arayüzünü yazdırmak için kullanılır." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Filament yazdırılabilir" @@ -17455,6 +17597,12 @@ msgstr "" "\n" "Aşağıdaki silme ayarından önce geri çekme miktarına bir değer ayarlamak, silme işleminden önce aşırı geri çekme işlemini gerçekleştirecektir, aksi takdirde silme işleminden sonra gerçekleştirilecektir." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Temizleme kulesi, nesneleri yazdırırken görünüm kusurlarını önlemek amacıyla nozul üzerindeki kalıntıları temizlemek ve nozul içindeki oda basıncını dengelemek için kullanılabilir." @@ -18543,6 +18691,9 @@ msgstr "Bir model dosyasının meshlenmesi başarısız oldu veya geçerli bir msgid "The supplied file couldn't be read because it's empty." msgstr "Seçilen dosya boş olduğundan okunamıyor." +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Bilinmeyen dosya formatı. Giriş dosyası .stl, .obj, .amf(.xml) uzantılı olmalıdır." diff --git a/localization/i18n/uk/OrcaSlicer_uk.po b/localization/i18n/uk/OrcaSlicer_uk.po index 2a8fb00b3e..e8dd6dde4b 100644 --- a/localization/i18n/uk/OrcaSlicer_uk.po +++ b/localization/i18n/uk/OrcaSlicer_uk.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: orcaslicerua\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-07-17 16:25+0300\n" "Last-Translator: Andrij Mizyk \n" "Language-Team: Ukrainian\n" @@ -2906,6 +2906,9 @@ msgstr "Змінити" msgid "Merge with" msgstr "Обʼєднати з" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "Видалити цей філамент" @@ -3197,6 +3200,9 @@ msgstr "Збірка" msgid "Merge parts to an object" msgstr "Обʼєднати частини в обʼєкт" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "Додати шари" @@ -7897,6 +7903,9 @@ msgstr "Пристосувати поточну пластину" msgid "The %s nozzle can not print %s." msgstr "Сопло %s не може друкувати %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "Змішування %1% з %2% в друці не рекомендується.\n" @@ -8025,12 +8034,36 @@ msgstr "Синхронізувати список ниток з AMS" msgid "Set filaments to use" msgstr "Встановити філаменти для використання" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Пошук пластини, об’єкта і деталі." msgid "Pellets" msgstr "Гранули" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8235,6 +8268,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Виявлено обʼєкт, що складається з кількох частин" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Завантажити ці файли як єдиний обʼєкт з кількома частинами?\n" @@ -8445,6 +8484,18 @@ msgstr "" msgid "Sync now" msgstr "Синхронізувати зараз" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Ви можете зберегти змінені пресети у новому проекті або відмовитися від них" @@ -9737,6 +9788,9 @@ msgstr "Спіральна ваза" msgid "First layer filament sequence" msgstr "Послідовність філаменту першого шару" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "По шарах" @@ -9783,18 +9837,58 @@ msgstr "Перейти на вебсторінку" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12500,6 +12594,9 @@ msgstr "Якщо ви все одно хочете друкувати, може msgid "No extrusions under current settings." msgstr "Немає екструзій під час поточних налаштувань." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Плавний режим таймлапсу не підтримується, коли послідовність \"по обʼєкт\" увімкнено." @@ -12540,6 +12637,9 @@ msgstr "Можливо, ви захочете зменшити розмір мо msgid "Variable layer height is not supported with Organic supports." msgstr "Змінна висота шару не підтримується з Органічними підтримками." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Різні діаметри сопел та різні діаметри філаменту можуть працювати некоректно коли ввімкнена підготовча вежа. Це експериментальна функція, тому використовуйте її з обережністю." @@ -14665,6 +14765,48 @@ msgstr "Матеріал підтримки" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Допоміжний матеріал зазвичай використовується для друку підтримки" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "Філамент придатний для друку" @@ -17660,6 +17802,12 @@ msgstr "" "\n" "Якщо встановити значення у параметрі \"Кількість втягування перед витиранням\" нижче, надлишкове втягування буде виконано перед витиранням, інакше воно буде виконано після нього." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + # AI Translated msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Вежа протирання може використовуватися для очищення залишків на соплі та стабілізації тиску в камері всередині сопла, щоб уникнути дефектів зовнішнього вигляду під час друку обʼєктів." @@ -18766,6 +18914,9 @@ msgstr "Не вдалося побудувати сітку файлу моде msgid "The supplied file couldn't be read because it's empty." msgstr "Наданий файл не вдалося прочитати, оскільки він порожній" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Невідомий формат файлу: вхідний файл повинен мати розширення .stl, .obj або .amf (.xml)." diff --git a/localization/i18n/vi/OrcaSlicer_vi.po b/localization/i18n/vi/OrcaSlicer_vi.po index 519a6c3b25..b5e5a5dc5f 100644 --- a/localization/i18n/vi/OrcaSlicer_vi.po +++ b/localization/i18n/vi/OrcaSlicer_vi.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2025-10-02 17:43+0700\n" "Last-Translator: \n" "Language-Team: hainguyen.ts13@gmail.com\n" @@ -3077,6 +3077,9 @@ msgstr "Chỉnh sửa" msgid "Merge with" msgstr "Gộp với" +msgid "Decompose Color" +msgstr "" + # AI Translated msgid "Delete this filament" msgstr "Xóa filament này" @@ -3389,6 +3392,9 @@ msgstr "Lắp ráp" msgid "Merge parts to an object" msgstr "Gộp các phần thành một vật thể" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "Thêm lớp" @@ -8294,6 +8300,9 @@ msgstr "Tùy chỉnh bản hiện tại" msgid "The %s nozzle can not print %s." msgstr "Đầu phun %s không thể in %s." +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + # AI Translated #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" @@ -8436,12 +8445,36 @@ msgstr "Đồng bộ danh sách filament từ AMS" msgid "Set filaments to use" msgstr "Đặt filament để sử dụng" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "Tìm kiếm bản, đối tượng và phần." msgid "Pellets" msgstr "Viên" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + # AI Translated #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." @@ -8640,6 +8673,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "Phát hiện đối tượng nhiều phần" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "Tải các file này như một đối tượng đơn với nhiều phần?\n" @@ -8869,6 +8908,18 @@ msgstr "" msgid "Sync now" msgstr "Đồng bộ ngay" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "Bạn có thể giữ các preset đã chỉnh sửa cho dự án mới hoặc loại bỏ chúng" @@ -10250,6 +10301,9 @@ msgstr "Bình xoắn ốc" msgid "First layer filament sequence" msgstr "Trình tự filament lớp đầu tiên" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "Theo lớp" @@ -10296,18 +10350,58 @@ msgstr "Chuyển đến trang web" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -13075,6 +13169,9 @@ msgstr "Nếu bạn vẫn muốn in, bạn có thể bật tùy chọn trong Tù msgid "No extrusions under current settings." msgstr "Không có đùn dưới cài đặt hiện tại." +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "Chế độ mượt của timelapse không được hỗ trợ khi trình tự \"theo đối tượng\" được bật." @@ -13113,6 +13210,9 @@ msgstr "Bạn có thể muốn giảm kích thước model của mình hoặc th msgid "Variable layer height is not supported with Organic supports." msgstr "Chiều cao lớp thay đổi không được hỗ trợ với support hữu cơ." +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "Đường kính đầu phun khác nhau và đường kính filament khác nhau có thể không hoạt động tốt khi prime tower được bật. Nó rất thử nghiệm, vì vậy vui lòng tiến hành thận trọng." @@ -15234,6 +15334,48 @@ msgstr "Vật liệu support" msgid "Support material is commonly used to print supports and support interfaces." msgstr "Vật liệu support thường được sử dụng để in support và giao diện support." +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + # AI Translated msgid "Filament printable" msgstr "Filament in được" @@ -18178,6 +18320,12 @@ msgstr "" "\n" "Đặt giá trị trong cài đặt lượng rút trước khi lau bên dưới sẽ thực hiện bất kỳ rút dư nào trước khi lau, nếu không nó sẽ được thực hiện sau." +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "Wipe tower có thể được sử dụng để làm sạch cặn trên đầu phun và ổn định áp suất buồng bên trong đầu phun, để tránh khuyết điểm bề ngoài khi in đối tượng." @@ -19298,6 +19446,9 @@ msgstr "Tạo lưới cho file mô hình thất bại hoặc không có hình d msgid "The supplied file couldn't be read because it's empty." msgstr "File được cung cấp không thể đọc được vì nó trống" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "Định dạng file không xác định. File đầu vào phải có phần mở rộng .stl, .obj, .amf(.xml)." diff --git a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po index 2abe2fc5d2..4787976785 100644 --- a/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po +++ b/localization/i18n/zh_CN/OrcaSlicer_zh_CN.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Slic3rPE\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2026-06-11 12:37-0300\n" "Last-Translator: Handle \n" "Language-Team: \n" @@ -2843,6 +2843,9 @@ msgstr "编辑" msgid "Merge with" msgstr "与其合并" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "移除此耗材" @@ -3124,6 +3127,9 @@ msgstr "组合体" msgid "Merge parts to an object" msgstr "合并零件到对象" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + msgid "Add layers" msgstr "添加层" @@ -7644,6 +7650,9 @@ msgstr "自定义当前盘" msgid "The %s nozzle can not print %s." msgstr "%s 喷嘴无法打印 %s。" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "不建议在打印时将 %1% 与 %2% 混合。\n" @@ -7769,12 +7778,36 @@ msgstr "从AMS同步材料列表" msgid "Set filaments to use" msgstr "配置可选择的材料" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "搜索盘、模型和零件。" msgid "Pellets" msgstr "颗粒" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "完成操作后,%s 项目将关闭并创建一个新项目。" @@ -7961,6 +7994,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "检测到多部分对象" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "将这些文件加载为一个多零件对象?\n" @@ -8166,6 +8205,18 @@ msgstr "" msgid "Sync now" msgstr "立即同步" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "您可以保留修改的预设到新项目中或者忽略这些修改" @@ -9412,6 +9463,9 @@ msgstr "旋转花瓶" msgid "First layer filament sequence" msgstr "首层耗材打印顺序" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "逐层" @@ -9458,18 +9512,58 @@ msgstr "跳转到网页" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12029,6 +12123,9 @@ msgstr "如果您仍要打印,可以在 偏好设置 / 控制 / 切片 / 移 msgid "No extrusions under current settings." msgstr "根据当前设置,不会生成任何打印。" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "平滑模式的延时摄影不支持在逐件打印模式下使用。" @@ -12065,6 +12162,9 @@ msgstr "或许您想要缩小模型的尺寸,或者更改当前打印设置, msgid "Variable layer height is not supported with Organic supports." msgstr "Organic支撑不支持可变层高。" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "当启用擦拭塔时,不同的喷嘴直径和不同的耗材丝直径可能无法很好地工作。这是非常实验性的,所以请谨慎操作。" @@ -14051,6 +14151,48 @@ msgstr "支撑材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撑材料通常用于打印支撑体和支撑接触面" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "可打印耗材" @@ -16885,6 +17027,12 @@ msgstr "" "\n" "在下方的擦拭前回抽量设置中输入一个数值,将在擦拭动作之前执行任何超出部分的回抽,否则超出部分的回抽将在擦拭之后执行。" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "擦拭塔可以用来清理喷嘴上的残留料和让喷嘴内部的腔压达到稳定状态,以避免打印物体时出现外观瑕疵。" @@ -17960,6 +18108,9 @@ msgstr "模型文件的网格划分失败,或缺少有效的形状。" msgid "The supplied file couldn't be read because it's empty." msgstr "无法读取提供的文件,因为该文件内容为空。" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "未知的文件格式。输入文件的扩展名必须为 .stl、.obj 或 .amf(.xml)。" diff --git a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po index 839e7cdb2b..f9287d0886 100644 --- a/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po +++ b/localization/i18n/zh_TW/OrcaSlicer_zh_TW.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-08-21 14:25+0800\n" +"POT-Creation-Date: 2026-08-28 12:15+0800\n" "PO-Revision-Date: 2025-11-28 13:48-0600\n" "Last-Translator: tntchn <15895303+tntchn@users.noreply.github.com>\n" "Language-Team: \n" @@ -2913,6 +2913,9 @@ msgstr "編輯" msgid "Merge with" msgstr "合併到" +msgid "Decompose Color" +msgstr "" + msgid "Delete this filament" msgstr "刪除此線材" @@ -3209,6 +3212,9 @@ msgstr "組合體" msgid "Merge parts to an object" msgstr "合併零件為物件" +msgid "Using variable layer height together with mixed color sublayer may result in poor color mixing quality." +msgstr "" + # AI Translated msgid "Add layers" msgstr "新增層" @@ -7805,6 +7811,9 @@ msgstr "自訂列印板參數" msgid "The %s nozzle can not print %s." msgstr "%s 噴嘴無法列印 %s。" +msgid "Printing mixed-color filament on a single-extruder printer requires frequent filament changes and flushing, which may significantly increase waste and the risk of nozzle / waste-chute clogging." +msgstr "" + #, boost-format msgid "Mixing %1% with %2% in printing is not recommended.\n" msgstr "不建議在列印時混用 %1% 和 %2%。\n" @@ -7930,12 +7939,36 @@ msgstr "從 AMS 同步線材清單" msgid "Set filaments to use" msgstr "設定可選擇的線材" +msgid "Add Mixed Filament" +msgstr "" + +msgid "Mixed Filament" +msgstr "" + +msgid "Remove last mixed filament" +msgstr "" + +msgid "Add mixed filament" +msgstr "" + +msgid "Mixed filament has invalid or mismatched components. Please re-edit affected entries." +msgstr "" + msgid "Search plate, object and part." msgstr "搜尋列印板、物件和零件。" msgid "Pellets" msgstr "顆粒" +msgid "Mixed filament has broken component references" +msgstr "" + +msgid "Edit / Delete / Merge" +msgstr "" + +msgid "The target mixed filament uses this physical filament as a component. Merging will remove this physical filament and may invalidate the mixed filament. Continue?" +msgstr "" + #, c-format, boost-format msgid "After completing your operation, %s project will be closed and create a new project." msgstr "完成操作後,%s 專案將關閉並建立新專案。" @@ -8122,6 +8155,12 @@ msgstr "" msgid "Multi-part object detected" msgstr "偵測到多部分物件" +msgid "Matching textures to filaments" +msgstr "" + +msgid "Texture Import Warning" +msgstr "" + msgid "Load these files as a single object with multiple parts?\n" msgstr "將這些檔案載入為一個多零件物件?\n" @@ -8332,6 +8371,18 @@ msgstr "" msgid "Sync now" msgstr "立即同步" +msgid "Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only." +msgstr "" + +msgid "Applying texture colors..." +msgstr "" + +msgid "Updating 3D view..." +msgstr "" + +msgid "Texture colors applied." +msgstr "" + msgid "You can keep the modified presets for the new project or discard them" msgstr "您可以將修改後的預設檔保留到新專案中或者忽略這些修改" @@ -9586,6 +9637,9 @@ msgstr "螺旋花瓶模式" msgid "First layer filament sequence" msgstr "首層線材列印順序" +msgid "The filament list contains mixed filaments. Custom filament sequence will not take effect." +msgstr "" + msgid "By Layer" msgstr "逐層" @@ -9631,18 +9685,58 @@ msgstr "跳至網頁" msgid "Material" msgstr "" +msgid "Mixed filament" +msgstr "" + +msgid "Some mixed filaments rely on filaments that will not be published:" +msgstr "" + +#, c-format, boost-format +msgid "Filament %d (mixed)" +msgstr "" + +msgid "needs" +msgstr "" + +msgid "not enabled" +msgstr "" + +msgid "material not published" +msgstr "" + +msgid "To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement." +msgstr "" + +msgid "Publish anyway" +msgstr "" + msgid "Publish 3MF..." msgstr "" msgid "Select which settings to embed in the 3MF file" msgstr "" +msgid "Mixed filament - published as a whole when \"Enable\" above is selected" +msgstr "" + +msgid "Publish this mixed filament and enable + Full Publish its component filaments" +msgstr "" + +msgid "Publish this filament slot in the 3MF file" +msgstr "" + msgid "Full Publish" msgstr "" msgid "Embed the entire filament of this slot in the 3MF file" msgstr "" +msgid "Material Ratio" +msgstr "" + +msgid "Model Height" +msgstr "" + msgid "Filter non-selected" msgstr "" @@ -12233,6 +12327,9 @@ msgstr "如果您仍想列印,可以在「偏好設定 / 控制 / 切片 / 移 msgid "No extrusions under current settings." msgstr "根據目前設定,不會進行任何列印。" +msgid "A gradient mixed filament is used, but 'Mixed color sublayer' is disabled. The gradient will not be printed." +msgstr "" + msgid "Smooth mode of timelapse is not supported when \"by object\" sequence is enabled." msgstr "逐件列印模式下不支援使用平滑模式的縮時錄影。" @@ -12269,6 +12366,9 @@ msgstr "您可能想要減小模型的尺寸或更改目前的列印設定並重 msgid "Variable layer height is not supported with Organic supports." msgstr "有機樹支撐不支持可變層高。" +msgid "The wipe tower filament cannot be a mixed filament." +msgstr "" + msgid "Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution." msgstr "當啟用換料時,不同的噴嘴直徑和線材直徑可能無法正常配合。此功能屬於實驗性階段,請小心使用。" @@ -14251,6 +14351,48 @@ msgstr "支撐材料" msgid "Support material is commonly used to print supports and support interfaces." msgstr "支撐材料通常用於列印支撐體和支撐接觸面" +msgid "Is mixed filament" +msgstr "" + +msgid "Whether this filament slot is a mixed filament composed of multiple physical filaments" +msgstr "" + +msgid "Mixed filament components" +msgstr "" + +msgid "Comma-separated 1-based indices of component filaments, e.g. \"1,3\"" +msgstr "" + +msgid "Mixed filament sublayer ratios" +msgstr "" + +msgid "Comma-separated ratio values summing to 1.0, e.g. \"0.7,0.3\"" +msgstr "" + +msgid "Mixed filament gradient" +msgstr "" + +msgid "Enable Z-direction gradient mode for mixed filament sub-layers. When enabled, the sub-layer ratios vary linearly across layers." +msgstr "" + +msgid "Mixed filament gradient range" +msgstr "" + +msgid "Start and end ratios for the first component in gradient mode. Comma-separated pair, e.g. \"0.10,0.90\" means 10% to 90%." +msgstr "" + +msgid "Mixed filament gradient curve" +msgstr "" + +msgid "Optional Photoshop-style custom curve mapping Z progress to the first component ratio. Encoded as pipe-separated control points, either \"x,y\" (legacy) or \"x,y,m_in,m_out\" when a tangent override is needed (empty token or \"nan\" means use PCHIP default). x in [0,1]; y is clamped to the configured ratio range, e.g. \"0,0.15|0.5,0.50|1,0.85\". When empty, the linear gradient_range is used instead." +msgstr "" + +msgid "Mixed filament per-part gradient" +msgstr "" + +msgid "When gradient mode is enabled, apply the gradient to each part of an assembly independently rather than treating the whole assembly as one Z range." +msgstr "" + msgid "Filament printable" msgstr "線材可列印" @@ -17079,6 +17221,12 @@ msgstr "" "\n" "在以下的『擦拭前的回抽量』設定中設定一個值,將在擦拭之前執行任何額外的回抽操作;否則,將在擦拭之後執行。" +msgid "Mixed color sublayer" +msgstr "" + +msgid "Enable mixed color sublayer splitting. When enabled, layers containing mixed color filaments will be split into sub-layers to achieve color mixing effects." +msgstr "" + msgid "The wiping tower can be used to clean up residue on the nozzle and stabilize the chamber pressure inside the nozzle in order to avoid appearance defects when printing objects." msgstr "換料塔的功能是用於清除噴嘴上的殘留物,同時穩定噴嘴內的壓力,從而避免列印物件時出現外觀瑕疵。" @@ -18148,6 +18296,9 @@ msgstr "模型檔案網格化失敗或沒有有效形狀" msgid "The supplied file couldn't be read because it's empty." msgstr "無法讀取提供的檔案,因為該檔案為空" +msgid "The file format is incompatible and cannot be parsed." +msgstr "" + msgid "Unknown file format: input file must have .stl, .obj, or .amf(.xml) extension." msgstr "檔案格式未知。輸入的檔案必須是 .stl、.obj 或 .amf(.xml) 格式。" diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index be28ae7499..1a8a860301 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -158,6 +158,43 @@ wxColour mixed_filament_blend_color(const DynamicPrintConfig& full, size_t slot) return slot < colors.size() ? colors[slot] : wxColour("#D9D9D9"); } +// The slot's own chip as the main GUI renders it: a curve-sampled gradient swatch for a +// gradient mixed slot, the static blended colour otherwise. Returns wxNullBitmap when nothing +// could be rendered (the caller falls back to a plain label). +wxBitmap mixed_filament_chip_bitmap(const DynamicPrintConfig& full, size_t slot, int swatch_sz) +{ + const std::string label = std::to_string(slot + 1); + const auto* grad_opt = full.opt("filament_mixed_gradient"); + const bool is_gradient = grad_opt != nullptr && slot < grad_opt->size() && grad_opt->values[slot]; + wxBitmap* icon = nullptr; + if (is_gradient) { + // The same curve-sampled ramp the sidebar chips use; an empty ramp (broken definition) + // degrades to a plain fade between the slot's two component colours. + const std::vector ramp = mixed_gradient_ramp(full, slot, swatch_sz); + if (!ramp.empty()) { + icon = get_extruder_color_icon(std::vector(), true, label, swatch_sz, swatch_sz, &ramp); + } else { + std::vector hexes; + for (const unsigned int comp : mixed_slot_components(full, slot)) { + std::string hex = filament_color_hex(full, size_t(comp) - 1); + if (hex.empty()) + hex = "#D9D9D9"; + hexes.push_back(std::move(hex)); + } + if (hexes.size() >= 2) + icon = get_extruder_color_icon(std::move(hexes), true, label, swatch_sz, swatch_sz); + } + } + if (icon == nullptr) { + const wxColour blend = mixed_filament_blend_color(full, slot); + const std::string blend_hex = blend.IsOk() ? + std::string(wxString::Format("#%02X%02X%02X", blend.Red(), blend.Green(), blend.Blue()).ToUTF8()) : + std::string("#808080"); + icon = get_extruder_color_icon(blend_hex, label, swatch_sz, swatch_sz); + } + return icon != nullptr ? *icon : wxNullBitmap; +} + // Tab-strip bitmap for a mixed slot: the mix's own chip, then its component swatches each // followed by their percent share (or a "->" arrow for gradients), mirroring the main GUI's // sidebar rows - e.g. "[3 purple]: [1 red] 50% + [2 blue] 50%". The whole composition is one @@ -207,13 +244,10 @@ wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, auto push_text = [&](const wxString& text) { pieces.push_back({Piece::Text, wxNullBitmap, text}); }; { - const wxColour blend = mixed_filament_blend_color(full, slot); - const std::string blend_hex = blend.IsOk() ? - std::string(wxString::Format("#%02X%02X%02X", blend.Red(), blend.Green(), blend.Blue()).ToUTF8()) : - std::string("#808080"); - const size_t before = pieces.size(); - push_swatch(blend_hex, std::to_string(slot + 1)); - has_lead = pieces.size() > before; + const wxBitmap chip = mixed_filament_chip_bitmap(full, slot, swatch_sz); + has_lead = chip.IsOk(); + if (has_lead) + pieces.push_back({Piece::Swatch, chip, wxString()}); } for (size_t ci = 0; ci < comps.size(); ++ci) { if (pieces.empty()) @@ -279,6 +313,83 @@ wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, } // namespace +// Warning shown on OK when an enabled mixed-filament slot relies on a filament that would ship +// without its material. One row per unmet dependency: the mixed slot's colour chip, the +// component filament's colour chip, and the reason. "Cancel" is the safe choice and keeps the +// dialog open; "Publish anyway" continues. +class MixedFilamentWarningDialog : public MsgDialog +{ +public: + MixedFilamentWarningDialog(wxWindow* parent, const DynamicPrintConfig& full, const std::vector& issues) + : MsgDialog(parent, _L("Warning"), wxEmptyString, wxOK | wxCANCEL | wxICON_WARNING) + { + auto* content = new wxBoxSizer(wxVERTICAL); + + auto* intro = new wxStaticText(this, wxID_ANY, _L("Some mixed filaments rely on filaments that will not be published:")); + intro->SetFont(Label::Body_13); + intro->Wrap(FromDIP(400)); + content->Add(intro, 0, wxEXPAND); + content->AddSpacer(FromDIP(10)); + + const int swatch = FromDIP(20); + for (const MixedDependencyIssue& issue : issues) { + auto* row = new wxBoxSizer(wxHORIZONTAL); + + // The mixed slot as just its own chip (gradient-aware, numbered like its tab); + // falls back to a plain label when the chip cannot be built. + const wxString mix_label = wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1); + const wxBitmap mix_bmp = mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch); + if (mix_bmp.IsOk()) { + auto* bmp = new wxStaticBitmap(this, wxID_ANY, mix_bmp); + bmp->SetToolTip(mix_label); + row->Add(bmp, 0, wxALIGN_CENTER_VERTICAL); + } else { + auto* label = new wxStaticText(this, wxID_ANY, mix_label); + label->SetFont(Label::Body_12); + row->Add(label, 0, wxALIGN_CENTER_VERTICAL); + } + + auto* needs = new wxStaticText(this, wxID_ANY, _L("needs")); + needs->SetFont(Label::Body_12); + needs->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); + row->Add(needs, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6)); + + // The component filament's colour chip, numbered like the tab strips; the slot + // name stays on hover to keep the row itself short. + std::string hex = filament_color_hex(full, issue.component_slot); + if (hex.empty()) + hex = "#D9D9D9"; + if (wxBitmap* chip = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch)) { + auto* comp_bmp = new wxStaticBitmap(this, wxID_ANY, *chip); + comp_bmp->SetToolTip(wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1)); + row->Add(comp_bmp, 0, wxALIGN_CENTER_VERTICAL); + } + + auto* reason = new wxStaticText(this, wxID_ANY, + issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") : _L("material not published")); + reason->SetFont(Label::Body_12); + reason->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#989898"))); + row->Add(reason, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); + + content->Add(row, 0, wxLEFT, FromDIP(10)); + content->AddSpacer(FromDIP(6)); + } + + auto* hint = new wxStaticText(this, wxID_ANY, + _L("To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement.")); + hint->SetFont(Label::Body_12); + hint->Wrap(FromDIP(380)); + content->Add(hint, 0, wxEXPAND | wxTOP, FromDIP(4)); + + content_sizer->Add(content, 0, wxEXPAND); + + SetButtonLabel(wxID_OK, _L("Publish anyway")); + SetButtonLabel(wxID_CANCEL, _L("Cancel"), true); // safe choice gets the focus + + finalize(); + } +}; + PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_spec(const DynamicPrintConfig& full, size_t slot) { MixedVisualSpec spec; @@ -417,20 +528,11 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { // Publish is always allowed: no settings selected means no settings override. Warn only - // when an enabled mixed filament would ship without the identity of one of its - // components; "Proceed" accepts that and publishes anyway. - if (const std::vector missing = unpublished_mixed_components(); !missing.empty()) { - wxString missing_list; - for (size_t i = 0; i < missing.size(); ++i) { - if (i > 0) - missing_list += ", "; - missing_list += wxString::Format(_L("Filament %d"), int(missing[i] + 1)); - } - const wxString msg = _L("The following filaments are used by published mixed filaments but will not carry their material identity:") - + wxString(" ") + missing_list; - MessageDialog warn(this, msg, _L("Warning"), wxICON_WARNING); - warn.AddButton(wxID_CANCEL, _L("Cancel"), true); // safe choice gets the focus - warn.AddButton(wxID_OK, _L("Proceed"), false); + // when an enabled mixed filament would ship without the material of one of its + // components; "Publish anyway" accepts that and publishes anyway. + if (const std::vector issues = unpublished_mixed_components(); !issues.empty()) { + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + MixedFilamentWarningDialog warn(this, full, issues); if (warn.ShowModal() != wxID_OK) return; // Cancel: dismiss the warning and stay in this dialog } @@ -1712,9 +1814,9 @@ std::vector PublishSettingsDialog::GetPublishedKeys() const return out; } -std::vector PublishSettingsDialog::unpublished_mixed_components() const +std::vector PublishSettingsDialog::unpublished_mixed_components() const { - std::set missing; + std::vector out; for (const Category& cat : m_categories) { if (!cat.is_mixed || cat.section != Section::Material) continue; @@ -1739,7 +1841,7 @@ std::vector PublishSettingsDialog::unpublished_mixed_components() const // "Full Publish" nor the "Type" requirement row checked. Colour never counts: // the receiver renders the mix from its own components' colours. if (!comp_cat->enable_check->GetValue()) { - missing.insert(component_slot); + out.push_back({cat.filament_slot, component_slot, MixedDependencyIssue::Reason::Disabled}); continue; } if (comp_cat->full_check != nullptr && comp_cat->full_check->GetValue()) @@ -1753,10 +1855,15 @@ std::vector PublishSettingsDialog::unpublished_mixed_components() const } } if (!type_checked) - missing.insert(component_slot); + out.push_back({cat.filament_slot, component_slot, MixedDependencyIssue::Reason::MaterialNotPublished}); } } - return std::vector(missing.begin(), missing.end()); + // Deterministic order for the warning rows: by mixed slot, then component slot. The same + // (mix, component) pair cannot repeat: each mix's components come from a config list. + std::sort(out.begin(), out.end(), [](const MixedDependencyIssue& a, const MixedDependencyIssue& b) { + return std::tie(a.mixed_slot, a.component_slot) < std::tie(b.mixed_slot, b.component_slot); + }); + return out; } std::vector PublishSettingsDialog::GetPublishedMaterialKeys() const diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 60c33e5a2d..8d59192309 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -28,6 +28,17 @@ struct PublishMaterialIdentity std::string id; }; +// One unmet dependency of an enabled mixed-filament slot: the component filament the mix uses +// would ship without its material (either the component slot is not enabled at all, or it is +// enabled with neither "Full Publish" nor the "Type" requirement checked). Slots are 0-based. +struct MixedDependencyIssue +{ + enum class Reason { Disabled, MaterialNotPublished }; + size_t mixed_slot{0}; + size_t component_slot{0}; + Reason reason{Reason::Disabled}; +}; + // Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout // mirroring the Process settings (Printer / Filament / Process outer tabs, category or material // tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become @@ -187,11 +198,11 @@ private: // "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); - // 0-based material slots required by enabled mixed-filament slots that would ship without - // their identity: "Enable" not checked, or enabled with neither "Full Publish" nor the + // Unmet dependencies of enabled mixed-filament slots, one record per (mix, component) pair: + // "Enable" not checked on the component, or enabled with neither "Full Publish" nor the // "Type" requirement row checked. Colour is deliberately ignored (the receiver renders the - // mix from its own components' colours). Sorted, deduplicated. - std::vector unpublished_mixed_components() const; + // mix from its own components' colours). Sorted by mixed slot, then component slot. + std::vector unpublished_mixed_components() const; // 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. From a62db72e023c6c85adadd2d175637087f8d9ad81 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 28 Aug 2026 15:24:03 +0800 Subject: [PATCH 32/60] Publish 3MF: import-side hardening and test coverage Validate mixed-filament definitions during the published material pass: definitions whose components reference slots that do not exist or hold other mixed filaments, or that carry fewer than two components, are reported through the shared skipped_keys channel instead of shipping a mix the GUI integrity check would only flag later. Fix the slot-limit exhaustion report being silently dropped: it wrote to published_config->skipped_keys, which the pass's final move-assignment from the local vector clobbers. All rejections now go through the local. Remove the unreachable persist branch from add_detached_preset: no caller passes save_to_project=false, so the parameter is gone and the copy is always project-embedded. Tests: cover the exhaustion path, the new definition validation, the identity-tier matching matrix (including substitute reporting), the structural-key denylist, whole-vector size-mismatch skips, relocation payload degradation, the "(Published 2)" uniquify chain, mixed blend colours staying out of shared preset configs, and duplicate-slot last-wins. Also fix the legacy-3mf scenario passing vacuously behind an if-guarded assertion. All existing published/3mf tests pass unchanged. --- src/libslic3r/Preset.cpp | 20 +- src/libslic3r/Preset.hpp | 12 +- src/libslic3r/PresetBundle.cpp | 53 +- tests/libslic3r/test_3mf.cpp | 7 +- .../libslic3r/test_preset_bundle_loading.cpp | 517 ++++++++++++++++++ 5 files changed, 582 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 908bdec2b3..6646d98e71 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3064,14 +3064,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det // diff against a parent; the caller decides whether to select it. // The published entry's filament_id is forwarded so user bases keep their stable // material grouping (get_filament_presets() groups user bases by filament_id). -// save_to_project=true (the Full Publish default) creates a project-embedded preset: -// it lives inside the loaded project only (serialized into the saved .3mf, restored by -// load_project_embedded_presets) and never touches the user's library directory; -// Preset::save() early-returns for embedded presets, so persistence is skipped here too. +// The copy is a project-embedded preset: it lives inside the loaded project only +// (serialized into the saved .3mf, restored by load_project_embedded_presets) and +// never touches the user's library directory; Preset::save() early-returns for +// embedded presets, so persistence is skipped here too. // Returns the final (uniquified) name; on collision "" -> " (Published)" -> // " (Published 2)" ... std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config, - const std::string &filament_id, bool save_to_project) + const std::string &filament_id) { if (name_base.empty()) return std::string(); @@ -3111,7 +3111,7 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base, preset.bundle_id.clear(); preset.file = this->path_for_preset(preset); preset.is_visible = true; - preset.is_project_embedded = save_to_project; + preset.is_project_embedded = true; if (m_type == Preset::TYPE_PRINT) preset.config.option("print_settings_id", true)->value = final_name; else if (m_type == Preset::TYPE_FILAMENT) @@ -3120,14 +3120,6 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base, preset.config.option("printer_settings_id", true)->value = final_name; unlock(); - if (!save_to_project) { - // Persist the full resolved config (no parent). Project-embedded presets are - // serialized into the .3mf instead; Preset::save() would early-return anyway. - // find by final_name — m_presets may have reallocated, so don't keep a raw ref. - auto persist_it = this->find_preset_internal(final_name); - if (persist_it != m_presets.end() && persist_it->name == final_name) - persist_it->save(nullptr); - } return final_name; } diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index e904851a1d..85908b95a5 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -639,16 +639,14 @@ public: // preset's stable material grouping (get_filament_presets groups user bases by // filament_id); the published entry's filament_id is forwarded so the copy keeps // the author's grouping. - // With save_to_project=true (default) the copy is a project-embedded preset - // ("Preset Inside Project"): it lives inside the loaded project only, is serialized - // into the saved .3mf via get_current_project_embedded_presets(), and is never - // written to the user's library directory. With false it persists as a normal - // user preset file. + // The copy is a project-embedded preset ("Preset Inside Project"): it lives inside + // the loaded project only, is serialized into the saved .3mf via + // get_current_project_embedded_presets(), and is never written to the user's + // library directory. // Returns the final (uniquified) name; on collision the suffix rule is: // "" -> " (Published)" -> " (Published 2)" ... std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config, - const std::string &filament_id = std::string(), - bool save_to_project = true); + const std::string &filament_id = std::string()); // Delete the current preset, activate the first visible preset. // returns true if the preset was deleted successfully. diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index b6b816b587..3219dec6d8 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5466,8 +5466,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, const std::string material_label = !entry.filament_id.empty() ? entry.filament_id : !entry.publish_type_value.empty() ? entry.publish_type_value : entry.filament_type; - published_config->skipped_keys.emplace_back("material:" + material_label + - " (mixed filament definition: filament slot limit reached)"); + // The local skipped_keys is published wholesale at the end of the pass; + // writing published_config->skipped_keys here would be clobbered by it. + skipped_keys.emplace_back("material:" + material_label + + " (mixed filament definition: filament slot limit reached)"); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot << " could not be placed: all " << next_free_slot << " slots exhausted"; entry_it = published_config->material_keys.erase(entry_it); @@ -5880,6 +5882,19 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // slots wrote to it; that compound case is not chased.) const bool edited_survives_load = this->filament_presets.empty() || this->filament_presets.front() == this->filaments.get_edited_preset().name; + // Final layout for mix-definition validation: every slot that will hold a + // mixed definition once this load completes - the receiver's own virtual + // slots plus each published mixed entry's final (possibly relocated) slot. + // Mix components are 1-based slot numbers, so a component is valid only + // when the slot it names exists and does not itself hold a mixed filament. + std::set mixed_final_slots; + for (const PublishedMaterialEntry& mix_entry : published_config->material_keys) + if (mix_entry.slot >= 0 && is_mixed_definition(mix_entry)) + mixed_final_slots.insert(mix_entry.slot); + for (size_t i = 0; i < this->filament_presets.size(); ++i) + if (this->is_mixed_filament(i)) + mixed_final_slots.insert(int(i)); + const size_t mixed_final_slot_count = this->filament_presets.size(); // Full Publish within-load dedup: identical Full materials (same setting_id // + preset_name identity) share one created instance, so an author who // pointed two slots at one preset yields one standalone copy here. @@ -5900,6 +5915,40 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, entry.publish_type_value) : entry.filament_id; + // Import-side validation of a mixed-filament definition: the publish + // dialog cannot produce a definition whose components reference slots + // that do not exist or hold other mixed filaments, so a broken one here + // means the payload itself is broken (hand-crafted or corrupt file). + // Report it through the same channel as every other rejected input and + // skip the entry, instead of shipping a mix the GUI integrity check + // (check_mixed_filament_integrity) would only flag later. A payload + // that omits the mixed arrays entirely is not an error here: the key + // routing below reports those per key as usual. + if (is_mixed_definition(entry)) { + std::string mix_error; + if (const ConfigOptionStrings* comp_opt = config.opt("filament_mixed_components"); + comp_opt != nullptr && entry.slot < static_cast(comp_opt->values.size())) { + const std::vector comps = parse_mixed_components(comp_opt->values[entry.slot]); + if (comps.size() < 2) + // An empty definition cell counts as broken too: applying it + // would ship a mix the sidebar would only flag later. + mix_error = "needs at least two components"; + else + for (unsigned int comp : comps) + if (comp < 1 || size_t(comp) > mixed_final_slot_count || + mixed_final_slots.count(int(comp) - 1) != 0) { + mix_error = "components reference missing slots"; + break; + } + } + if (!mix_error.empty()) { + skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot + << " rejected: " << mix_error; + continue; + } + } + // Full Publish: always create a standalone detached copy (even on exact // identity match) as a "Preset Inside Project" (project-embedded: lives // in this project only, never written to the library), universally diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 37147b93b8..f595926699 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -688,10 +688,9 @@ SCENARIO("Legacy 3MF without published metadata loads unchanged", "[3mf]") { LoadStrategy::LoadModel | LoadStrategy::LoadConfig); THEN("no published key is fabricated") { REQUIRE(loaded); - if (dst_model.model_info != nullptr) { - REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_TAG) == 0); - REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_KEYS_TAG) == 0); - } + REQUIRE(dst_model.model_info != nullptr); + REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_TAG) == 0); + REQUIRE(dst_model.model_info->metadata_items.count(ORCA_PUBLISHED_KEYS_TAG) == 0); } release_PlateData_list(dst_plates); } diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 931f9e96c3..356645d06c 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -3279,3 +3279,520 @@ TEST_CASE("remap_model_filament_slots repoints extruder configs and color painti CHECK_FALSE(TriangleSelector::has_facets(chained.objects[0]->volumes.front()->mmu_segmentation_facets.get_data(), EnforcerBlockerType(5))); } + +// The slot ceiling (EnforcerBlockerType::ExtruderMax) is what the color-painting encoding can +// address, so a mixed filament that does not fit must be dropped and reported instead of being +// forced onto one of the receiver's physical filaments. +TEST_CASE("Published 3MF drops a mixed filament that does not fit the slot limit and reports it", "[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" }; + // A receiver already at the format's slot ceiling. + bundle.set_num_filaments(unsigned(EnforcerBlockerType::ExtruderMax), "#123456"); + const std::vector receiver_colours = + bundle.project_config.opt("filament_colour")->values; + + PublishedMaterialEntry mix; + mix.filament_type = "PLA"; + mix.filament_vendor = "Generic"; + mix.slot = int(EnforcerBlockerType::ExtruderMax) + 6; // beyond the ceiling + 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 = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // Nothing grew and no slot became a virtual mix. + REQUIRE(bundle.filament_presets.size() == size_t(EnforcerBlockerType::ExtruderMax)); + for (size_t i = 0; i < bundle.filament_presets.size(); ++i) + CHECK_FALSE(bundle.is_mixed_filament(i)); + // The receiver's colours were not touched. + CHECK(bundle.project_config.opt("filament_colour")->values == receiver_colours); + // The mix was reported instead of being applied. + REQUIRE(contains_key(pub.skipped_keys, "material:PLA (mixed filament definition: filament slot limit reached)")); + CHECK(pub.material_replacements.empty()); +} + +// The publish dialog can only produce definitions whose components reference existing physical +// slots, so a payload whose components point at slots that do not exist (or at another mixed +// slot) or that carries fewer than two components is broken. The load reports it through the +// same channel as every other rejected input instead of shipping a mix the GUI integrity check +// would only flag later. +TEST_CASE("Published 3MF rejects a mixed filament definition with impossible components", "[Preset][Bundle][Published]") +{ + // A two-physical-plus-one-mix author file; the definition under test sits on slot 2. + auto make_file_config = [](const std::string &components) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#0000FF", "#800080" }; + config.opt("filament_type")->values = { "PLA", "PETG", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("filament_is_mixed")->values = { 0, 0, 1 }; + config.opt("filament_mixed_components")->values = { "", "", components }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" }; + return config; + }; + + 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" }; + + for (const char *components : { "1,4", "1,3", "1" }) { + // The claimed components: "1,4" names a slot past the final count, "1,3" names the mix + // slot itself (1-based), "1" is not enough components to blend. + INFO("components = " << components); + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt("filament_colour", true)->values = { "#123456" }; + bundle.filament_presets = { "My PLA" }; + + 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(components); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The definition was rejected, not applied: the slot stays a plain (grown) slot. + REQUIRE(bundle.filament_presets.size() == 3); + CHECK_FALSE(bundle.is_mixed_filament(2)); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[2].empty()); + CHECK(bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2].empty()); + // Reported through the shared rejection channel. + if (std::string(components) == "1") + CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)")); + else + CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: components reference missing slots)")); + // The blended colour was not written into the (shared) slot preset either. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == + std::vector{ "#123456" }); + CHECK(pub.material_replacements.empty()); + } +} + +// The relocation shifts cells inside the file's per-slot mixed arrays; a payload too short to +// actually carry the definition degrades to empty cells, which the definition validation then +// reports - an empty mix must not ship as a virtual slot. +TEST_CASE("Published 3MF reports a relocated mixed filament whose payload cells are missing", "[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", "My PLA", "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(5, "#123456"); + const std::vector receiver_colours = + bundle.project_config.opt("filament_colour")->values; + + PublishedMaterialEntry mix; + mix.filament_type = "PLA"; + mix.filament_vendor = "Generic"; + mix.filament_id = "GFL99"; + mix.slot = 3; // authored slot 3; the receiver's five real slots occupy 0-4 + 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 }; + // The file's mixed arrays only cover its single physical slot: the definition data for + // slot 3 does not exist in the payload. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75 }; + config.opt("filament_self_index")->values = { 1 }; + config.opt("filament_extruder_variant")->values = { "Direct Drive Standard" }; + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt("filament_type")->values = { "PLA" }; + config.opt("filament_vendor")->values = { "Generic" }; + config.opt("filament_is_mixed")->values = { 0 }; + config.opt("filament_mixed_components")->values = { "" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "" }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The mix was relocated past the physical territory... + REQUIRE(pub.mixed_slot_relocations.size() == 1); + CHECK(pub.mixed_slot_relocations.at(3) == 5); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0].find("slot 3 -> slot 5") != std::string::npos); + // ...and the receiver grew to hold the destination slot, but the definition itself was + // rejected: the relocated cells degraded to empty defaults and were reported. + REQUIRE(bundle.filament_presets.size() == 6); + CHECK_FALSE(bundle.is_mixed_filament(5)); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[5].empty()); + CHECK(bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[5].empty()); + CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)")); + // The five real slots kept their colours. + CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), + bundle.project_config.opt("filament_colour")->values.begin())); +} + +// A grown slot's material is chosen by identity tiers: exact preset name, then the bare +// name/alias form, then exact setting_id, then exact filament_id, then vendor+type, then type +// only. Each section pits two adjacent tiers against each other. +TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Preset][Bundle][Published]") +{ + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + return config; + }; + + PublishedMaterialEntry entry; + entry.slot = 2; + entry.filament_type = "PLA"; + + SECTION("an exact preset name outranks the bare-name form") + { + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &bare = add_inmemory_preset(bundle.filaments, "Authored PLA"); + bare.config.opt_string("filament_type", 0u) = "PLA"; + Preset &exact = add_inmemory_preset(bundle.filaments, "Authored PLA @Vendor"); + exact.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA" }; + + entry.preset_name = "Authored PLA @Vendor"; + 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.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "Authored PLA @Vendor"); + CHECK(pub.material_replacements.empty()); + } + + SECTION("a bare name outranks an exact setting_id") + { + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &bare = add_inmemory_preset(bundle.filaments, "Authored PLA"); + bare.config.opt_string("filament_type", 0u) = "PLA"; + Preset &sid = add_inmemory_preset(bundle.filaments, "Bbb PLA"); + sid.config.opt_string("filament_type", 0u) = "PLA"; + sid.setting_id = "SID123"; + bundle.filament_presets = { "My PLA" }; + + entry.preset_name = "Authored PLA @Vendor"; // no library preset carries this name + entry.setting_id = "SID123"; + 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.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "Authored PLA"); + } + + SECTION("an exact setting_id outranks an exact filament_id") + { + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &sid = add_inmemory_preset(bundle.filaments, "Bbb PLA"); + sid.config.opt_string("filament_type", 0u) = "PLA"; + sid.setting_id = "SID123"; + Preset &fid = add_inmemory_preset(bundle.filaments, "Ccc PLA"); + fid.config.opt_string("filament_type", 0u) = "PLA"; + fid.filament_id = "GFA00"; + bundle.filament_presets = { "My PLA" }; + + entry.preset_name = "Authored PLA @Vendor"; // no library preset carries this name + entry.setting_id = "SID123"; + entry.filament_id = "GFA00"; + 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.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[2] == "Bbb PLA"); + } + + SECTION("a vendor+type match is reported as a substitute") + { + PresetBundle bundle; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); + mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &exact_vendor = add_inmemory_preset(bundle.filaments, "Aaa PLA"); + exact_vendor.config.opt_string("filament_type", 0u) = "PLA"; + exact_vendor.config.opt_string("filament_vendor", 0u) = "Generic"; + Preset &other_vendor = add_inmemory_preset(bundle.filaments, "Zzz PLA"); + other_vendor.config.opt_string("filament_type", 0u) = "PLA"; + other_vendor.config.opt_string("filament_vendor", 0u) = "Other"; + bundle.filament_presets = { "My PLA" }; + + entry.filament_vendor = "Generic"; // no name or id identity: the family tiers decide + 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.size() == 3); + CHECK(bundle.filament_presets[1] == "My PLA"); + // The same-vendor PLA outranks the type-only candidate... + CHECK(bundle.filament_presets[2] == "Aaa PLA"); + // ...and since it is not an exact material match, the load says so. + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: Aaa PLA (substitute: no exact material match)"); + } +} + +// Structural keys (identity links like filament_ids / inherits / printer_settings_id) are +// never applied onto the receiver and never reported: a hand-crafted file listing them must +// not trigger the "could not be applied" warning, while unknown keys still do. +TEST_CASE("Published 3MF silently ignores structural keys in published_keys", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt_float("layer_height") = 0.28; + Preset::normalize(config); + + PresetBundle bundle; + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.1; + const std::vector ids_before = + bundle.filaments.get_edited_preset().config.opt("filament_settings_id")->values; + + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "filament_ids", "inherits", "printer_settings_id", "layer_height", "not_a_setting" }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The real setting applied... + CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 1e-6)); + CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); + // ...the structural keys were neither applied nor reported... + CHECK_FALSE(contains_key(pub.skipped_keys, "filament_ids")); + CHECK_FALSE(contains_key(pub.skipped_keys, "inherits")); + CHECK_FALSE(contains_key(pub.skipped_keys, "printer_settings_id")); + CHECK(bundle.filaments.get_edited_preset().config.opt("filament_settings_id")->values == ids_before); + // ...while an unknown key still reports. + CHECK(contains_key(pub.skipped_keys, "not_a_setting")); +} + +// A whole-vector key requires the receiver's vector to have the same number of elements as the +// author's: pasting a 3-extruder list into a 2-extruder machine would overwrite the wrong +// elements, so the key is reported as skipped and the receiver keeps its own values. +TEST_CASE("Published 3MF skips a whole-vector key whose size does not match the receiver", "[Preset][Bundle][Published]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_colour")->values = { "#FF0000" }; + config.opt_float("layer_height") = 0.28; + // Author's wiping matrix sized for three extruders. + config.set_key_value("wiping_volumes_extruders", new ConfigOptionFloats({ 10., 20., 30. })); + Preset::normalize(config); + + PresetBundle bundle; + // Receiver sized for two extruders. + bundle.prints.get_edited_preset().config.set_key_value("wiping_volumes_extruders", new ConfigOptionFloats({ 40., 50. })); + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.1; + + PublishedConfig pub; + pub.published = true; + pub.published_keys = { "wiping_volumes_extruders", "layer_height" }; + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + check_double_vector(bundle.prints.get_edited_preset().config.opt("wiping_volumes_extruders")->values, + { 40., 50. }); + CHECK(contains_key(pub.skipped_keys, "wiping_volumes_extruders")); + // The matching scalar key still applied. + CHECK_FALSE(contains_key(pub.skipped_keys, "layer_height")); + CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.28, 1e-6)); +} + +// The uniquify chain continues past the first suffix: with both "X" and "X (Published)" already +// present, the next imported copy of "X" lands as "X (Published 2)" and leaves the others alone. +TEST_CASE("Published 3MF uniquifies a second imported full material as (Published 2)", "[Preset][Bundle][Published]") +{ + PresetBundle bundle; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + petg.config.opt("filament_retraction_length", true)->values = { 0.6 }; + Preset &bare = add_inmemory_preset(bundle.filaments, "Generic PLA"); + bare.config.opt_string("filament_type", 0u) = "PLA"; + bare.config.opt("filament_retraction_length", true)->values = { 0.5 }; + Preset &pub1 = add_inmemory_preset(bundle.filaments, "Generic PLA (Published)"); + pub1.config.opt_string("filament_type", 0u) = "PLA"; + pub1.config.opt("filament_retraction_length", true)->values = { 0.5 }; + bundle.filament_presets = { "My PETG" }; + + PublishedMaterialEntry entry; + entry.slot = 0; + entry.full = true; + entry.publish_type = true; + entry.publish_type_value = "PLA"; + entry.preset_name = "Generic PLA @Qidi Q2 0.4 nozzle"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = published_pla_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets[0] == "Generic PLA (Published 2)"); + check_double_vector(bundle.filaments.find_preset("Generic PLA (Published 2)", false, true) + ->config.opt("filament_retraction_length")->values, + { 0.9 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA", false, true) + ->config.opt("filament_retraction_length")->values, + { 0.5 }); + check_double_vector(bundle.filaments.find_preset("Generic PLA (Published)", false, true) + ->config.opt("filament_retraction_length")->values, + { 0.5 }); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 0: My PETG -> Generic PLA (Published 2) (published material imported)"); + CHECK(pub.skipped_keys.empty()); +} + +// A mixed filament's blended colour is a swatch for the project's colour strip only: it must +// never be written into the slot's (possibly shared) preset config, or every slot referencing +// that preset would turn into the blend colour. +TEST_CASE("Published 3MF never writes a mixed filament's blended colour into the slot's preset", "[Preset][Bundle][Published]") +{ + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt("filament_colour", true)->values = { "#123456" }; + Preset &petg = add_inmemory_preset(bundle.filaments, "My PETG"); + petg.config.opt_string("filament_type", 0u) = "PETG"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(3); + bundle.project_config.opt("filament_is_mixed")->values[2] = 1; + bundle.project_config.opt("filament_mixed_components")->values[2] = "1,1"; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2] = "0.5,0.5"; + + 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 = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#0000FF", "#800080" }; + config.opt("filament_type")->values = { "PLA", "PETG", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("filament_is_mixed")->values = { 0, 0, 1 }; + config.opt("filament_mixed_components")->values = { "", "", "1,2" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "0.6,0.4" }; + config.opt("filament_mixed_gradient")->values = { 0, 0, 1 }; + config.opt("filament_mixed_gradient_range")->values = { "", "", "0.9,0.1" }; + config.opt("filament_mixed_gradient_curve")->values = { "", "", "0,0.1|1,0.9" }; + config.opt("filament_mixed_gradient_per_part")->values = { 0, 0, 1 }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The definition applied like-for-like onto the virtual slot... + CHECK(bundle.filament_presets.size() == 3); + CHECK(bundle.is_mixed_filament(2)); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[2] == "1,2"); + // ...the blend colour landed in the project strip only... + CHECK(bundle.project_config.opt("filament_colour")->values[2] == "#800080"); + // ...and the shared preset kept its own colour: slots 0 and 1 render unchanged. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == + std::vector{ "#123456" }); + CHECK(bundle.filament_presets[0] == "My PLA"); + CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(pub.skipped_keys.empty()); + CHECK(pub.material_replacements.empty()); +} + +// Duplicate entries for the same authored slot only occur in hand-crafted files (the dialog +// emits one entry per slot); the load's contract under that input is deterministic last-wins, +// not corruption. +TEST_CASE("Published 3MF applies duplicate entries for one slot last-wins", "[Preset][Bundle][Published]") +{ + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + pla.config.opt("filament_colour", true)->values = { "#000000" }; + bundle.filament_presets = { "My PLA" }; + + auto make_entry = [](const char *color) { + PublishedMaterialEntry entry; + entry.slot = 0; + entry.publish_color = true; + entry.color = color; + entry.keys = { "filament_retraction_length" }; + return entry; + }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_entry("#AA0000"), make_entry("#BB0000") }; + DynamicPrintConfig config = published_pla_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The second entry won both the project strip and the slot's preset. + CHECK(bundle.project_config.opt("filament_colour")->values[0] == "#BB0000"); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == + std::vector{ "#BB0000" }); + check_double_vector(bundle.filaments.find_preset("My PLA", false, true) + ->config.opt("filament_retraction_length")->values, + { 0.9 }); + CHECK(pub.skipped_keys.empty()); + CHECK(pub.material_replacements.empty()); +} From d9fa43f1d7e867cc315beecef2eeb06604a5118e Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 28 Aug 2026 19:40:31 +0800 Subject: [PATCH 33/60] fix: add opc support for ota workflow --- src/slic3r/Utils/PresetUpdater.cpp | 159 ++++++++++++++++++----------- 1 file changed, 100 insertions(+), 59 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 56a2b66b49..db29c7abf7 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -29,6 +29,7 @@ #include "libslic3r/format.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PresetCacheFormat.hpp" #include "libslic3r_version.h" #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" @@ -96,6 +97,8 @@ struct Update bool forced_update; //BBS: add directory support bool is_directory {false}; + // Orca: a vendor update may be the cache-only form. + bool is_opc {false}; Update() {} //BBS: add directory support @@ -131,6 +134,18 @@ struct Update } else { copy_file_fix(source, target); + + // A vendor must be installed in exactly one form. Remove the + // representation that would otherwise be stale or shadow this one. + boost::system::error_code ec; + if (is_opc) { + fs::remove(target.parent_path() / (vendor + ".json"), ec); + ec.clear(); + fs::remove_all(target.parent_path() / vendor, ec); + } + else { + fs::remove(target.parent_path() / (vendor + ".opc"), ec); + } } } @@ -708,6 +723,7 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) fs::remove_all(cache_profile_path / vendor_id, ec); fs::remove(cache_profile_path / (vendor_id + ".json"), ec); fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec); + fs::remove(cache_profile_path / (vendor_id + ".opc"), ec); // Download the zip BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading update for " << vendor_id @@ -735,7 +751,7 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) if (!download_ok || cancel || vendor_check_cancel) return; // Extract vendor profile bundles under ota/profiles. The downloaded zip contains - // the vendor json/folder at its root. + // either the vendor json/folder or the vendor cache at its root. BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting update for " << vendor_id; if (!extract_file(download_file, cache_profile_path)) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for " << vendor_id; @@ -1147,68 +1163,93 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version if (!fs::exists(cache_profile_path)) return updates; - for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) { - const auto &path = dir_entry.path(); - std::string file_path = path.string(); - if (is_json_file(file_path)) { - const auto path_in_vendor = vendor_path / path.filename(); - std::string vendor_name = path.filename().string(); - // Remove the .json suffix. - vendor_name.erase(vendor_name.size() - 5); - auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME); - auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); - auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); + for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) { + const auto &path = dir_entry.path(); + std::string file_path = path.string(); + const bool is_opc_file = boost::iequals(path.extension().string(), ".opc"); + if (!is_json_file(file_path) && !is_opc_file) + continue; - if (is_vendor_installed(vendor_name) - || fs::exists(print_in_cache) - || fs::exists(filament_in_cache) - || fs::exists(machine_in_cache)) { - // Orca: a vendor installed as a preset cache carries its version there. - Semver vendor_ver = installed_vendor_version(vendor_name); + const std::string vendor_name = path.stem().string(); + auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME); + auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); + auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); - std::map key_values; - std::vector keys(3); - Semver cache_ver; - keys[0] = BBL_JSON_KEY_VERSION; - keys[1] = BBL_JSON_KEY_DESCRIPTION; - keys[2] = BBL_JSON_KEY_FORCE_UPDATE; - get_values_from_json(file_path, keys, key_values); - std::string description = key_values[BBL_JSON_KEY_DESCRIPTION]; - bool force_update = false; - if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end()) - force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false; - auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]); - if (config_version) - cache_ver = *config_version; + if (is_vendor_installed(vendor_name) + || is_opc_file + || fs::exists(print_in_cache) + || fs::exists(filament_in_cache) + || fs::exists(machine_in_cache)) { + // Orca: a vendor installed as a preset cache carries its version there. + Semver vendor_ver = installed_vendor_version(vendor_name); - std::string changelog; - std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string(); - boost::nowide::ifstream ifs(changelog_file); - if (ifs) { - std::ostringstream oss; - oss<< ifs.rdbuf(); - changelog = oss.str(); - ifs.close(); - } + Semver cache_ver; + std::string description; + bool force_update = false; + if (is_opc_file) { + cache_ver = VendorCacheFile::usable_version(file_path, vendor_name); + if (!cache_ver.valid()) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring unreadable vendor cache " << file_path; + continue; + } + } + else { + std::map key_values; + std::vector keys(3); + keys[0] = BBL_JSON_KEY_VERSION; + keys[1] = BBL_JSON_KEY_DESCRIPTION; + keys[2] = BBL_JSON_KEY_FORCE_UPDATE; + get_values_from_json(file_path, keys, key_values); + description = key_values[BBL_JSON_KEY_DESCRIPTION]; + if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end()) + force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false; + auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]); + if (config_version) + cache_ver = *config_version; + } - if (vendor_ver < cache_ver) { - BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string() - << " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION; - Version version; - version.config_version = cache_ver; - version.comment = description; - // Orca: update vendor.json - updates.updates.emplace_back(std::move(file_path), path_in_vendor.string(), std::move(version), vendor_name, changelog, "", force_update, false); - //Orca: update vendor folder - updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true); - } else { - BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name - << " are not newer than installed version, installed " << vendor_ver.to_string() - << ", cached " << cache_ver.to_string(); - } - } - } - } + std::string changelog; + std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string(); + boost::nowide::ifstream ifs(changelog_file); + if (ifs) { + std::ostringstream oss; + oss<< ifs.rdbuf(); + changelog = oss.str(); + ifs.close(); + } + + if (vendor_ver < cache_ver) { + BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string() + << " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION; + Version version; + version.config_version = cache_ver; + version.comment = description; + if (is_opc_file) { + // A cache contains the vendor profile and all presets. + // Install it directly; Update::install removes any + // superseded JSON representation. + auto &update = updates.updates.emplace_back( + std::move(file_path), vendor_path / (vendor_name + ".opc"), + std::move(version), vendor_name, changelog, "", force_update, false); + update.is_opc = true; + } + else { + // JSON profile and its preset directory are installed + // separately, as before. + updates.updates.emplace_back(std::move(file_path), + vendor_path / (vendor_name + ".json"), std::move(version), + vendor_name, changelog, "", force_update, false); + updates.updates.emplace_back(cache_profile_path / vendor_name, + vendor_path / vendor_name, Version(), vendor_name, + "", "", force_update, true); + } + } else { + BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name + << " are not newer than installed version, installed " << vendor_ver.to_string() + << ", cached " << cache_ver.to_string(); + } + } + } return updates; } From 06517e623f0bcd61518dfe9409d77eac2b73ada2 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 10:46:23 +0800 Subject: [PATCH 34/60] Fixes issue where user imports a 3MF file where filament slots exceeds the maximum number of slots that user has on its printer --- src/libslic3r/PresetBundle.cpp | 183 ++++++++--- src/libslic3r/PublishSettings.hpp | 7 + .../libslic3r/test_preset_bundle_loading.cpp | 296 ++++++++++++++++++ 3 files changed, 446 insertions(+), 40 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 3219dec6d8..0e82046ce9 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5407,6 +5407,11 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // preset no other slot references wins on equal scores; with no replacement // available the receiver's material is kept and the keys are reported as skipped; // - colour: applied to the slot regardless of the type gate. + // - capacity: on a non-SEMM receiver whose printer has fewer nozzles than the + // authored slot needs, the entry becomes an empty mixed-filament placeholder + // appended at the tail (the GUI flags it; the user assigns components from their + // own filaments); on a single-physical-slot receiver it is dropped and reported + // instead, since an empty mix could never be edited there. // Applied partial values land on the collection's edited layer when the slot references // it and that layer survives the load (visible as a modification, revertible, the user's // unsaved edits preserved), otherwise on the stored preset in place. @@ -5417,11 +5422,38 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // Grow the receiver's slots only as far as the highest published slot (never // shrink, never pull filler materials for unpublished slots). bool has_published_entries = false; - size_t grow_target = 0; + // Physical filament capacity of the receiver's printer: a non-SEMM tool-changer + // feeds filament N from nozzle N, so the nozzle count is the hard limit; a SEMM + // printer (single_extruder_multi_material) sizes its slot list by hand, so only + // the global slot limit applies (same condition as GUI_App::load_current_presets). + // Published entries that would need a NEW physical slot past this capacity are + // appended as empty mixed-filament placeholders instead of growing the list. + size_t physical_capacity = size_t(EnforcerBlockerType::ExtruderMax); + { + const Preset& receiver_printer = this->printers.get_edited_preset(); + if (receiver_printer.printer_technology() == ptFFF && + !receiver_printer.config.opt_bool("single_extruder_multi_material")) { + if (const auto* nozzle_diameter = receiver_printer.config.option("nozzle_diameter"); + nozzle_diameter != nullptr && !nozzle_diameter->values.empty()) + physical_capacity = nozzle_diameter->values.size(); + } + } + const std::set& mixed_definitions = publish_mixed_keys(); + auto is_mixed_definition = [&mixed_definitions](const PublishedMaterialEntry& entry) { + return std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) { + return mixed_definitions.count(publish_base_key(key)) != 0; + }); + }; + size_t grow_target = 0; for (const PublishedMaterialEntry& entry : published_config->material_keys) { has_published_entries = true; - if (entry.slot >= 0) - grow_target = std::max(grow_target, size_t(entry.slot) + 1); + if (entry.slot < 0) + continue; + // A physical entry past the capacity becomes a tail placeholder below; its + // growth is covered by the append counter, not the positional target. + if (!is_mixed_definition(entry) && size_t(entry.slot) >= physical_capacity) + continue; + grow_target = std::max(grow_target, size_t(entry.slot) + 1); } // Mixed-filament definitions live in project-level virtual slots, so applying one // positionally onto a receiver slot that holds a real, physical filament would @@ -5439,13 +5471,12 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // from the new index. No existing slot changes meaning. // - destinations are also capped: appends past the extruder limit are dropped // and reported instead of being forced onto a physical filament. - const std::set& mixed_definitions = publish_mixed_keys(); - auto is_mixed_definition = [&mixed_definitions](const PublishedMaterialEntry& entry) { - return std::any_of(entry.keys.begin(), entry.keys.end(), [&](const std::string& key) { - return mixed_definitions.count(publish_base_key(key)) != 0; - }); - }; - size_t next_free_slot = this->filament_presets.size(); + // Physical entries past the printer's capacity join the same append counter + // (dest = next_free, packed consecutively at the tail - never max(authored, + // next_free), which would grow filler physical slots past the capacity) and are + // flagged mixed_placeholder: they become empty mixed-filament placeholders the + // GUI flags for the user to assign components to. + size_t next_free_slot = this->filament_presets.size(); bool any_mixed_relocated = false; // All authored-slot -> destination moves decided by this pass, applied to the // incoming config in one batched snapshot step below (an earlier move's @@ -5455,42 +5486,95 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, std::vector> mixed_moves; for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) { PublishedMaterialEntry& entry = *entry_it; - if (entry.slot < 0 || !is_mixed_definition(entry) || - // Like-for-like override of a virtual receiver slot (bounds-checked). - this->is_mixed_filament(size_t(entry.slot))) { + if (entry.slot < 0) { ++entry_it; continue; } + const bool is_payload_mix = is_mixed_definition(entry); + // Does this entry need a tail slot at all? A payload mixed definition does, + // except when it like-for-like overrides a receiver slot that is already a + // mix (bounds-checked). A physical entry only becomes a placeholder when it + // would need a NEW physical slot: an authored position the receiver's list + // already covers is applied positionally as before, even when that list sits + // above the printer's capacity (pre-existing state is never shrunk). + bool keep_place = false; + if (is_payload_mix) + keep_place = this->is_mixed_filament(size_t(entry.slot)); + else + keep_place = size_t(entry.slot) < this->filament_presets.size() || + size_t(entry.slot) < physical_capacity; + if (keep_place) { + ++entry_it; + continue; + } + const std::string material_label = !entry.filament_id.empty() ? entry.filament_id : + !entry.publish_type_value.empty() ? entry.publish_type_value : + entry.filament_type; if (std::max(size_t(entry.slot), next_free_slot) >= size_t(EnforcerBlockerType::ExtruderMax)) { // No free virtual slot left: report instead of destroying a real filament. - const std::string material_label = !entry.filament_id.empty() ? entry.filament_id : - !entry.publish_type_value.empty() ? entry.publish_type_value : - entry.filament_type; // The local skipped_keys is published wholesale at the end of the pass; // writing published_config->skipped_keys here would be clobbered by it. - skipped_keys.emplace_back("material:" + material_label + - " (mixed filament definition: filament slot limit reached)"); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot - << " could not be placed: all " << next_free_slot << " slots exhausted"; + skipped_keys.emplace_back("material:" + material_label + (is_payload_mix ? + " (mixed filament definition: filament slot limit reached)" : + " (filament slot limit reached)")); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF " << (is_payload_mix ? "mixed filament definition" : "material") + << " from slot " << entry.slot << " could not be placed: all " << next_free_slot + << " slots exhausted"; + entry_it = published_config->material_keys.erase(entry_it); + continue; + } + if (!is_payload_mix && physical_capacity < 2) { + // A single physical slot can never host a mixed-filament editor (the + // sidebar's mixed section needs two physical filaments to mix), so a + // placeholder would be invisible and unfixable: drop the entry and + // report it like any other unappliable input. + skipped_keys.emplace_back("material:" + material_label + " (printer supports only " + + std::to_string(physical_capacity) + " filament)"); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF material from slot " << entry.slot + << " dropped: printer supports only " << physical_capacity << " filament"; entry_it = published_config->material_keys.erase(entry_it); continue; } const int authored_slot = entry.slot; - const int dest_slot = int(std::max(size_t(authored_slot), next_free_slot)); + const int dest_slot = is_payload_mix ? int(std::max(size_t(authored_slot), next_free_slot)) : int(next_free_slot); next_free_slot = size_t(dest_slot) + 1; - if (dest_slot == authored_slot) - // Uncontended fresh tail slot: the definition is already readable there. - ++entry_it; - else { - entry.slot = dest_slot; + if (is_payload_mix) { + if (dest_slot == authored_slot) + // Uncontended fresh tail slot: the definition is already readable there. + ++entry_it; + else { + entry.slot = dest_slot; + any_mixed_relocated = true; + mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot)); + published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot); + published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " + + std::to_string(entry.slot) + + ": mixed filament relocated (would have replaced a physical filament)"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot + << " -> " << entry.slot; + ++entry_it; + } + } else { + // Surplus published material beyond the printer's capacity: become an + // empty mixed-filament placeholder at the next free tail slot. The flag + // routes the entry to the placeholder finalize below; the slot's + // definition stays empty until the user assigns components. + entry.mixed_placeholder = true; any_mixed_relocated = true; - mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot)); - published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot); - published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " + - std::to_string(entry.slot) + - ": mixed filament relocated (would have replaced a physical filament)"); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot - << " -> " << entry.slot; + if (dest_slot != authored_slot) { + entry.slot = dest_slot; + mixed_moves.emplace_back(size_t(authored_slot), size_t(dest_slot)); + published_config->mixed_slot_relocations.emplace(authored_slot, dest_slot); + } + published_config->material_replacements.emplace_back( + (dest_slot != authored_slot ? + "slot " + std::to_string(authored_slot) + " -> slot " + std::to_string(dest_slot) : + "slot " + std::to_string(dest_slot)) + + ": " + material_label + " placed as an unassigned mixed filament (printer supports only " + + std::to_string(physical_capacity) + " filaments)"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF material from slot " << authored_slot + << " placed as an unassigned mixed filament at slot " << dest_slot + << " (printer supports only " << physical_capacity << " filaments)"; ++entry_it; } } @@ -5501,9 +5585,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // Defensive cap: growth never exceeds the file's own filament count. The // receiver's current slot count is a floor: neither the preset list nor the // project vectors are ever shrunk, even when the file carries fewer filaments - // than the receiver has slots. Relocated mixed entries legitimately land past - // the file's own slot count (virtual slots consume no nozzle or tray), so - // their destinations lift the ceiling explicitly. + // than the receiver has slots. Relocated mixed entries and capacity + // placeholders legitimately land past the file's own slot count (virtual + // slots consume no nozzle or tray), so their append destinations lift the + // ceiling explicitly. const size_t target_slots = std::max({this->filament_presets.size(), std::min(grow_target, num_filaments), any_mixed_relocated ? next_free_slot : size_t(0)}); // Slots carrying published content, steering the initial preset selection of @@ -5884,12 +5969,14 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, this->filament_presets.front() == this->filaments.get_edited_preset().name; // Final layout for mix-definition validation: every slot that will hold a // mixed definition once this load completes - the receiver's own virtual - // slots plus each published mixed entry's final (possibly relocated) slot. - // Mix components are 1-based slot numbers, so a component is valid only - // when the slot it names exists and does not itself hold a mixed filament. + // slots, each published mixed entry's final (possibly relocated) slot, and + // each capacity placeholder (they become mixes in the finalize below, before + // this loop's is_mixed_filament scan would see them). Mix components are + // 1-based slot numbers, so a component is valid only when the slot it names + // exists and does not itself hold a mixed filament. std::set mixed_final_slots; for (const PublishedMaterialEntry& mix_entry : published_config->material_keys) - if (mix_entry.slot >= 0 && is_mixed_definition(mix_entry)) + if (mix_entry.slot >= 0 && (is_mixed_definition(mix_entry) || mix_entry.mixed_placeholder)) mixed_final_slots.insert(mix_entry.slot); for (size_t i = 0; i < this->filament_presets.size(); ++i) if (this->is_mixed_filament(i)) @@ -5903,6 +5990,22 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, 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); + + // Surplus published material beyond the printer's capacity: finalize the + // tail placeholder - mark the slot virtual with an intentionally empty + // definition. The GUI flags the empty mix (check_mixed_filament_integrity) + // and blocks slicing until the user assigns components from their own + // filaments. The entry's keys are deliberately not applied and no preset + // is detached: the placeholder carries no material of its own. The colour + // was already seeded into the project arrays by the growth pass above. + if (entry.mixed_placeholder) { + if (ConfigOptionBools* is_mixed_opt = this->project_config.opt("filament_is_mixed"); + is_mixed_opt != nullptr && slot < is_mixed_opt->values.size()) + is_mixed_opt->values[slot] = true; + material_applied = true; + continue; + } + // Resolve the stored preset itself (real=true), never the edited snapshot: // find_preset would return &m_edited_preset for the selected slot. The // overlay target below decides between the edited layer and this preset. diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index 038a6eca11..d7f2c13c5f 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -80,6 +80,13 @@ struct PublishedMaterialEntry { // Required filament colour, applied on load regardless of the type match. bool publish_color{false}; std::string color; + // Import-side only, never serialized: the entry's authored slot sits past the receiver + // printer's physical filament capacity, so instead of growing a physical slot the entry + // is appended as an empty mixed-filament placeholder (virtual tail slot; the GUI flags + // it and the user assigns components from their own filaments). The flag also keeps the + // entry out of the payload mixed-definition validation and the value-apply passes, + // which only make sense for a slot that carries a real material. + bool mixed_placeholder{false}; }; // "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact. diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 356645d06c..069d40b8e5 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -2932,6 +2932,302 @@ TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a phy } } +// The receiver's printer gates how many PHYSICAL filament slots a published 3MF may add: a +// non-SEMM tool-changer feeds filament N from nozzle N, so a published slot past the nozzle +// count cannot become a physical filament. It becomes an empty mixed-filament placeholder +// instead - a virtual tail slot the GUI flags (broken mix) and the user fills with components +// from their own filaments. SEMM receivers keep the ungated behaviour. +TEST_CASE("Published 3MF turns a surplus slot past the printer's filament capacity into an empty mixed placeholder", "[Preset][Bundle][Published]") +{ + // An author project with physical slots, no mixed ones. + auto make_file_config = [](size_t num_author_slots) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + std::vector diameters(num_author_slots, 1.75); + std::vector self_index; + std::vector variants; + for (size_t i = 0; i < num_author_slots; ++i) { + self_index.push_back(int(i + 1)); + variants.emplace_back("Direct Drive Standard"); + } + config.opt("filament_diameter")->values = diameters; + config.opt("filament_self_index")->values = self_index; + config.opt("filament_extruder_variant")->values = variants; + config.opt("filament_colour")->values.resize(num_author_slots, "#808080"); + config.opt("filament_type")->values.assign(num_author_slots, "PLA"); + config.opt("filament_vendor")->values.assign(num_author_slots, "Generic"); + config.opt("filament_ids")->values.resize(num_author_slots); + return config; + }; + // A non-SEMM receiver with nozzles running copies of one preset. + auto make_receiver = [](PresetBundle &bundle, size_t nozzles, size_t slots) { + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets.assign(slots, "My PLA"); + bundle.set_num_filaments(slots, "#123456"); + auto &printer_config = bundle.printers.get_edited_preset().config; + printer_config.opt("single_extruder_multi_material", true)->value = false; + printer_config.opt("nozzle_diameter", true)->values.assign(nozzles, 0.4); + }; + auto make_physical_entry = [](int slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.publish_color = true; + entry.color = color; + return entry; + }; + + // The reported case: an author publishes with a filament on slot 5; the receiver is a + // 4-filament tool-changer. The receiver keeps its four physical slots and the surplus + // material lands as an empty mixed placeholder at the tail. + { + PresetBundle bundle; + make_receiver(bundle, 4, 4); + const std::vector receiver_colours = + bundle.project_config.opt("filament_colour")->values; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(4, "#ABCDEF") }; + DynamicPrintConfig config = make_file_config(5); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The receiver grew by exactly one virtual slot, not a fifth physical one. + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + for (size_t i = 0; i < 4; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[4]); + // The placeholder carries no definition: the GUI's integrity check flags it and + // blocks slicing until the user assigns components. + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 5); + CHECK(components[4].empty()); + // The four physical slots kept their meaning and colours. + CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), + bundle.project_config.opt("filament_colour")->values.begin())); + CHECK(bundle.filament_presets[0] == "My PLA"); + CHECK(bundle.filament_presets[3] == "My PLA"); + // The published colour seeds the placeholder's swatch. + CHECK(bundle.project_config.opt("filament_colour")->values[4] == "#ABCDEF"); + // The conversion is surfaced through the post-import notice. + bool placeholder_reported = false; + for (const std::string &message : pub.material_replacements) + if (message.find("unassigned mixed filament") != std::string::npos) + placeholder_reported = true; + CHECK(placeholder_reported); + CHECK(pub.skipped_keys.empty()); + CHECK(pub.mixed_slot_relocations.empty()); + } + + // Two surplus slots (5 and 6) become two consecutive empty placeholders. + { + PresetBundle bundle; + make_receiver(bundle, 4, 4); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(4, "#ABCDEF"), make_physical_entry(5, "#F0F0F0") }; + DynamicPrintConfig config = make_file_config(6); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 6); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 6); + for (size_t i = 0; i < 4; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[4]); + CHECK(is_mixed[5]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 6); + CHECK(components[4].empty()); + CHECK(components[5].empty()); + const auto &colour = bundle.project_config.opt("filament_colour")->values; + REQUIRE(colour.size() == 6); + CHECK(colour[4] == "#ABCDEF"); + CHECK(colour[5] == "#F0F0F0"); + CHECK(pub.skipped_keys.empty()); + CHECK(pub.mixed_slot_relocations.empty()); + } + + // A surplus slot past both the receiver's list and the capacity packs onto the next free + // tail slot (never max(authored, next_free), which would grow filler physical slots past + // the capacity), and the relocation is recorded for the model-reference remapping. + { + PresetBundle bundle; + make_receiver(bundle, 2, 2); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(3, "#ABCDEF") }; + DynamicPrintConfig config = make_file_config(4); + 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("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 3); + CHECK_FALSE(is_mixed[0]); + CHECK_FALSE(is_mixed[1]); + CHECK(is_mixed[2]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 3); + CHECK(components[2].empty()); + REQUIRE(pub.mixed_slot_relocations.size() == 1); + CHECK(pub.mixed_slot_relocations.at(3) == 2); + bool relocation_reported = false; + for (const std::string &message : pub.material_replacements) + if (message.find("slot 3 -> slot 2") != std::string::npos && + message.find("unassigned mixed filament") != std::string::npos) + relocation_reported = true; + CHECK(relocation_reported); + CHECK(pub.skipped_keys.empty()); + } + + // A Full Publish entry past the capacity becomes a placeholder too: no standalone + // detached copy is created for a material that got no physical slot. + { + PresetBundle bundle; + make_receiver(bundle, 4, 4); + + PublishedMaterialEntry entry = make_physical_entry(4, "#ABCDEF"); + entry.full = true; + entry.preset_name = "Generic PLA @System"; + entry.filament_id = "GFL99"; + entry.full_keys = { "filament_retraction_length" }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { entry }; + DynamicPrintConfig config = make_file_config(5); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + CHECK(is_mixed[4]); + // No detached copy under the stripped name or its uniquified forms. + CHECK(bundle.filaments.find_preset("Generic PLA", false, true) == nullptr); + CHECK(bundle.filaments.find_preset("Generic PLA (Published)", false, true) == nullptr); + CHECK(pub.skipped_keys.empty()); + } + + // A SEMM receiver (the default printer preset) sizes its slot list by hand: the published + // slot past the nozzle count still grows physically, as before the capacity gate. + { + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(4, "#123456"); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(4, "#ABCDEF") }; + DynamicPrintConfig config = make_file_config(5); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + for (size_t i = 0; i < 5; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(bundle.project_config.opt("filament_colour")->values[4] == "#ABCDEF"); + CHECK(pub.skipped_keys.empty()); + } + + // A pre-existing oversized slot list is never shrunk: a published entry pointing at one + // of its slots is applied positionally even though the list exceeds the nozzle count. + { + PresetBundle bundle; + make_receiver(bundle, 4, 5); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(4, "#ABCDEF") }; + DynamicPrintConfig config = make_file_config(5); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + for (size_t i = 0; i < 5; ++i) + CHECK_FALSE(is_mixed[i]); + // The published colour reached the addressed slot's (shared) preset in place. + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == + std::vector{ "#ABCDEF" }); + CHECK(bundle.project_config.opt("filament_colour")->values[4] == "#ABCDEF"); + CHECK(pub.skipped_keys.empty()); + } + + // On a single-physical-slot receiver an empty mix could never be edited (the sidebar's + // mixed section needs two physical filaments), so the surplus entry is dropped and + // reported instead of becoming an unfixable placeholder. + { + PresetBundle bundle; + make_receiver(bundle, 1, 1); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_physical_entry(1, "#ABCDEF") }; + DynamicPrintConfig config = make_file_config(2); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + CHECK(bundle.filament_presets.size() == 1); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 1); + CHECK_FALSE(is_mixed[0]); + REQUIRE(pub.skipped_keys.size() == 1); + CHECK(pub.skipped_keys.front().find("printer supports only 1") != std::string::npos); + } + + // A payload mixed definition is exempt from the capacity gate: mixes are virtual slots + // that consume no nozzle, so a published mix past the nozzle count still lands. + { + PresetBundle bundle; + make_receiver(bundle, 4, 4); + + PublishedMaterialEntry mix; + mix.slot = 4; + mix.publish_color = true; + mix.color = "#800080"; + 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(5); + config.opt("filament_is_mixed")->values.assign(5, 0); + config.opt("filament_mixed_components")->values.assign(5, ""); + config.opt("filament_mixed_sublayer_ratios")->values.assign(5, ""); + config.opt("filament_is_mixed")->values[4] = 1; + config.opt("filament_mixed_components")->values[4] = "1,2"; + config.opt("filament_mixed_sublayer_ratios")->values[4] = "0.6,0.4"; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + CHECK(is_mixed[4]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 5); + CHECK(components[4] == "1,2"); + const auto &ratios = bundle.project_config.opt("filament_mixed_sublayer_ratios")->values; + REQUIRE(ratios.size() == 5); + CHECK(ratios[4] == "0.6,0.4"); + CHECK(pub.skipped_keys.empty()); + } +} + // 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. From fff0efdb275003cf19d2c04b9605f3af6270a37e Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 13:57:21 +0800 Subject: [PATCH 35/60] Fixes filament import bug --- src/libslic3r/PresetBundle.cpp | 61 ++++++++- .../libslic3r/test_preset_bundle_loading.cpp | 123 +++++++++++++++++- 2 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 2a7e97a34e..688d2b90b0 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5979,6 +5979,22 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, if (this->is_mixed_filament(i)) mixed_final_slots.insert(int(i)); const size_t mixed_final_slot_count = this->filament_presets.size(); + // Finalize a slot as an empty mixed-filament placeholder: mark it virtual with + // an intentionally empty definition, and add it to the final-layout set so a + // later entry's components validate against the new state. Used by the capacity + // placeholders and by any mixed definition that has to be rejected after its + // slot was already grown and seeded - without it such a slot would keep the + // seeded preset and masquerade as a real filament. No definition is written: + // the placeholder carries no material of its own (the GUI flags the empty mix + // via check_mixed_filament_integrity and blocks slicing until components are + // assigned). The slot's colour was already seeded by the growth pass above. + auto finalize_mixed_placeholder = [&](size_t slot_idx) { + if (ConfigOptionBools* is_mixed_opt = this->project_config.opt("filament_is_mixed"); + is_mixed_opt != nullptr && slot_idx < is_mixed_opt->values.size()) + is_mixed_opt->values[slot_idx] = true; + mixed_final_slots.insert(int(slot_idx)); + material_applied = true; + }; // Full Publish within-load dedup: identical Full materials (same setting_id // + preset_name identity) share one created instance, so an author who // pointed two slots at one preset yields one standalone copy here. @@ -5996,10 +6012,7 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // is detached: the placeholder carries no material of its own. The colour // was already seeded into the project arrays by the growth pass above. if (entry.mixed_placeholder) { - if (ConfigOptionBools* is_mixed_opt = this->project_config.opt("filament_is_mixed"); - is_mixed_opt != nullptr && slot < is_mixed_opt->values.size()) - is_mixed_opt->values[slot] = true; - material_applied = true; + finalize_mixed_placeholder(slot); continue; } @@ -6007,8 +6020,19 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // find_preset would return &m_edited_preset for the selected slot. The // overlay target below decides between the edited layer and this preset. Preset* recv = this->filaments.find_preset(this->filament_presets[slot], false, true); - if (recv == nullptr) + if (recv == nullptr) { + // Defensive: the slot exists (grown and seeded above) but its preset + // could not be resolved. A mixed definition left unapplied on such a + // slot would keep the seeded preset and look like a real filament: + // finalize it as an empty placeholder instead, like a rejected + // definition below. + if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) { + finalize_mixed_placeholder(slot); + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + + ": mixed filament definition could not be imported"); + } continue; + } const std::string material_label = entry.filament_id.empty() ? (entry.publish_type_value.empty() ? entry.filament_type : @@ -6045,6 +6069,23 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")"); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot << " rejected: " << mix_error; + // Degradation, not silent corruption: the slot was already grown and + // seeded by the pass above, so skipping the definition alone would + // leave it holding the seeded preset and looking like a real + // filament that carries the mix identity but no mix. + // - a slot that was NOT already a receiver mix is finalized as an + // empty mixed placeholder (consistent with the capacity + // placeholders; the user assigns components there); + // - a like-for-like override of the receiver's own mix leaves that + // valid definition untouched: nothing was applied, so the + // receiver's cells are intact and only the author's definition + // is reported as skipped above. + if (!this->is_mixed_filament(slot)) { + finalize_mixed_placeholder(slot); + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + + ": mixed filament definition could not be imported (" + + mix_error + ")"); + } continue; } } @@ -6247,6 +6288,16 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, for (const std::string& key : entry.keys) skipped_keys.emplace_back("material:" + material_label + " (" + key + ")"); apply_slot = false; + // A mixed definition left unapplied here would keep the grown + // slot's seeded preset and masquerade as a real filament: + // finalize it as an empty placeholder like a rejected + // definition (a like-for-like override of the receiver's own + // mix is left untouched). + if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) { + finalize_mixed_placeholder(slot); + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + + ": mixed filament definition could not be imported"); + } } } } diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index a493ea9b7b..17e33f3320 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -11,6 +11,7 @@ #include "test_utils.hpp" #include +#include #include using namespace Slic3r; @@ -3671,9 +3672,11 @@ TEST_CASE("Published 3MF rejects a mixed filament definition with impossible com Preset::normalize(config); bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); - // The definition was rejected, not applied: the slot stays a plain (grown) slot. + // The definition was rejected, not applied: the grown slot is finalized as an empty + // mixed placeholder instead of keeping the seeded preset and masquerading as a real + // filament. REQUIRE(bundle.filament_presets.size() == 3); - CHECK_FALSE(bundle.is_mixed_filament(2)); + CHECK(bundle.is_mixed_filament(2)); CHECK(bundle.project_config.opt("filament_mixed_components")->values[2].empty()); CHECK(bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2].empty()); // Reported through the shared rejection channel. @@ -3681,13 +3684,119 @@ TEST_CASE("Published 3MF rejects a mixed filament definition with impossible com CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)")); else CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: components reference missing slots)")); + // The slot change was surfaced like the other slot adaptations. + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0].find("slot 2: mixed filament definition could not be imported") != std::string::npos); // The blended colour was not written into the (shared) slot preset either. CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); - CHECK(pub.material_replacements.empty()); } } +// The reported scenario: an author publishes two mixed filaments whose components are +// full-published physical slots; the receiver is a smaller tool-changer, so some of those +// component slots become empty mixed placeholders. A mix whose component turned into a +// placeholder can never be valid (mixes cannot reference mixes): it is rejected, and its +// grown slot must be finalized as an empty mixed placeholder too - not keep the seeded +// preset and masquerade as a real filament carrying the mix identity. +TEST_CASE("Published 3MF finalizes a mixed filament rejected over a placeholder component as an empty placeholder", "[Preset][Bundle][Published]") +{ + // An author project: six physical slots plus two tail mixes, the second referencing the + // sixth physical slot (H2C-style: slot 7 = 1+2, slot 8 = 2+6). + auto make_file_config = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = std::vector(8, 1.75); + config.opt("filament_self_index")->values = { 1, 2, 3, 4, 5, 6, 7, 8 }; + config.opt("filament_extruder_variant")->values = std::vector(8, "Direct Drive Standard"); + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00", + "#FF00FF", "#00FFFF", "#800080", "#804000" }; + config.opt("filament_type")->values.assign(8, "PLA"); + config.opt("filament_vendor")->values.assign(8, "Generic"); + config.opt("filament_is_mixed")->values = { 0, 0, 0, 0, 0, 0, 1, 1 }; + config.opt("filament_mixed_components")->values = { "", "", "", "", "", "", "1,2", "2,6" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "", "", "", "", "0.6,0.4", "0.5,0.5" }; + // The export always serializes all seven masked mixed arrays, not just the ones in + // use; the unused gradient arrays ride along as defaults. + config.opt("filament_mixed_gradient")->values = { 0, 0, 0, 0, 0, 0, 0, 0 }; + config.opt("filament_mixed_gradient_range")->values.assign(8, ""); + config.opt("filament_mixed_gradient_curve")->values.assign(8, ""); + config.opt("filament_mixed_gradient_per_part")->values = { 0, 0, 0, 0, 0, 0, 0, 0 }; + return config; + }; + // A non-SEMM receiver with four nozzles and four slots (tool-changer style). + PresetBundle bundle; + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets.assign(4, "My PLA"); + bundle.set_num_filaments(4, "#123456"); + auto &printer_config = bundle.printers.get_edited_preset().config; + printer_config.opt("single_extruder_multi_material", true)->value = false; + printer_config.opt("nozzle_diameter", true)->values.assign(4, 0.4); + + auto make_full_entry = [](int slot) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.full = true; + entry.full_keys = { "filament_retraction_length" }; + return entry; + }; + auto make_mix_entry = [](int slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.publish_color = true; + entry.color = color; + 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" }; + return entry; + }; + + // The dialog's emit order: the full-published physical slots (1, 2, 5, 6) and both mixes. + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_full_entry(0), make_full_entry(1), make_full_entry(4), make_full_entry(5), + make_mix_entry(6, "#800080"), make_mix_entry(7, "#804000") }; + DynamicPrintConfig config = make_file_config(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The receiver grew to the author's slot count, all virtual territory at the tail. + REQUIRE(bundle.filament_presets.size() == 8); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 8); + // Slots 0-3 stayed physical; authored slots 5 and 6 (0-based 4 and 5) became capacity + // placeholders. + for (size_t i = 0; i < 4; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[4]); + CHECK(is_mixed[5]); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[4].empty()); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[5].empty()); + // The first mix applied onto its uncontended tail slot. + CHECK(is_mixed[6]); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[6] == "1,2"); + // The second mix was rejected - its second component (authored slot 6) turned into a + // placeholder - and its slot was finalized as an empty mixed placeholder instead of + // keeping the seeded preset as a phantom real filament. + CHECK(is_mixed[7]); + CHECK(bundle.project_config.opt("filament_mixed_components")->values[7].empty()); + CHECK(bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[7].empty()); + REQUIRE(pub.skipped_keys.size() == 1); + CHECK(pub.skipped_keys[0] == "material:PLA (mixed filament definition: components reference missing slots)"); + // Two Full Publish detach lines (slots 0-1), two capacity placeholder lines (slots 4-5), + // and the rejected mix's finalization line (slot 7). + REQUIRE(pub.material_replacements.size() == 5); + CHECK(std::any_of(pub.material_replacements.begin(), pub.material_replacements.end(), + [](const std::string &line) { + return line.find("slot 7: mixed filament definition could not be imported") != std::string::npos; + })); +} + // The relocation shifts cells inside the file's per-slot mixed arrays; a payload too short to // actually carry the definition degrades to empty cells, which the definition validation then // reports - an empty mix must not ship as a virtual slot. @@ -3734,15 +3843,17 @@ TEST_CASE("Published 3MF reports a relocated mixed filament whose payload cells // The mix was relocated past the physical territory... REQUIRE(pub.mixed_slot_relocations.size() == 1); CHECK(pub.mixed_slot_relocations.at(3) == 5); - REQUIRE(pub.material_replacements.size() == 1); + REQUIRE(pub.material_replacements.size() == 2); CHECK(pub.material_replacements[0].find("slot 3 -> slot 5") != std::string::npos); // ...and the receiver grew to hold the destination slot, but the definition itself was - // rejected: the relocated cells degraded to empty defaults and were reported. + // rejected: the relocated cells degraded to empty defaults, the slot was finalized as an + // empty mixed placeholder (not a real filament), and both facts were reported. REQUIRE(bundle.filament_presets.size() == 6); - CHECK_FALSE(bundle.is_mixed_filament(5)); + CHECK(bundle.is_mixed_filament(5)); CHECK(bundle.project_config.opt("filament_mixed_components")->values[5].empty()); CHECK(bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[5].empty()); CHECK(contains_key(pub.skipped_keys, "material:GFL99 (mixed filament definition: needs at least two components)")); + CHECK(pub.material_replacements[1].find("slot 5: mixed filament definition could not be imported") != std::string::npos); // The five real slots kept their colours. CHECK(std::equal(receiver_colours.begin(), receiver_colours.end(), bundle.project_config.opt("filament_colour")->values.begin())); From a6483e79b241ce2b9ee79841913422f7a5a726dc Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 14:50:15 +0800 Subject: [PATCH 36/60] Fix ImGui crash --- src/slic3r/GUI/NotificationManager.cpp | 6 +++++- src/slic3r/GUI/NotificationManager.hpp | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index bfb9f50130..044771bf7c 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -3083,7 +3083,7 @@ bool NotificationManager::push_notification_data(std::unique_ptractivate_existing(notification.get())) { - if (m_initialized) { // ignore update action - it cant be initialized if canvas and imgui context is not ready + if (m_initialized && m_imgui_ready) { if (notification->get_type() == NotificationType::SlicingWarning) { m_pop_notifications.back()->append(notification->get_data().ori_text); } else { @@ -3129,6 +3129,10 @@ void NotificationManager::stop_delayed_notifications_of_type(const NotificationT void NotificationManager::render_notifications(GLCanvas3D &canvas, float overlay_width, float bottom_margin, float right_margin) { + // Notifications render inside an ImGui frame, so the font atlas is built from this point on + // and pushed notifications may safely measure their text. + m_imgui_ready = true; + sort_notifications(); float bottom_up_last_y = bottom_margin; // ORCA dont scale margins diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index d7a5d49977..55d10dd95c 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -1054,6 +1054,11 @@ private: bool m_is_dark = false; // set by init(), until false notifications are only added not updated and frame is not requested after push bool m_initialized{ false }; + // set by render_notifications() on the first rendered frame. m_initialized only proves the + // manager exists, not that the ImGui context can measure text: the font atlas is built lazily + // in ImGuiWrapper::new_frame() on the first GL render, so updating a notification before that + // (PopNotification::init -> count_spaces -> ImGui::CalcTextSize) dereferences a null font. + bool m_imgui_ready{ false }; // Target for wxWidgets events sent by clicking on the hyperlink available at some notifications. wxEvtHandler* m_evt_handler; // Cache of IDs to identify and reuse ImGUI windows. From c4fea8ad2477259fa4c50541407ebb4398f8d755 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 31 Aug 2026 15:48:13 +0800 Subject: [PATCH 37/60] gate workflow with enable_ota flag in app config --- src/slic3r/Utils/PresetUpdater.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index db29c7abf7..2de8716f47 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -283,7 +283,7 @@ void PresetUpdater::priv::set_download_prefs(AppConfig *app_config) version_check_url = app_config->version_check_url(); auto profile_update_url = app_config->profile_update_url(); - if (!profile_update_url.empty()) + if (!profile_update_url.empty() && app_config->get_bool("enable_ota")) enabled_config_update = true; else enabled_config_update = false; @@ -1055,6 +1055,10 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking whether the profile from resource is newer"; AppConfig *app_config = GUI::wxGetApp().app_config; + + if (!app_config->get_bool("enable_ota")) + return; + const auto enabled_vendors = app_config->vendors(); std::set bundles; From 21c0bf795ad25ed55000d2bf6c923d88a5ba888e Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 16:19:10 +0800 Subject: [PATCH 38/60] Fixes indentation on the Full Publish toggle --- src/slic3r/GUI/PublishSettingsDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 1a8a860301..455a318935 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -986,13 +986,13 @@ size_t PublishSettingsDialog::category_index_for( page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); // Line 2: the "Full Publish" toggle, on its own line below the title (hidden until - // the slot is enabled). + // the slot is enabled), aligned with the colour chip above it. auto* full_sizer = new wxBoxSizer(wxHORIZONTAL); 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")); full_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL); - category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(2)); + category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6)); } } From b585d9f3d96b511eaf65011061959b60b990c73b Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Mon, 31 Aug 2026 17:45:29 +0800 Subject: [PATCH 39/60] feat: add new profiles over ota & updater url via app config --- resources/web/guide/21/common.css | 10 +- resources/web/guide/21/common.js | 37 ++- resources/web/guide/24/24.js | 16 ++ resources/web/guide/24/index.html | 3 +- src/libslic3r/AppConfig.cpp | 8 +- src/slic3r/GUI/WebGuideDialog.cpp | 87 ++++++- src/slic3r/Utils/PresetUpdater.cpp | 381 +++++++++++++++++++++++++++-- src/slic3r/Utils/PresetUpdater.hpp | 8 + 8 files changed, 523 insertions(+), 27 deletions(-) diff --git a/resources/web/guide/21/common.css b/resources/web/guide/21/common.css index e7cd57a32f..992c55d7a1 100644 --- a/resources/web/guide/21/common.css +++ b/resources/web/guide/21/common.css @@ -415,7 +415,9 @@ img.ModelThumbnail { border-width: 1px; border-style: solid; border-radius: 4px; - background-color: inherit; + border-color: var(--border-color); + background-color: var(--bg-color); + color: var(--fg-color-text); position: absolute; left: 50%; top: 200px; @@ -423,13 +425,17 @@ img.ModelThumbnail { } #NoticeBar { - background-color:#00f0d8; + background-color: var(--main-color); height: 40px; line-height: 40px; color: #fff; text-align: center; } +#NoticeBar.notice-error { + background-color: var(--button-bg-alert); +} + #NoticeContent { padding: 4mm 10mm; } diff --git a/resources/web/guide/21/common.js b/resources/web/guide/21/common.js index f7e8b1d409..9a3c76c248 100644 --- a/resources/web/guide/21/common.js +++ b/resources/web/guide/21/common.js @@ -37,6 +37,34 @@ function HandleStudio( pVal ) { HandleModelList(pVal['response']); } + else if(strCmd=='check_new_printers_result') + { + let button = document.getElementById("CheckNewPrintersBtn"); + if (button) { + button.style.pointerEvents = "auto"; + button.style.opacity = "1"; + } + + let noticeBar = document.getElementById("NoticeBar"); + let noticeText = document.getElementById("NoticeText"); + let hasError = pVal.hasOwnProperty("error"); + noticeBar.classList.toggle("notice-error", hasError); + if (hasError) { + noticeBar.textContent = "Error"; + noticeText.textContent = pVal["error"]; + } else if (pVal["vendors"] && pVal["vendors"].length > 0) { + noticeBar.textContent = "New printers found"; + noticeText.textContent = "New printer vendors installed: " + pVal["vendors"].join(", "); + } else if (pVal["declined"]) { + noticeBar.textContent = "Information"; + noticeText.textContent = "New printer vendors were found, but installation was cancelled."; + } else { + noticeBar.textContent = "Information"; + noticeText.textContent = "No new printers found."; + } + + ShowNotice(1); + } } function HandleModelList( pVal ) @@ -78,9 +106,13 @@ function HandleModelList( pVal ) } //Update Nozzel Html Append + // ORCA: HandleModelList can now be called more than once per dialog (e.g. after a new vendor + // is installed via "check for new printers"). pModel always holds the full, current list, so + // clear each vendor's printer area before repopulating instead of appending on top of a + // previous render. for( let key in ModelHtml ) { - $(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").append( ModelHtml[key] ); + $(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").empty().append( ModelHtml[key] ); } //Update Checkbox @@ -343,6 +375,9 @@ function OnExit() let nTotal=ModelSelect.length; if( nTotal==0 ) { + let noticeBar = document.getElementById("NoticeBar"); + noticeBar.classList.add("notice-error"); + noticeBar.textContent = "Error"; ShowNotice(1); return 0; } diff --git a/resources/web/guide/24/24.js b/resources/web/guide/24/24.js index 74a03db7b9..19ccb892c6 100644 --- a/resources/web/guide/24/24.js +++ b/resources/web/guide/24/24.js @@ -36,6 +36,22 @@ function ConfirmSelect() } } +function CheckForNewPrinters() +{ + var tSend={}; + tSend['sequence_id']=Math.round(new Date() / 1000); + tSend['command']="check_for_new_printers"; + tSend['data']={}; + + var button = document.getElementById("CheckNewPrintersBtn"); + if (button) { + button.style.pointerEvents = "none"; + button.style.opacity = "0.6"; + } + + SendWXMessage( JSON.stringify(tSend) ); +} + function CreateNewPrinter() { var tSend={}; diff --git a/resources/web/guide/24/index.html b/resources/web/guide/24/index.html index efb6148e0d..5f0a2407cc 100644 --- a/resources/web/guide/24/index.html +++ b/resources/web/guide/24/index.html @@ -82,7 +82,8 @@
Create
-
Confirm
+
Check for new printers
+
Confirm
Cancel
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 2d0c6a5d8d..020fc0662d 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -42,6 +42,9 @@ namespace Slic3r { static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest"; static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile"; + +constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url"; + static const std::string MODELS_STR = "models"; const std::string AppConfig::SECTION_FILAMENTS = "filaments"; @@ -1814,7 +1817,10 @@ std::string AppConfig::version_check_url() const std::string AppConfig::profile_update_url() const { - return PROFILE_UPDATE_URL; + std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL); + if (orca_updater_url.empty()) + return PROFILE_UPDATE_URL; + return orca_updater_url; } bool AppConfig::exists() diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 6b58fffead..0820f7181b 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -562,6 +563,78 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt) m_ProfileJson["filament"][fName]["selected"] = 1; } } + else if (strCmd == "check_for_new_printers") { + json response = json::object(); + response["command"] = "check_new_printers_result"; + // Guide pages currently send sequence_id as a number, while older + // pages may send it as a string. Preserve the value without + // forcing either representation. + if (j.contains("sequence_id")) + response["sequence_id"] = j["sequence_id"]; + else + response["sequence_id"] = ""; + + if (!m_MainPtr->preset_updater) { + response["error"] = "Printer update service is unavailable."; + wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true)); + wxGetApp().CallAfter([this, strJS] { RunScript(strJS); }); + } else { + // Orca: enumerate vendors directly from disk rather than from m_ProfileJson["model"] + // — a vendor with no machine models (e.g. a filament-only bundle, or a test fixture + // like "test123" with an empty machine_model_list) never gets a "vendor" entry + // pushed into "model" by LoadProfileFamily(), so it would be invisible to the + // request body and get endlessly re-offered by the server. Scan both the system dir + // (already-installed vendors) and the bundled resources dir (shipped-but-not-yet- + // installed vendors), same as LoadProfileData() does when building loaded_vendors. + std::set system_vendors; + for (const auto& dir : {vendor_dir, rsrc_vendor_dir}) { + if (!boost::filesystem::exists(dir)) + continue; + for (const auto& entry : boost::filesystem::directory_iterator(dir)) { + if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json")) + system_vendors.insert(entry.path().stem().string()); + } + } + // Orca: check_new_vendors() is async (network + confirmation dialog + download + // all happen off the calling thread apart from the dialog itself); guard against + // this dialog being closed before the callback fires. + wxWeakRef weak_this(this); + try { + m_MainPtr->preset_updater->check_new_vendors( + system_vendors, [weak_this, response](std::vector installed_vendors, bool declined) mutable { + if (!weak_this) + return; + + // Orca: append the newly installed vendor(s) into the in-memory + // profile data (instead of a full LoadProfileData() rescan of every + // vendor) and push the refreshed list to the webview, the same way + // request_userguide_profile does, so the printer list picks them up + // without needing to reopen the guide. + for (const auto& vendor_id : installed_vendors) { + weak_this->LoadProfileFamily(vendor_id, (weak_this->vendor_dir / (vendor_id + ".json")).string()); + } + if (!installed_vendors.empty()) { + json profile_response = json::object(); + profile_response["command"] = "response_userguide_profile"; + profile_response["sequence_id"] = "10001"; + profile_response["response"] = weak_this->m_ProfileJson; + wxString profileJS = wxString::Format("HandleStudio(%s)", profile_response.dump(-1, ' ', true)); + weak_this->RunScript(profileJS); + } + + response["vendors"] = installed_vendors; + response["declined"] = declined; + wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true)); + weak_this->RunScript(strJS); + }); + } catch (const std::exception &e) { + BOOST_LOG_TRIVIAL(warning) << "Failed to check for new printers: " << e.what(); + response["error"] = "Failed to check for new printers."; + wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true)); + wxGetApp().CallAfter([this, strJS] { RunScript(strJS); }); + } + } + } else if (strCmd == "user_guide_finish") { SaveProfile(); @@ -1644,13 +1717,15 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath OneModel["materials"] = pm["default_materials"]; // wxString strCoverPath = wxString::Format("%s\\%s\\%s_cover.png", strFolder, strVendor, std::string(s1.mb_str())); - std::string cover_file = s1 + "_cover.png"; - boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file).make_preferred(); + std::string cover_file = s1 + "_cover.png"; + boost::filesystem::path cover_path = boost::filesystem::absolute(vendor_dir / cover_file).make_preferred(); + BOOST_LOG_TRIVIAL(info) << "[WebGuideDialog] " << cover_path; if (!boost::filesystem::exists(cover_path)) { - cover_path = - (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / - cover_file) - .make_preferred(); + cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file) + .make_preferred(); + if (!boost::filesystem::exists(cover_path)) + cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file) + .make_preferred(); } OneModel["cover"] = cover_path.string(); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 2de8716f47..42bd346222 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -129,10 +130,9 @@ struct Update //BBS: add directory support void install() const { - if (is_directory) { + if (is_directory) { copy_directory_recursively(source, target, file_filter); - } - else { + } else { copy_file_fix(source, target); // A vendor must be installed in exactly one form. Remove the @@ -147,7 +147,7 @@ struct Update fs::remove(target.parent_path() / (vendor + ".opc"), ec); } } - } + } friend std::ostream& operator<<(std::ostream& os, const Update &self) { @@ -195,6 +195,8 @@ struct Updates std::vector updates; }; +static bool reload_configs_update_gui(); + wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); @@ -222,6 +224,10 @@ struct PresetUpdater::priv // Per-vendor update checking std::set checked_vendors; + // Orca (PR #130): changelog text for each vendor, captured in memory during + // sync_vendor_config()/check_new_vendors() instead of written beside the cache. + std::unordered_map vendor_changelogs; + mutable std::mutex vendor_changelogs_mutex; std::vector vendor_check_threads; std::atomic vendor_check_cancel{false}; @@ -246,6 +252,8 @@ struct PresetUpdater::priv void parse_version_string(const std::string& body) const; void sync_resources(std::string http_url, std::map &resources, bool check_patch = false, std::string current_version="", std::string changelog_file=""); void sync_vendor_config(const std::string& vendor_id); + void check_new_vendors(const std::set& system_vendors, + std::function, bool)> callback); void sync_tooltip(std::string http_url, std::string language); void sync_plugins(std::string http_url, std::string plugin_version); void sync_printer_config(std::string http_url); @@ -686,6 +694,9 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION std::string download_url_str; + std::string changelog; + + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url; Http::get(url) .timeout_connect(5) @@ -698,9 +709,12 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) if (http_status != 200) return; try { json j = json::parse(body); + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body; + if (j.contains("vendor_version") && j.contains("download_url")) { online_version_str = j["vendor_version"].get(); download_url_str = j["download_url"].get(); + changelog = j.value("changelog", std::string()); } } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what(); @@ -722,8 +736,11 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) boost::system::error_code ec; fs::remove_all(cache_profile_path / vendor_id, ec); fs::remove(cache_profile_path / (vendor_id + ".json"), ec); - fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec); + // Orca: the OPC cache is the vendor's whole installation in one file; clear it too. fs::remove(cache_profile_path / (vendor_id + ".opc"), ec); + // Best-effort cleanup of the legacy on-disk changelog written by older builds + // (changelogs are now kept in memory - see vendor_changelogs). + fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec); // Download the zip BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading update for " << vendor_id @@ -761,6 +778,23 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) if (cancel || vendor_check_cancel) return; + const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json"); + const fs::path cached_vendor_folder = cache_profile_path / vendor_id; + if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) || + fs::is_empty(cached_vendor_folder)) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id + << ": expected " << vendor_id << ".json and a non-empty " + << vendor_id << " directory"; + fs::remove_all(cached_vendor_folder, ec); + fs::remove(cached_vendor_json, ec); + return; + } + + { + std::lock_guard lock(vendor_changelogs_mutex); + vendor_changelogs[vendor_id] = std::move(changelog); + } + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI"; GUI::wxGetApp().CallAfter([] { GUI::wxGetApp().check_config_updates_from_updater(); @@ -1167,6 +1201,14 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version if (!fs::exists(cache_profile_path)) return updates; + // Orca (PR #130): vendor changelogs are captured in memory during + // sync_vendor_config()/check_new_vendors(), not written beside the cache. + std::unordered_map changelogs; + { + std::lock_guard lock(vendor_changelogs_mutex); + changelogs = vendor_changelogs; + } + for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) { const auto &path = dir_entry.path(); std::string file_path = path.string(); @@ -1179,6 +1221,21 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME); auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME); + // Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty + // / preset directory; a stray or half-downloaded .json is + // skipped. An .opc cache is a single self-contained file (validated below), + // so this check does not apply to it. + if (!is_opc_file) { + const auto vendor_folder_in_cache = cache_profile_path / vendor_name; + if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) || + fs::is_empty(vendor_folder_in_cache)) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for " + << vendor_name << ": expected " << vendor_name + << ".json and a non-empty " << vendor_name << " directory"; + continue; + } + } + if (is_vendor_installed(vendor_name) || is_opc_file || fs::exists(print_in_cache) @@ -1212,15 +1269,9 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version cache_ver = *config_version; } - std::string changelog; - std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string(); - boost::nowide::ifstream ifs(changelog_file); - if (ifs) { - std::ostringstream oss; - oss<< ifs.rdbuf(); - changelog = oss.str(); - ifs.close(); - } + // Orca (PR #130): changelog for this vendor was captured in memory at sync time. + const auto changelog_it = changelogs.find(vendor_name); + std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string(); if (vendor_ver < cache_ver) { BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string() @@ -1362,6 +1413,9 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string // after the startup printer preset has been restored. this->p->sync_plugins(http_url, plugin_version); this->p->sync_printer_config(http_url); + // Orca (PR #130): the filament library is always installed, so refresh it + // from the updater on every startup sync rather than deferring to check_vendor_update(). + this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY); //if (p->cancel) // return; //remove the tooltip currently @@ -1394,6 +1448,302 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id) }); } +// Orca: ask the server which vendors from `system_vendors` have a profile bundle available that +// isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently +// installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps +// vendor id -> {version, download_url, changelog} for each vendor the server has an update for. +// Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config() +// does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates() +// flow then installs the cached profiles into data_dir()/system. +// +// Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract +// work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only +// the confirmation dialog (which must run on the UI thread) and the final callback are marshaled +// back via CallAfter(). +void PresetUpdater::priv::check_new_vendors(const std::set& system_vendors, + std::function, bool)> callback) +{ + vendor_check_threads.emplace_back([this, system_vendors, callback]() { + AppConfig* app_config = GUI::wxGetApp().app_config; + std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION); + + auto check_cancel = [this](Http::Progress, bool& cancel_http) { + if (cancel || vendor_check_cancel) + cancel_http = true; + }; + + json request_body = json::object(); + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:"; + for (const auto& vendor_id : system_vendors) { + // Orca: installed_vendor_version() reads whichever form the vendor is + // installed as - the .json profile or the .opc preset cache stamp - + // so a cache-only vendor is not reported as version 0.0.0 and then + // endlessly re-offered by the server. + Semver installed_ver = installed_vendor_version(vendor_id); + request_body[vendor_id] = installed_ver.to_string(); + BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")"; + } + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url; + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2); + + json response_json; + bool got_response = false; + + auto post = Http::post(url); + + post.timeout_connect(5); + post.on_progress(check_cancel); + post.header("Content-Type", "application/json"); + post.on_error([](std::string body, std::string error, unsigned http_status) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error; + }) + .on_complete([&response_json, &got_response](std::string body, unsigned http_status) { + if (http_status != 200) + return; + try { + json j = json::parse(body); + if (j.is_object()) { + response_json = std::move(j); + got_response = true; + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what(); + } + }); + + post.set_post_body(request_body.dump()); + post.perform_sync(); + + if (cancel || vendor_check_cancel) + return; + + if (!got_response) { + GUI::wxGetApp().CallAfter([callback]() { callback({}, false); }); + return; + } + + // Collect candidates before touching the filesystem or network again. + struct NewVendorCandidate + { + std::string vendor_id; + Semver version; + std::string download_url; + std::string changelog; + }; + std::vector candidates; + for (auto it = response_json.begin(); it != response_json.end(); ++it) { + const json& entry = it.value(); + if (!entry.is_object()) + continue; + std::string download_url_str = entry.value("download_url", std::string()); + if (download_url_str.empty()) + continue; + + NewVendorCandidate candidate; + candidate.vendor_id = it.key(); + auto parsed_ver = Semver::parse(entry.value("version", std::string())); + candidate.version = parsed_ver ? *parsed_ver : Semver(); + candidate.download_url = std::move(download_url_str); + candidate.changelog = entry.value("changelog", std::string()); + candidates.push_back(std::move(candidate)); + } + + if (candidates.empty()) { + GUI::wxGetApp().CallAfter([callback]() { callback({}, false); }); + return; + } + + // Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual + // download/install work is dispatched back onto a new background thread from there, + // same as check_vendor_update() does for a single vendor. + GUI::wxGetApp().CallAfter([this, candidates, callback]() { + std::vector updates_msg; + for (const auto& candidate : candidates) + updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog); + + GUI::MsgUpdateConfig dlg(updates_msg); + if (dlg.ShowModal() != wxID_OK) { + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors"; + callback({}, true); + return; + } + + // Orca: the actual download runs on a background thread below (so it doesn't block the + // UI), but that also means nothing visibly happens for the several seconds it can take + // (longer still if a retry kicks in) — push a notification so it's clear work is + // ongoing rather than looking hung. + { + std::string vendor_list; + for (const auto& candidate : candidates) { + if (!vendor_list.empty()) + vendor_list += ", "; + vendor_list += candidate.vendor_id; + } + GUI::wxGetApp().plater()->get_notification_manager()->push_notification( + _u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("...")); + } + + vendor_check_threads.emplace_back([this, candidates, callback]() { + auto check_cancel = [this](Http::Progress, bool& cancel_http) { + if (cancel || vendor_check_cancel) + cancel_http = true; + }; + + std::vector new_vendor_ids; + std::vector failed_vendor_ids; + auto cache_profile_path = cache_path / "profiles"; + fs::create_directories(cache_profile_path); + boost::system::error_code ec; + + for (const auto& candidate : candidates) { + if (cancel || vendor_check_cancel) + break; + + const std::string& vendor_id = candidate.vendor_id; + const std::string& download_url_str = candidate.download_url; + std::string changelog = candidate.changelog; + + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string(); + + // Clear only this vendor's cached data, same as sync_vendor_config(). + fs::remove_all(cache_profile_path / vendor_id, ec); + fs::remove(cache_profile_path / (vendor_id + ".json"), ec); + + fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION); + bool download_ok = false; + + // Orca: same retry pattern as Plater.cpp's project download — a single-shot + // 5s connect timeout against GitHub's redirect chain is prone to transient + // failures (DNS/connect hiccups) that succeed a moment later, so retry a few + // times before giving up rather than failing the whole vendor on one blip. + int retry_count = 0; + const int max_retries = 3; + bool keep_trying = true; + while (keep_trying && retry_count < max_retries) { + retry_count++; + Http::get(download_url_str) + .timeout_connect(5) + .on_progress(check_cancel) + .on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id + << " (attempt " << retry_count << "/" << max_retries << "): " << error; + }) + .on_complete([&](std::string body, unsigned http_status) { + if (http_status != 200) + return; + fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc); + if (!file.good()) + return; + file.write(body.c_str(), body.size()); + file.close(); + if (file.good()) + download_ok = true; + }) + .perform_sync(); + + keep_trying = !download_ok && !(cancel || vendor_check_cancel); + } + + if (!download_ok || cancel || vendor_check_cancel) { + if (!download_ok) + failed_vendor_ids.push_back(vendor_id); + continue; + } + + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id; + if (!extract_file(download_file, cache_profile_path)) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id; + fs::remove(download_file, ec); + failed_vendor_ids.push_back(vendor_id); + continue; + } + fs::remove(download_file, ec); + + const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json"); + const fs::path cached_vendor_folder = cache_profile_path / vendor_id; + if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) || + fs::is_empty(cached_vendor_folder)) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id + << ".json and a non-empty " << vendor_id << " directory"; + fs::remove_all(cached_vendor_folder, ec); + fs::remove(cached_vendor_json, ec); + failed_vendor_ids.push_back(vendor_id); + continue; + } + + { + std::lock_guard lock(vendor_changelogs_mutex); + vendor_changelogs[vendor_id] = std::move(changelog); + } + + new_vendor_ids.push_back(vendor_id); + } + + if (!new_vendor_ids.empty()) { + // Orca: the user already confirmed via the dialog above, so install right away + // instead of routing through check_config_updates_from_updater(), which only + // queues a passive notification (meant for the silent background per-vendor + // check) requiring yet another click + confirmation before anything is copied + // into data_dir()/system. + GUI::wxGetApp().CallAfter([this, new_vendor_ids] { + AppConfig* app_config = GUI::wxGetApp().app_config; + Updates updates = get_config_updates(app_config->orig_version()); + + // Only install the vendors just confirmed; leave any other unrelated + // pending cached update (from a background sync_vendor_config()) alone, + // still gated behind its own notification/confirmation. + std::set confirmed(new_vendor_ids.begin(), new_vendor_ids.end()); + Updates filtered; + for (auto& update : updates.updates) + if (confirmed.count(update.vendor)) + filtered.updates.push_back(std::move(update)); + + if (filtered.updates.empty()) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected"; + return; + } + + if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) { + BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors"; + return; + } + + BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed"; + for (const auto& vendor_id : new_vendor_ids) { + Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id); + GUI::wxGetApp().plater()->get_notification_manager()->push_notification( + GUI::NotificationType::PresetUpdateFinished, + GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel, + _u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string()); + } + }); + } + + if (!failed_vendor_ids.empty()) { + GUI::wxGetApp().CallAfter([failed_vendor_ids] { + std::string vendor_list; + for (const auto& vendor_id : failed_vendor_ids) { + if (!vendor_list.empty()) + vendor_list += ", "; + vendor_list += vendor_id; + } + GUI::wxGetApp().plater()->get_notification_manager()->push_notification( + _u8L("Failed to download vendor profile(s): ") + vendor_list); + }); + } + + GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); }); + }); + }); + }); +} + +void PresetUpdater::check_new_vendors(const std::set& system_vendors, + std::function, bool)> callback) +{ + p->check_new_vendors(system_vendors, std::move(callback)); +} + void PresetUpdater::slic3r_update_notify() { if (! p->enabled_version_check) @@ -1415,10 +1765,9 @@ static bool reload_configs_update_gui() GUI::wxGetApp().load_current_presets(); GUI::wxGetApp().plater()->set_bed_shape(); - return true; + return true; } - PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const { if (! p->enabled_config_update) { return R_NOOP; } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 27ec6748f2..ccb0902fd5 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -1,7 +1,9 @@ #ifndef slic3r_PresetUpdate_hpp_ #define slic3r_PresetUpdate_hpp_ +#include #include +#include #include #include @@ -59,6 +61,12 @@ public: void on_update_notification_confirm(); void do_printer_config_update(); void check_vendor_update(const std::string& vendor_id); + // Orca: async, mirrors check_vendor_update()/sync_vendor_config() — the network query and any + // download/install work happen on a background thread; only the confirmation dialog runs on + // the UI thread. `callback` is invoked on the UI thread with the ids of vendors that were + // installed (empty if none were found, or the user declined) and whether the user declined. + void check_new_vendors(const std::set& system_vendors, + std::function installed_vendors, bool declined)> callback); bool version_check_enabled() const; From 684cd37f8dd94e451d9013f065764b007ef6cd12 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 31 Aug 2026 18:01:15 +0800 Subject: [PATCH 40/60] UI Fixes and Polish --- src/slic3r/GUI/FilamentBitmapUtils.cpp | 385 +++++++++++++++++++++++ src/slic3r/GUI/FilamentBitmapUtils.hpp | 78 +++++ src/slic3r/GUI/GradientCurveEditor.cpp | 231 +++----------- src/slic3r/GUI/MixedFilamentDialog.cpp | 94 +----- src/slic3r/GUI/MixedFilamentDialog.hpp | 4 - src/slic3r/GUI/PublishSettingsDialog.cpp | 330 ++++++------------- src/slic3r/GUI/Widgets/TabCtrl.cpp | 2 +- 7 files changed, 622 insertions(+), 502 deletions(-) diff --git a/src/slic3r/GUI/FilamentBitmapUtils.cpp b/src/slic3r/GUI/FilamentBitmapUtils.cpp index 23b14c385b..3af3d1ad01 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.cpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.cpp @@ -1,11 +1,22 @@ #include +#include #include +#include +#include #include #include +#include +#include +#include +#include #include "EncodedFilament.hpp" #include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" +#include "GuiColor.hpp" +#include "I18N.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/StateColor.hpp" #include "libslic3r/FilamentMixer.hpp" #include "libslic3r/PrintConfig.hpp" @@ -488,4 +499,378 @@ void recompute_mixed_slot_colors(std::vector& colors, } } +namespace { + +// Layout ratios of the gradient plot rect, copied from GradientCurveEditor so the read-only +// preview and the interactive editor stay pixel-identical. Plot rect is square 1:1; the +// right/bottom margins host the axis arrows and labels. +constexpr double kPlotLeftRatio = 0.0316; +constexpr double kPlotRightRatio = 0.6766; +constexpr double kPlotTopRatio = 0.1529; +constexpr double kPlotBottomRatio = 0.8474; +constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. +constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling) +constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) +constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) +constexpr int kPointRadius = 4; // anchor outer radius (DIP) +constexpr float kBgSimilarThreshold = 15.0f; +constexpr int kOutlineExtraDip = 2; +constexpr double kTriangleMarginDip = 20.0; + +// Quadratic blend that never goes out of gamut, matching MixedFilamentDialog::blend_colors. +wxColour lerp_blend(const wxColour& a, const wxColour& b, double ratio_a) +{ + unsigned char r, g, bl; + Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(), + b.Red(), b.Green(), b.Blue(), + static_cast(1.0 - ratio_a), &r, &g, &bl); + return wxColour(r, g, bl); +} + +// DIP conversion for these free functions: unlike the wxWindow member FromDIP, it needs the +// window parameter explicitly; nullptr picks the app's default DPI like the Publish dialog does. +int dip_px(int v) { return wxWindow::FromDIP(v, nullptr); } + +} // namespace + +wxRect mixed_gradient_plot_rect(const wxSize& sz) +{ + const int x = static_cast(std::lround(sz.x * kPlotLeftRatio)); + const int y = static_cast(std::lround(sz.y * kPlotTopRatio)); + const int x2 = static_cast(std::lround(sz.x * kPlotRightRatio)); + const int y2 = static_cast(std::lround(sz.y * kPlotBottomRatio)); + const int side = std::max(1, std::min(x2 - x, y2 - y)); + return wxRect(x, y, side, side); +} + +void draw_mixed_gradient_plot(wxDC& raw_dc, const wxSize& canvas, + const std::vector& curves, + const std::vector& anchors, + const MixedGradientTheme& theme) +{ + // Draw into an internal opaque buffer so wxGCDC text/curves anti-alias against a solid + // background (never a transparent one), then blit the finished image onto the caller's + // buffered paint DC. wxGCDC cannot wrap a generic wxDC&, so the buffer is always a + // wxMemoryDC -- the one type wxGCDC accepts on every platform. + if (canvas.x <= 0 || canvas.y <= 0) + return; + const wxRect rc = mixed_gradient_plot_rect(canvas); + if (rc.width <= 0 || rc.height <= 0) + return; + + wxBitmap buf(canvas); + wxMemoryDC memdc(buf); + memdc.SetBackground(wxBrush(theme.background)); + memdc.Clear(); + wxGCDC dc(memdc); + wxGraphicsContext* gc = dc.GetGraphicsContext(); + + // 10x10 light grid (10 lines including outer borders, 9 equal divisions). + dc.SetPen(wxPen(theme.grid, 1)); + for (int i = 0; i <= kGridDivisions; ++i) { + const int x = rc.x + rc.width * i / kGridDivisions; + const int y = rc.y + rc.height * i / kGridDivisions; + dc.DrawLine(x, rc.y, x, rc.y + rc.height); + dc.DrawLine(rc.x, y, rc.x + rc.width, y); + } + + // Set the label font first so text width measurements drive arrow / label placement. + wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); + label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); + dc.SetFont(label_font); + + const wxString axis_y_title = _L("Material Ratio"); + const wxString axis_x_title = _L("Model Height"); + const wxString pct_text = wxT("100%"); + const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); + const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); + + wxFont strong_font = label_font; + strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); + dc.SetFont(strong_font); + const wxSize pct_text_sz = dc.GetTextExtent(pct_text); + dc.SetFont(label_font); + + // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the + // canvas top edge; X-axis extends past the plot right toward the canvas right edge. + const int arrow_half = dip_px(kAxisArrowHalf); + const int arrow_len = dip_px(kAxisArrowLen); + dc.SetPen(wxPen(theme.axis, kStrokeAxis)); + dc.SetBrush(wxBrush(theme.axis)); + + const int y_axis_x = rc.x; + const int y_title_pct_gap = dip_px(1); + const int y_title_bottom_pad = dip_px(2); + const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); + const int y_arrow_tip_y = y_title_y; + const int y_arrow_ty = y_arrow_tip_y + arrow_len; + dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); + { + wxPoint tri[3] = { + wxPoint(y_axis_x, y_arrow_tip_y), + wxPoint(y_axis_x - arrow_half, y_arrow_ty), + wxPoint(y_axis_x + arrow_half, y_arrow_ty), + }; + dc.DrawPolygon(3, tri); + } + + const int x_axis_y = rc.y + rc.height; + const int x_label_gap = dip_px(4); + const int x_edge_pad = dip_px(6); + const int x_arrow_ideal = rc.x + rc.width + dip_px(10); + const int x_arrow_max = canvas.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; + const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, std::min(x_arrow_ideal, x_arrow_max)); + const int x_arrow_tip_x = x_arrow_tx + arrow_len; + const int x_title_x = x_arrow_tip_x + x_label_gap; + dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); + { + wxPoint tri[3] = { + wxPoint(x_arrow_tip_x, x_axis_y), + wxPoint(x_arrow_tx, x_axis_y - arrow_half), + wxPoint(x_arrow_tx, x_axis_y + arrow_half), + }; + dc.DrawPolygon(3, tri); + } + + // Labels: "Material Ratio" and the leading "100%" share the same left x; the trailing + // "Model Height" follows the X-axis arrow tip (already clamped to make room). + const int label_left_x = y_axis_x + dip_px(10); + dc.SetTextForeground(theme.label); + dc.DrawText(axis_y_title, label_left_x, y_title_y); + + dc.SetFont(strong_font); + dc.SetTextForeground(theme.label_strong); + dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); + + dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); + dc.SetFont(label_font); + dc.SetTextForeground(theme.label); + dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); + + if (!gc) + return; + + // Outline only when the curve colour is perceptually close to the background; otherwise the + // plain filament colour reads fine and the extra stroke would look heavy. + auto needs_outline = [&](const wxColour& c) { + return calc_color_distance(c, theme.background) < kBgSimilarThreshold; + }; + + // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint + // and would quantize the curve back to whole pixels. The pen is still set on the dc, which + // forwards it here while keeping its own cached state in sync for later dc drawing. + auto draw_polyline = [&](const MixedGradientCurve& curve) { + if (curve.points.size() < 2) + return; + dc.SetPen(wxPen(curve.colour, dip_px(curve.stroke_dip))); + gc->StrokeLines(curve.points.size(), curve.points.data()); + }; + + for (const MixedGradientCurve& curve : curves) { + if (needs_outline(curve.colour)) + draw_polyline({curve.points, theme.outline, curve.stroke_dip + kOutlineExtraDip}); + draw_polyline(curve); + } + + // Control points: hollow circle with axis-colour border, theme-aware fill, drawn with a + // sub-pixel centre so the ring stays centred on the curve. + if (!anchors.empty()) { + const double r = dip_px(kPointRadius); + dc.SetPen(wxPen(theme.axis, 1)); + dc.SetBrush(wxBrush(theme.point_fill)); + for (const wxPoint2DDouble& p : anchors) + gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + } + + memdc.SelectObject(wxNullBitmap); + raw_dc.DrawBitmap(buf, 0, 0); +} + +void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first, + const wxColour& second, double second_fraction) +{ + if (rect.width <= 0 || rect.height <= 0) + return; + for (int x = 0; x < rect.width; ++x) { + const double t = rect.width > 1 ? double(x) / rect.width : 0.0; + const wxColour c = lerp_blend(first, second, 1.0 - t); + dc.SetPen(wxPen(c)); + dc.DrawLine(rect.x + x, rect.y, rect.x + x, rect.y + rect.height); + } + + // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over + // blended filament colour, so it has to keep its contrast against data rather than chrome. + const int div_x = rect.x + static_cast(second_fraction * rect.width); + dc.SetPen(wxPen(wxColour(80, 80, 80), dip_px(4))); + dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height); + dc.SetPen(wxPen(*wxWHITE, dip_px(2))); + dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height); +} + +void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector& colours, + const std::vector& shares) +{ + const size_t n = std::min(colours.size(), shares.size()); + if (n == 0 || rect.width <= 0 || rect.height <= 0) + return; + std::vector norm = shares; + double total = 0.0; + for (double s : norm) + total += s; + if (total <= 0.0) { + norm.assign(n, 1.0 / n); + total = 1.0; + } + auto share_to_px = [&](double share_sum) { return rect.x + int(std::lround(share_sum / total * double(rect.width))); }; + int x0 = rect.x; + std::vector segs(n); + for (size_t i = 0; i < n; ++i) { + int x1 = rect.x + rect.width; + if (i + 1 < n) + x1 = share_to_px(std::accumulate(norm.begin(), norm.begin() + i + 1, 0.0)); + segs[i] = wxRect(x0, rect.y, std::max(1, x1 - x0), rect.height); + x0 = segs[i].GetRight() + 1; + } + for (size_t i = 0; i < n; ++i) { + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(colours[i])); + dc.DrawRectangle(segs[i]); + } + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1)); + dc.DrawRectangle(rect); +} + +namespace { + +struct TriCacheKey +{ + int w, h; + int c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b; + int bg_r, bg_g, bg_b, ol_r, ol_g, ol_b; + bool operator<(const TriCacheKey& o) const + { + return std::tie(w, h, c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b, bg_r, bg_g, bg_b, ol_r, ol_g, ol_b) < + std::tie(o.w, o.h, o.c0r, o.c0g, o.c0b, o.c1r, o.c1g, o.c1b, o.c2r, o.c2g, o.c2b, o.bg_r, o.bg_g, o.bg_b, o.ol_r, o.ol_g, o.ol_b); + } +}; + +std::map& tri_cache() +{ + static std::map cache; + return cache; +} + +} // namespace + +std::array mixed_triangle_vertices(const wxSize& size, double margin_dip) +{ + const double pw = size.GetWidth(), ph = size.GetHeight(); + const double margin = dip_px(int(margin_dip)); + const double avail = std::min(pw, ph) - 2.0 * margin; + const double side = avail; + const double tri_h = side * std::sqrt(3.0) / 2.0; + const double cx = pw / 2.0; + const double top_y = (ph - tri_h) / 2.0; + return {{{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}}; +} + +void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array& colours, + const std::array& weights, const MixedTriangleTheme& theme) +{ + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + return; + const std::array v = mixed_triangle_vertices(size, kTriangleMarginDip); + + dc.SetBrush(wxBrush(theme.background)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight()); + + const wxColour& c0 = colours[0]; + const wxColour& c1 = colours[1]; + const wxColour& c2 = colours[2]; + const TriCacheKey key{size.GetWidth(), size.GetHeight(), + c0.Red(), c0.Green(), c0.Blue(), + c1.Red(), c1.Green(), c1.Blue(), + c2.Red(), c2.Green(), c2.Blue(), + theme.background.Red(), theme.background.Green(), theme.background.Blue(), + theme.outline.Red(), theme.outline.Green(), theme.outline.Blue()}; + + wxBitmap& bmp = tri_cache()[key]; + if (!bmp.IsOk()) { + bmp = wxBitmap(size.GetWidth(), size.GetHeight(), 24); + wxMemoryDC mdc(bmp); + mdc.SetBrush(wxBrush(theme.background)); + mdc.SetPen(*wxTRANSPARENT_PEN); + mdc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight()); + + const int min_y = int(std::min({v[0].y, v[1].y, v[2].y})); + const int max_y = int(std::max({v[0].y, v[1].y, v[2].y})); + const int min_x = int(std::min({v[0].x, v[1].x, v[2].x})); + const int max_x = int(std::max({v[0].x, v[1].x, v[2].x})); + for (int py = min_y; py <= max_y; ++py) { + for (int px = min_x; px <= max_x; ++px) { + const TriPoint p = {double(px), double(py)}; + if (!tri_contains(p, v[0], v[1], v[2])) + continue; + double w0, w1, w2; + tri_barycentric(p, v[0], v[1], v[2], w0, w1, w2); + unsigned char mr, mg, mb; + if (w0 + w1 > 1e-6) { + float t01 = float(w1 / (w0 + w1)); + Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, &mb); + Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), float(w2), &mr, &mg, &mb); + } else { + mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); + } + mdc.SetPen(wxPen(wxColour(mr, mg, mb))); + mdc.DrawPoint(px, py); + } + } + + mdc.SetPen(wxPen(theme.outline, 1)); + mdc.SetBrush(*wxTRANSPARENT_BRUSH); + const wxPoint pts[3] = {{int(v[0].x), int(v[0].y)}, {int(v[1].x), int(v[1].y)}, {int(v[2].x), int(v[2].y)}}; + mdc.DrawPolygon(3, pts); + mdc.SelectObject(wxNullBitmap); + + // Keep the cache from growing without bound across DPI/size changes. + if (tri_cache().size() > 6) { + auto& cache = tri_cache(); + cache.erase(cache.begin()); + } + } + dc.DrawBitmap(bmp, 0, 0); + + // Published-ratio marker (read-only twin of the editor's drag handle). + const double w0 = weights[0], w1 = weights[1], w2 = weights[2]; + const int hx = int(w0 * v[0].x + w1 * v[1].x + w2 * v[2].x); + const int hy = int(w0 * v[0].y + w1 * v[1].y + w2 * v[2].y); + dc.SetBrush(*wxWHITE_BRUSH); + dc.SetPen(wxPen(theme.ring, dip_px(2))); + dc.DrawCircle(hx, hy, dip_px(5)); +} + +void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array& weights, + const MixedTriangleTheme& theme) +{ + const std::array v = mixed_triangle_vertices(size, kTriangleMarginDip); + dc.SetFont(::Label::Body_12); + dc.SetTextForeground(theme.label); + + // "Ratio" title, sitting above the top vertex. + const wxString title = _L("Ratio"); + dc.DrawText(title, dip_px(2), std::max(0, int(v[0].y - dc.GetTextExtent(title).GetHeight() - dip_px(4)))); + + for (int i = 0; i < 3; ++i) { + const wxString text = wxString::Format("%d%%", int(std::lround(weights[i] * 100.0))); + const wxSize tsz = dc.GetTextExtent(text); + int lx = int(v[i].x - tsz.GetWidth() / 2.0); + int ly = (i == 0) ? int(v[i].y - tsz.GetHeight() - dip_px(4)) : int(v[i].y + dip_px(3)); + ly = std::clamp(ly, 0, size.GetHeight() - tsz.GetHeight()); + lx = std::clamp(lx, 0, size.GetWidth() - tsz.GetWidth()); + dc.DrawText(text, lx, ly); + } +} + }} // namespace Slic3r::GUI \ No newline at end of file diff --git a/src/slic3r/GUI/FilamentBitmapUtils.hpp b/src/slic3r/GUI/FilamentBitmapUtils.hpp index fc6eb1f9dd..687d34a825 100644 --- a/src/slic3r/GUI/FilamentBitmapUtils.hpp +++ b/src/slic3r/GUI/FilamentBitmapUtils.hpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include // Orca: forward-declare so the header is self-contained outside libslic3r_gui's @@ -79,6 +82,81 @@ wxBitmap create_gradient_ramp_bitmap(const std::vector& ramp, const wx void recompute_mixed_slot_colors(std::vector& colors, const Slic3r::DynamicPrintConfig& cfg); +// --- Gradient plot (shared by GradientCurveEditor and the Publish dialog's read-only +// preview). The plot is a square 1:1 rect laid out with the editor's ratios so both +// render identically; curves are drawn as sub-pixel anti-aliased polylines through +// wxGCDC so they never quantize to whole pixels. + +// One curve of the plot: screen-space sub-pixel points already mapped into the plot +// rect, the stroke colour and the stroke width in DIP. +struct MixedGradientCurve +{ + std::vector points; + wxColour colour; + int stroke_dip; +}; + +// Theme tokens, resolved by the caller through StateColor::darkModeColorFor. +struct MixedGradientTheme +{ + wxColour background; // for near-background outline detection + wxColour grid; // grid line + wxColour axis; // axis + arrow fill + wxColour label; // "Material Ratio" / "Model Height" + wxColour label_strong; // "100%" + wxColour outline; // near-background curve lift + wxColour point_fill; // anchor fill +}; + +// Square 1:1 plot rect inside `canvas`, using the editor's plot ratios. +wxRect mixed_gradient_plot_rect(const wxSize& canvas); + +// Draw the whole plot (grid, axes + arrowheads, axis labels, each curve with an optional +// near-background outline, and anchor circles). `anchors` are empty when the caller has +// none to show. `dc` is the caller's buffered paint DC; a wxGCDC is created inside so the +// geometry gets anti-aliased. +void draw_mixed_gradient_plot(wxDC& dc, const wxSize& canvas, + const std::vector& curves, + const std::vector& anchors, + const MixedGradientTheme& theme); + +// --- Ratio bar (2-component continuous blend + divider, matching MixedFilamentDialog). +// Colours blend first->second across the rect; the divider marks `second_fraction` of the +// rect's width (the second component's share, 0..1). +void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first, + const wxColour& second, double second_fraction); + +// Fallback ratio bar for N>2 non-gradient slots: one solid segment per component, +// widths proportional to shares. Label text (the "NN%" inside wide-enough segments) is +// the caller's concern. +void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector& colours, + const std::vector& shares); + +// --- Triangle picker (3-component), shared by MixedFilamentDialog and the Publish preview. +struct MixedTriangleTheme +{ + wxColour background; + wxColour outline; // triangle border + wxColour ring; // drag-handle ring + wxColour label; // "Ratio" title + per-vertex labels +}; + +// The three vertices of the read-only/miniature triangle inside a `size` square panel, +// with `margin_dip` inset. Order: top, bottom-left, bottom-right. +std::array mixed_triangle_vertices(const wxSize& size, double margin_dip = 20.0); + +// Draw background, the cached barycentric fill, the outline and the drag-handle marker. +// `weights` are the three barycentric shares (sum 1). The "Ratio" title and per-vertex +// percentage labels are drawn by the caller so the interactive editor can keep its own +// live child labels while the read-only preview draws them as text. +void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array& colours, + const std::array& weights, const MixedTriangleTheme& theme); + +// Draw the "Ratio" title plus one "NN%" label per vertex (used by the read-only preview; +// the interactive editor positions its own live labels instead). +void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array& weights, + const MixedTriangleTheme& theme); + }} // namespace Slic3r::GUI #endif // slic3r_GUI_FilamentBitmapUtils_hpp_ \ No newline at end of file diff --git a/src/slic3r/GUI/GradientCurveEditor.cpp b/src/slic3r/GUI/GradientCurveEditor.cpp index 5b1073d231..6d38ef81a5 100644 --- a/src/slic3r/GUI/GradientCurveEditor.cpp +++ b/src/slic3r/GUI/GradientCurveEditor.cpp @@ -1,4 +1,5 @@ #include "GradientCurveEditor.hpp" +#include "FilamentBitmapUtils.hpp" #include "GUI_App.hpp" #include "GuiColor.hpp" #include "I18N.hpp" @@ -19,23 +20,13 @@ namespace GUI { wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent); namespace { -// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing. -// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels. -constexpr double kPlotLeftRatio = 0.0316; -constexpr double kPlotRightRatio = 0.6766; -constexpr double kPlotTopRatio = 0.1529; -constexpr double kPlotBottomRatio = 0.8474; -constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders. - -// Hit / stroke (DIP). +// Hit / stroke (DIP). The plot-rect ratios, grid divisions, axis/arrow geometry and the +// near-background outline threshold now live in FilamentBitmapUtils so the read-only Publish +// preview and this editor stay pixel-identical. constexpr int kHitRadius = 6; constexpr int kCurveHitRadius = 5; -constexpr int kPointRadius = 4; // anchor outer radius (DIP) constexpr int kStrokeUnselected = 2; constexpr int kStrokeSelected = 4; -constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention) -constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP) -constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP) // Light-mode design tokens. Resolved through StateColor::darkModeColorFor() // at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B -> @@ -46,12 +37,6 @@ const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700 const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700 const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900 const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements - -// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve -// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than -// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline. -constexpr float kBgSimilarThreshold = 15.0f; -constexpr int kOutlineExtraDip = 2; } // namespace GradientCurveEditor::GradientCurveEditor(wxWindow* parent, @@ -177,15 +162,8 @@ void GradientCurveEditor::emit_changed() wxRect GradientCurveEditor::plot_rect() const { - const wxSize sz = GetClientSize(); - const int x = static_cast(std::lround(sz.x * kPlotLeftRatio)); - const int y = static_cast(std::lround(sz.y * kPlotTopRatio)); - const int x2 = static_cast(std::lround(sz.x * kPlotRightRatio)); - const int y2 = static_cast(std::lround(sz.y * kPlotBottomRatio)); - // Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at - // the top-left so the "100%" labels on the bottom/right still align with the plot edges. - const int side = std::max(1, std::min(x2 - x, y2 - y)); - return wxRect(x, y, side, side); + // Square 1:1 plot, shared with the Publish dialog's read-only preview. + return mixed_gradient_plot_rect(GetClientSize()); } wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const @@ -330,171 +308,52 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/) raw_dc.SetBackground(wxBrush(bg)); raw_dc.Clear(); - // Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered - // DC is the actual back buffer that gets blitted to the window. - wxGCDC dc(raw_dc); - // The curve and its anchors are drawn straight on the graphics context so their - // coordinates stay sub-pixel accurate (see data_to_px_f). - wxGraphicsContext* gc = dc.GetGraphicsContext(); - - const wxRect rc = plot_rect(); - if (rc.width <= 0 || rc.height <= 0) - return; - - // 10x10 light grid (10 lines including outer borders, 9 equal divisions). - dc.SetPen(wxPen(grid_color, 1)); - for (int i = 0; i <= kGridDivisions; ++i) { - const int x = rc.x + rc.width * i / kGridDivisions; - const int y = rc.y + rc.height * i / kGridDivisions; - dc.DrawLine(x, rc.y, x, rc.y + rc.height); - dc.DrawLine(rc.x, y, rc.x + rc.width, y); - } - - // Set the label font first so text width measurements drive arrow / label placement. - wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); - dc.SetFont(label_font); - - const wxString axis_y_title = _L("Material Ratio"); - const wxString axis_x_title = _L("Model Height"); - const wxString pct_text = wxT("100%"); - const wxSize x_title_sz = dc.GetTextExtent(axis_x_title); - const wxSize y_title_sz = dc.GetTextExtent(axis_y_title); - - wxFont strong_font = label_font; - strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD); - dc.SetFont(strong_font); - const wxSize pct_text_sz = dc.GetTextExtent(pct_text); - dc.SetFont(label_font); - - // Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the - // canvas top edge; X-axis extends past the plot right toward the canvas right edge. - const int arrow_half = FromDIP(kAxisArrowHalf); - const int arrow_len = FromDIP(kAxisArrowLen); - const wxSize sz = GetClientSize(); - dc.SetPen(wxPen(axis_color, kStrokeAxis)); - dc.SetBrush(wxBrush(axis_color)); - - // Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom. - const int y_axis_x = rc.x; - const int y_title_pct_gap = FromDIP(1); - const int y_title_bottom_pad = FromDIP(2); - const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad); - const int y_arrow_tip_y = y_title_y; - const int y_arrow_ty = y_arrow_tip_y + arrow_len; - dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height); - { - wxPoint tri[3] = { - wxPoint(y_axis_x, y_arrow_tip_y), - wxPoint(y_axis_x - arrow_half, y_arrow_ty), - wxPoint(y_axis_x + arrow_half, y_arrow_ty), + // Render the plot (grid, axes, labels, curves, anchors) through the shared painter so the + // interactive editor and the Publish dialog's read-only preview stay pixel-identical. The + // curves are handed over as sub-pixel polylines and anti-alias inside the helper. + std::vector curves; + std::vector anchors; + if (m_points.size() >= 2) { + auto color_for_curve = [&](int curve_idx) -> wxColour { + wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; + // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; }; - dc.DrawPolygon(3, tri); - } - // X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing - // "Material Ratio" label still fits inside the canvas without overlapping the arrow. - const int x_axis_y = rc.y + rc.height; - const int x_label_gap = FromDIP(4); - const int x_edge_pad = FromDIP(6); - const int x_arrow_ideal = rc.x + rc.width + FromDIP(10); - const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len; - const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, - std::min(x_arrow_ideal, x_arrow_max)); - const int x_arrow_tip_x = x_arrow_tx + arrow_len; - const int x_title_x = x_arrow_tip_x + x_label_gap; - dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y); - { - wxPoint tri[3] = { - wxPoint(x_arrow_tip_x, x_axis_y), - wxPoint(x_arrow_tx, x_axis_y - arrow_half), - wxPoint(x_arrow_tx, x_axis_y + arrow_half), + auto build_polyline = [&](int curve_idx) -> std::vector { + const int samples = std::max(128, plot_rect().width * 2); + std::vector poly; + poly.reserve(samples + 1); + for (int s = 0; s <= samples; ++s) { + const double x = double(s) / samples; + const double y0 = sample_curve_y(x); + const double vy = to_visual_y(curve_idx, y0); + poly.push_back(data_to_px_f(x, vy)); + } + return poly; }; - dc.DrawPolygon(3, tri); - } - // Labels. - // "Model Height" and "100%" share the same left x; the gap is larger than the - // axis-arrow half-base so the text never visually touches the Y-axis arrow. - const int label_left_x = y_axis_x + FromDIP(10); - dc.SetTextForeground(label_muted); - dc.DrawText(axis_y_title, label_left_x, y_title_y); - - dc.SetFont(strong_font); - dc.SetTextForeground(label_strong); - dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap); - - // Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the - // X-axis arrow tip (placement was already clamped above to leave room). - dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y); - dc.SetFont(label_font); - dc.SetTextForeground(label_muted); - dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2); - - if (m_points.size() < 2 || !gc) - return; - - auto color_for_curve = [&](int curve_idx) -> wxColour { - wxColour c = (curve_idx == 0) ? m_color_low : m_color_high; - // Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible. - // Lift alpha so the curve stays visible while still hinting at transparency. - if (c.Alpha() == 0) - c.Set(c.Red(), c.Green(), c.Blue(), 150); - return c; - }; - - auto build_polyline = [&](int curve_idx) -> std::vector { - const int samples = std::max(128, rc.width * 2); - std::vector poly; - poly.reserve(samples + 1); - for (int s = 0; s <= samples; ++s) { - const double x = double(s) / samples; - const double y0 = sample_curve_y(x); - const double vy = to_visual_y(curve_idx, y0); - poly.push_back(data_to_px_f(x, vy)); + // Draw unselected first so the selected curve sits on top. + const int other = 1 - m_selected_curve; + for (const int idx : {other, m_selected_curve}) { + std::vector pts = build_polyline(idx); + if (pts.empty()) + continue; + curves.push_back({std::move(pts), color_for_curve(idx), idx == m_selected_curve ? kStrokeSelected : kStrokeUnselected}); } - return poly; - }; - // Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint - // and would quantize the curve back to whole pixels. The pen is still set on the dc, which - // forwards it here while keeping its own cached state in sync for later dc drawing. - auto draw_polyline = [&](const std::vector& poly, const wxColour& col, int stroke_dip) { - dc.SetPen(wxPen(col, FromDIP(stroke_dip))); - gc->StrokeLines(poly.size(), poly.data()); - }; - - // Outline only when the curve color is perceptually close to the background; otherwise - // the plain filament color reads fine and the extra stroke would look heavy. - auto needs_outline = [&](const wxColour& c) { - return calc_color_distance(c, bg) < kBgSimilarThreshold; - }; - - auto draw_one = [&](int curve_idx, int stroke_dip) { - const auto poly = build_polyline(curve_idx); - const wxColour col = color_for_curve(curve_idx); - if (needs_outline(col)) - draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip); - draw_polyline(poly, col, stroke_dip); - }; - - // Draw unselected first so the selected curve sits on top. - const int other = 1 - m_selected_curve; - draw_one(other, kStrokeUnselected); - draw_one(m_selected_curve, kStrokeSelected); - - // Control points (selected curve only): hollow circle with axis-color border, theme-aware fill. - // Drawn on the graphics context with a sub-pixel center so the ring stays centered on the - // curve instead of drifting up to half a pixel off it; pen and brush go through the dc for - // the same reason as in draw_polyline above. - const double r = FromDIP(kPointRadius); - dc.SetPen(wxPen(axis_color, 1)); - dc.SetBrush(wxBrush(point_fill)); - for (size_t i = 0; i < m_points.size(); ++i) { - const double vy = to_visual_y(m_selected_curve, m_points[i].y); - const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy); - gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2); + // Control points (selected curve only). + anchors.reserve(m_points.size()); + for (size_t i = 0; i < m_points.size(); ++i) { + const double vy = to_visual_y(m_selected_curve, m_points[i].y); + anchors.push_back(data_to_px_f(m_points[i].x, vy)); + } } + + const MixedGradientTheme theme{bg, grid_color, axis_color, label_muted, label_strong, outline_color, point_fill}; + draw_mixed_gradient_plot(raw_dc, GetClientSize(), curves, anchors, theme); } void GradientCurveEditor::on_left_down(wxMouseEvent& evt) diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index c1cbcc7450..a3047aad34 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -847,23 +847,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider() m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) { wxBufferedPaintDC dc(m_ratio_bar); wxSize sz = m_ratio_bar->GetClientSize(); - - wxColour col_a = comp_colour(0), col_b = comp_colour(1); - - for (int x = 0; x < sz.GetWidth(); ++x) { - double t = (double)x / sz.GetWidth(); - wxColour c = blend_colors(col_a, col_b, 1.0 - t); - dc.SetPen(wxPen(c)); - dc.DrawLine(x, 0, x, sz.GetHeight()); - } - - int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth()); - // Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over - // blended filament colour, so it has to keep its contrast against data rather than chrome. - dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4))); - dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); - dc.SetPen(wxPen(*wxWHITE, FromDIP(2))); - dc.DrawLine(div_x, 0, div_x, sz.GetHeight()); + draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()), + comp_colour(0), comp_colour(1), ratio(1) / 100.0); }); m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) { @@ -954,72 +939,15 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker() wxSize sz = m_triangle_panel->GetClientSize(); auto [v0, v1, v2] = get_vertices(); - wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); - dc.SetBrush(wxBrush(tri_bg)); - dc.SetPen(*wxTRANSPARENT_PEN); - dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); - - wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2); - - const bool cache_valid = m_tri_cache_bmp.IsOk() && - m_tri_cache_size == sz && - m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2; - - if (!cache_valid) { - int min_y = (int)std::min({v0.y, v1.y, v2.y}); - int max_y = (int)std::max({v0.y, v1.y, v2.y}); - int min_x = (int)std::min({v0.x, v1.x, v2.x}); - int max_x = (int)std::max({v0.x, v1.x, v2.x}); - - m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24); - wxMemoryDC mdc(m_tri_cache_bmp); - mdc.SetBrush(wxBrush(tri_bg)); - mdc.SetPen(*wxTRANSPARENT_PEN); - mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); - - for (int py = min_y; py <= max_y; ++py) { - for (int px = min_x; px <= max_x; ++px) { - TriPoint p = {(double)px, (double)py}; - if (!tri_contains(p, v0, v1, v2)) continue; - double w0, w1, w2; - tri_barycentric(p, v0, v1, v2, w0, w1, w2); - unsigned char mr, mg, mb; - if (w0 + w1 > 1e-6) { - float t01 = static_cast(w1 / (w0 + w1)); - Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), - c1.Red(), c1.Green(), c1.Blue(), - t01, &mr, &mg, &mb); - float t2 = static_cast(w2); - Slic3r::filament_mixer_lerp(mr, mg, mb, - c2.Red(), c2.Green(), c2.Blue(), - t2, &mr, &mg, &mb); - } else { - mr = c2.Red(); mg = c2.Green(); mb = c2.Blue(); - } - mdc.SetPen(wxPen(wxColour(mr, mg, mb))); - mdc.DrawPoint(px, py); - } - } - - mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1)); - mdc.SetBrush(*wxTRANSPARENT_BRUSH); - wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}}; - mdc.DrawPolygon(3, pts); - - mdc.SelectObject(wxNullBitmap); - m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2; - m_tri_cache_size = sz; - } - - dc.DrawBitmap(m_tri_cache_bmp, 0, 0); - - // Drag handle (always redrawn on top of cached bitmap) - double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x; - double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y; - int handle_r = FromDIP(5); - dc.SetBrush(*wxWHITE_BRUSH); - dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2))); - dc.DrawCircle((int)hx, (int)hy, handle_r); + // Draw the background, cached barycentric fill, outline and drag-handle marker through the + // shared picker painter (same geometry the read-only Publish preview uses). + draw_mixed_triangle_picker(dc, sz, + {comp_colour(0), comp_colour(1), comp_colour(2)}, + {m_tri_wx, m_tri_wy, m_tri_wz}, + {StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour("#CECECE")), + StateColor::darkModeColorFor(wxColour("#262E30")), + StateColor::darkModeColorFor(COLOR_LABEL_MUTED)}); if (m_result.ratios.size() >= 3) { dc.SetFont(::Label::Body_10); diff --git a/src/slic3r/GUI/MixedFilamentDialog.hpp b/src/slic3r/GUI/MixedFilamentDialog.hpp index ea8ac5ad16..274eac1f92 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.hpp +++ b/src/slic3r/GUI/MixedFilamentDialog.hpp @@ -170,10 +170,6 @@ private: // Triangle picker drag point (barycentric weights) double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334}; - // Cached triangle color bitmap (invalidated when colors or size change) - wxBitmap m_tri_cache_bmp; - wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2; - wxSize m_tri_cache_size; std::array m_triangle_ratio_labels{nullptr, nullptr, nullptr}; }; diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 455a318935..73016f4902 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -31,6 +31,7 @@ #include #include #include +#include namespace Slic3r { namespace GUI { namespace { @@ -423,7 +424,7 @@ PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_ spec.tri_weights = spec.ratios; // the picker's barycentric shares } else { const Slic3r::GradientCurve curve = mixed_gradient_curve(full, slot); - constexpr int kSamples = 64; + constexpr int kSamples = 256; for (int i = 0; i <= kSamples; ++i) { const double t = double(i) / kSamples; spec.gradient_samples.emplace_back(t, sample_gradient_curve(curve, t)); @@ -894,9 +895,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind) section.mixed_tabs = new TabCtrl(section.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, s_tab_style); section.mixed_tabs->SetFont(Label::Body_14); section.mixed_tabs->SetBackgroundColour(GetBackgroundColour()); - // The mixed tabs carry full swatch compositions: give them extra room to breathe so - // neighbouring compositions do not read as one long row (must precede AppendItem). - section.mixed_tabs->SetItemSpace(FromDIP(5)); + // The mixed tabs carry full swatch compositions: give them a touch more room than the + // filament tabs so neighbouring compositions stay distinguishable (must precede AppendItem). + section.mixed_tabs->SetItemSpace(FromDIP(3)); page_sizer->Add(section.mixed_tabs, 0, wxEXPAND | wxTOP, FromDIP(2)); section.mixed_tabs->Hide(); } @@ -1180,18 +1181,10 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV auto* viz = new wxPanel(category.page, wxID_ANY); viz->SetBackgroundStyle(wxBG_STYLE_PAINT); - // Per-panel fill-bitmap cache for the ternary branch; rebuilt only when size or colours - // change (shared_ptr keeps the lifetime independent of this method's locals). - struct TriCache - { - wxBitmap bmp; - wxSize sz{0, 0}; - wxColour c0, c1, c2; - }; - auto tri_cache = std::make_shared(); // Theme colours and DIP metrics are resolved inside the paint handler so dark-mode toggles - // and DPI changes are picked up on the next repaint without any explicit listener. - viz->Bind(wxEVT_PAINT, [this, panel = viz, spec, tri_cache](wxPaintEvent&) { + // and DPI changes are picked up on the next repaint without any explicit listener. The + // ternary fill cache lives inside the shared triangle painter (FilamentBitmapUtils). + viz->Bind(wxEVT_PAINT, [this, panel = viz, spec](wxPaintEvent&) { const wxColour bg = StateColor::darkModeColorFor(*wxWHITE); wxBufferedPaintDC pdc(panel); pdc.SetBackground(wxBrush(bg)); @@ -1203,236 +1196,117 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV const size_t n = spec.component_colours.size(); if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) { - // Ternary mix: a read-only miniature of the MixedFilamentDialog's triangle picker. - // Per-pixel barycentric fill is cached into a bitmap keyed on size + colours; the - // marker and labels are redrawn on top every paint. - const wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE); - const wxColour outline = StateColor::darkModeColorFor(wxColour("#CECECE")); - const wxColour ring = StateColor::darkModeColorFor(wxColour("#262E30")); - const wxColour label_c = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 - const double margin_dip = 24.0; - auto& cache = *tri_cache; - - auto vertices_for = [&](const wxSize& sz) -> std::tuple { - const double pw = sz.GetWidth(), ph = sz.GetHeight(); - const int margin = FromDIP(int(margin_dip)); - const double avail = std::min(pw, ph) - 2.0 * margin; - const double side = avail; - const double tri_h = side * std::sqrt(3.0) / 2.0; - const double cx = pw / 2.0; - const double top_y = (ph - tri_h) / 2.0; - return {{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}; - }; - - pdc.SetFont(::Label::Body_12); - const wxColour& c0 = spec.component_colours[0]; - const wxColour& c1 = spec.component_colours[1]; - const wxColour& c2 = spec.component_colours[2]; - - if (!cache.bmp.IsOk() || cache.sz != rc.GetSize() || cache.c0 != c0 || cache.c1 != c1 || cache.c2 != c2) { - auto [v0, v1, v2] = vertices_for(rc.GetSize()); - cache.bmp = wxBitmap(rc.width, rc.height, 32); - wxMemoryDC mdc(cache.bmp); - mdc.SetBrush(wxBrush(tri_bg)); - mdc.SetPen(*wxTRANSPARENT_PEN); - mdc.DrawRectangle(0, 0, rc.width, rc.height); - - const int min_y = int(std::min({v0.y, v1.y, v2.y})); - const int max_y = int(std::max({v0.y, v1.y, v2.y})); - const int min_x = int(std::min({v0.x, v1.x, v2.x})); - const int max_x = int(std::max({v0.x, v1.x, v2.x})); - for (int py = min_y; py <= max_y; ++py) - for (int px = min_x; px <= max_x; ++px) { - const TriPoint p = {double(px), double(py)}; - if (!tri_contains(p, v0, v1, v2)) - continue; - double w0, w1, w2; - tri_barycentric(p, v0, v1, v2, w0, w1, w2); - unsigned char mr, mg, mb; - if (w0 + w1 > 1e-6) { - float t01 = static_cast(w1 / (w0 + w1)); - Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, - &mb); - Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), static_cast(w2), &mr, &mg, &mb); - } else { - mr = c2.Red(); - mg = c2.Green(); - mb = c2.Blue(); - } - mdc.SetPen(wxPen(wxColour(mr, mg, mb))); - mdc.DrawPoint(px, py); - } - - mdc.SetPen(wxPen(outline, 1)); - mdc.SetBrush(*wxTRANSPARENT_BRUSH); - const wxPoint pts[3] = {{int(v0.x), int(v0.y)}, {int(v1.x), int(v1.y)}, {int(v2.x), int(v2.y)}}; - mdc.DrawPolygon(3, pts); - mdc.SelectObject(wxNullBitmap); - - cache.sz = rc.GetSize(); - cache.c0 = c0; - cache.c1 = c1; - cache.c2 = c2; - } - pdc.DrawBitmap(cache.bmp, 0, 0); - - // Published-ratio marker (read-only twin of the editor's drag handle). - { - auto [v0, v1, v2] = vertices_for(rc.GetSize()); - const double w0 = spec.tri_weights[0], w1 = spec.tri_weights[1], w2 = spec.tri_weights[2]; - const int hx = int(w0 * v0.x + w1 * v1.x + w2 * v2.x); - const int hy = int(w0 * v0.y + w1 * v1.y + w2 * v2.y); - pdc.SetBrush(*wxWHITE_BRUSH); - pdc.SetPen(wxPen(ring, FromDIP(2))); - pdc.DrawCircle(hx, hy, FromDIP(5)); - - // Percent label beside each vertex. - for (int i = 0; i < 3; ++i) { - const wxString text = wxString::Format("%d%%", int(std::lround(spec.tri_weights[i] * 100.0))); + // Ternary mix: read-only miniature of the MixedFilamentDialog's triangle picker. + const MixedTriangleTheme tri_theme{StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour("#CECECE")), + StateColor::darkModeColorFor(wxColour("#262E30")), + StateColor::darkModeColorFor(wxColour(107, 107, 107))}; + draw_mixed_triangle_picker(pdc, rc.GetSize(), {spec.component_colours[0], spec.component_colours[1], spec.component_colours[2]}, + {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme); + draw_mixed_triangle_labels(pdc, rc.GetSize(), {spec.tri_weights[0], spec.tri_weights[1], spec.tri_weights[2]}, tri_theme); + } else if (!spec.is_gradient) { + // Ratio bar. A 2-component slot matches the MixedFilamentDialog's continuous blend + + // divider (with its two end labels); wider non-gradient mixes (rare) fall back to one + // solid segment per component. + if (n == 2) { + const int bar_h = FromDIP(27); + draw_mixed_ratio_blend_bar(pdc, wxRect(rc.x, rc.y, rc.width, bar_h), spec.component_colours[0], + spec.component_colours[1], spec.ratios[1]); + // Read-only twin of the dialog's left/right percentage labels. + pdc.SetFont(::Label::Body_12); + pdc.SetTextForeground(StateColor::darkModeColorFor(wxColour(107, 107, 107))); + const wxString la = wxString::Format("%d%%", int(std::lround(spec.ratios[0] * 100.0))); + const wxString lb = wxString::Format("%d%%", int(std::lround(spec.ratios[1] * 100.0))); + const int lab_y = rc.y + bar_h + FromDIP(2); + pdc.DrawText(la, rc.x, lab_y); + pdc.DrawText(lb, rc.x + rc.width - pdc.GetTextExtent(lb).GetWidth(), lab_y); + } else { + draw_mixed_ratio_segments(pdc, rc, spec.component_colours, spec.ratios); + // Percent label centred in each segment wide enough to hold it. + std::vector shares = spec.ratios; + double total = 0.0; + for (double r : shares) + total += r; + if (total <= 0.0) { + shares.assign(n, 1.0 / n); + total = 1.0; + } + pdc.SetFont(::Label::Body_12); + int x0 = rc.x; + for (size_t i = 0; i < n; ++i) { + const int x1 = (i + 1 < n) + ? rc.x + int(std::lround(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0) / total * double(rc.width))) + : rc.x + rc.width; + const int w = std::max(1, x1 - x0); + const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); const wxSize tsz = pdc.GetTextExtent(text); - const TriPoint vtx = (i == 0) ? v0 : (i == 1) ? v1 : v2; - int lx = int(vtx.x - tsz.GetWidth() / 2.0); - int ly = (i == 0) ? int(vtx.y - tsz.GetHeight()) : int(vtx.y + FromDIP(3)); - ly = std::clamp(ly, 0, rc.height - tsz.GetHeight()); - lx = std::clamp(lx, 0, rc.width - tsz.GetWidth()); - pdc.SetTextForeground(label_c); - pdc.DrawText(text, lx, ly); + if (tsz.GetWidth() + FromDIP(4) <= w) { + const wxColour& c = spec.component_colours[i]; + const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue(); + pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE); + pdc.DrawText(text, x0 + (w - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2); + } + x0 = x1; } } - } else if (!spec.is_gradient) { - // Stacked ratio bar: one solid segment per component, widths proportional to the - // published shares. Integer widths accumulate left to right; the last segment takes - // the rounding remainder so the bar always fills exactly. - std::vector shares = spec.ratios; - double total = 0.0; - for (double r : shares) - total += r; - if (shares.size() != n || total <= 0.0) { - shares.assign(n, 1.0 / n); - total = 1.0; - } - auto share_to_px = [&](double share_sum) { return rc.x + int(std::lround(share_sum / total * double(rc.width))); }; - std::vector segs(n); - int x0 = rc.x; - for (size_t i = 0; i < n; ++i) { - int x1 = rc.x + rc.width; - if (i + 1 < n) - x1 = share_to_px(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0)); - segs[i] = wxRect(x0, rc.y, std::max(1, x1 - x0), rc.height); - x0 = segs[i].GetRight() + 1; - } - - for (size_t i = 0; i < n; ++i) { - pdc.SetPen(*wxTRANSPARENT_PEN); - pdc.SetBrush(wxBrush(spec.component_colours[i])); - pdc.DrawRectangle(segs[i]); - } - pdc.SetBrush(*wxTRANSPARENT_BRUSH); - pdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1)); - pdc.DrawRectangle(rc); - - // Percent label centred in each segment wide enough to hold it. - pdc.SetFont(::Label::Body_12); - for (size_t i = 0; i < n; ++i) { - const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); - const wxSize tsz = pdc.GetTextExtent(text); - if (tsz.GetWidth() + FromDIP(4) > segs[i].GetWidth()) - continue; - // Label contrast follows the swatch itself, not the theme. - const wxColour& c = spec.component_colours[i]; - const double lum = 0.299 * c.Red() + 0.587 * c.Green() + 0.114 * c.Blue(); - pdc.SetTextForeground(lum > 140 ? wxColour("#262E30") : *wxWHITE); - pdc.DrawText(text, segs[i].x + (segs[i].GetWidth() - tsz.GetWidth()) / 2, rc.y + (rc.height - tsz.GetHeight()) / 2); - } } else { - // Gradient: compact "Material Ratio" over "Model Height" graph, a read-only - // miniature of the GradientCurveEditor plot. Component order matches the config; - // the second component's curve is the mirror of the first's. - const wxColour grid_color = StateColor::darkModeColorFor(wxColour(238, 238, 238)); // grey 300 - const wxColour axis_color = StateColor::darkModeColorFor(wxColour(107, 107, 107)); // grey 700 - const wxColour label_muted = StateColor::darkModeColorFor(wxColour(107, 107, 107)); - const wxColour point_fill = StateColor::darkModeColorFor(*wxWHITE); - - const int pad_left = FromDIP(34); - const int pad_right = FromDIP(10); - const int pad_top = FromDIP(18); - const int pad_bottom = FromDIP(16); - const wxRect plot(rc.x + pad_left, rc.y + pad_top, std::max(1, rc.width - pad_left - pad_right), - std::max(1, rc.height - pad_top - pad_bottom)); - - constexpr int kGridDivisions = 5; - pdc.SetPen(wxPen(grid_color, 1)); - for (int i = 0; i <= kGridDivisions; ++i) { - const int gx = plot.x + plot.width * i / kGridDivisions; - const int gy = plot.y + plot.height * i / kGridDivisions; - pdc.DrawLine(gx, plot.y, gx, plot.y + plot.height); - pdc.DrawLine(plot.x, gy, plot.x + plot.width, gy); - } - - // Axes with small filled arrowheads along the plot's left and bottom edges. - const int arrow_len = FromDIP(7); - const int arrow_half = FromDIP(3); - pdc.SetPen(wxPen(axis_color, 1)); - pdc.SetBrush(wxBrush(axis_color)); - pdc.DrawLine(plot.x, plot.y + plot.height, plot.x, plot.y); - { - wxPoint tri[3] = {wxPoint(plot.x, plot.y - arrow_len), wxPoint(plot.x - arrow_half, plot.y), - wxPoint(plot.x + arrow_half, plot.y)}; - pdc.DrawPolygon(3, tri); - } - pdc.DrawLine(plot.x, plot.y + plot.height, plot.x + plot.width, plot.y + plot.height); - { - wxPoint tri[3] = {wxPoint(plot.x + plot.width + arrow_len, plot.y + plot.height), - wxPoint(plot.x + plot.width, plot.y + plot.height - arrow_half), - wxPoint(plot.x + plot.width, plot.y + plot.height + arrow_half)}; - pdc.DrawPolygon(3, tri); - } - - wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); - label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1)); - pdc.SetFont(label_font); - pdc.SetTextForeground(label_muted); - pdc.DrawText(_L("Material Ratio"), plot.x + FromDIP(4), plot.y - pdc.GetTextExtent(_L("Material Ratio")).GetHeight()); - const wxString height_title = _L("Model Height"); - pdc.DrawText(height_title, plot.x + plot.width - pdc.GetTextExtent(height_title).GetWidth(), plot.y + plot.height + FromDIP(2)); - + // Gradient: read-only miniature of the GradientCurveEditor plot, drawn through the + // shared painter so it anti-aliases and keeps the square proportions and labels. + const MixedGradientTheme grad_theme{StateColor::darkModeColorFor(*wxWHITE), + StateColor::darkModeColorFor(wxColour(238, 238, 238)), + StateColor::darkModeColorFor(wxColour(107, 107, 107)), + StateColor::darkModeColorFor(wxColour(107, 107, 107)), + StateColor::darkModeColorFor(wxColour(38, 46, 48)), + StateColor::darkModeColorFor(wxColour(172, 172, 172)), + StateColor::darkModeColorFor(*wxWHITE)}; + std::vector curves; + std::vector anchors; if (spec.gradient_samples.size() >= 2 && n >= 2) { - auto curve_point = [&](double t, double ratio) { - return wxPoint(plot.x + int(std::lround(t * plot.width)), plot.y + int(std::lround((1.0 - ratio) * plot.height))); + const wxRect plot = mixed_gradient_plot_rect(rc.GetSize()); + auto lift_alpha = [](wxColour c) { + if (c.Alpha() == 0) + c.Set(c.Red(), c.Green(), c.Blue(), 150); + return c; }; - // First component's ratio solid, its mirror dashed-free twin for the other. - const wxColour& col_a = spec.component_colours[0]; - const wxColour& col_b = spec.component_colours[1]; - std::vector pts_a, pts_b; + // First component's ratio solid, its mirror twin for the other. + const wxColour col_a = lift_alpha(spec.component_colours[0]); + const wxColour col_b = lift_alpha(spec.component_colours[1]); + std::vector pts_a, pts_b; pts_a.reserve(spec.gradient_samples.size()); pts_b.reserve(spec.gradient_samples.size()); for (const auto& [t, r] : spec.gradient_samples) { - pts_a.push_back(curve_point(t, r)); - pts_b.push_back(curve_point(t, 1.0 - r)); + pts_a.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height}); + pts_b.push_back({plot.x + t * plot.width, plot.y + r * plot.height}); } - pdc.SetPen(wxPen(col_b, 2)); - for (size_t i = 0; i + 1 < pts_b.size(); ++i) - pdc.DrawLine(pts_b[i], pts_b[i + 1]); - pdc.SetPen(wxPen(col_a, 2)); - for (size_t i = 0; i + 1 < pts_a.size(); ++i) - pdc.DrawLine(pts_a[i], pts_a[i + 1]); - // Control-point anchors of the stored curve on the first component's line. - pdc.SetBrush(wxBrush(point_fill)); - pdc.SetPen(wxPen(col_a, 1)); - for (const auto& [t, r] : spec.gradient_anchors) { - const wxPoint c = curve_point(t, r); - pdc.DrawCircle(c, FromDIP(3)); - } + anchors.reserve(spec.gradient_anchors.size()); + for (const auto& [t, r] : spec.gradient_anchors) + anchors.push_back({plot.x + t * plot.width, plot.y + (1.0 - r) * plot.height}); + curves.push_back({std::move(pts_a), col_a, 4}); + curves.push_back({std::move(pts_b), col_b, 2}); } + draw_mixed_gradient_plot(pdc, rc.GetSize(), curves, anchors, grad_theme); } }); // Fixed DIP size, left-aligned: the visualization keeps its proportions no matter how the // dialog is resized (the paint handler draws into whatever client rect the panel ends up - // with, so nothing else has to change). - const int viz_h = spec.is_gradient ? 150 : (spec.tri_weights.size() == 3 ? 180 : 30); - const wxSize viz_sz(FromDIP(240), FromDIP(viz_h)); + // with, so nothing else has to change). Sizes mirror the MixedFilamentDialog controls: the + // gradient plot and triangle picker match the editor's 260x200 / 160x160, the ratio bar is + // the dialog's 27px bar plus its label line. + int viz_h; + int viz_w; + if (spec.is_gradient) { + viz_w = 260; + viz_h = 200; + } else if (spec.tri_weights.size() == 3 && spec.component_colours.size() == 3) { + viz_w = 160; + viz_h = 160; + } else { + viz_w = 240; + viz_h = spec.component_colours.size() == 2 ? 50 : 30; + } + const wxSize viz_sz(FromDIP(viz_w), FromDIP(viz_h)); viz->SetMinSize(viz_sz); viz->SetMaxSize(viz_sz); // Parented to the page right above the scroll area, so it is always shown with the tab: diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 7e1f748e36..4090bf9c1b 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -92,7 +92,7 @@ int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* cli btn->Create(this, item, "", wxBORDER_NONE); btn->SetFont(GetFont()); btn->SetTextColor( - StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal))); + StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(wxColour("#262E30"), (int) StateColor::Normal))); btn->SetBackgroundColor(StateColor()); btn->SetCornerRadius(0); btn->SetPaddingSize({TAB_BUTTON_PADDING}); From 174d23e22fc82a511fd95199750e9440a583cb15 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 12:40:41 +0800 Subject: [PATCH 41/60] Fixes windows light mode text issues --- src/slic3r/GUI/PublishSettingsDialog.cpp | 106 +++++++++++++---------- src/slic3r/GUI/PublishSettingsDialog.hpp | 2 + 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 73016f4902..26ed31dfc3 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -200,8 +200,12 @@ wxBitmap mixed_filament_chip_bitmap(const DynamicPrintConfig& full, size_t slot, // followed by their percent share (or a "->" arrow for gradients), mirroring the main GUI's // sidebar rows - e.g. "[3 purple]: [1 red] 50% + [2 blue] 50%". The whole composition is one // bitmap because a TabCtrl item cannot interleave images into its text; the tab's text is -// therefore empty. Transparent background like the other chips. -wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, int swatch_sz) +// therefore empty. The bitmap is opaque and bakes tab_bg (the tab strip's background colour): +// its texts are rasterized with plain GDI on that solid ground, like the sidebar rows and the +// color chips. Text on a transparent bitmap has to go through the graphics-context route, and +// on MSW that degrades into jagged ClearType-fallback glyphs with fringe halos - the same +// reason draw_mixed_gradient_plot renders into an opaque buffer (FilamentBitmapUtils.cpp). +wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, int swatch_sz, const wxColour& tab_bg) { const std::vector comps = mixed_slot_components(full, slot); if (comps.empty()) @@ -272,40 +276,26 @@ wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, for (const Piece& p : pieces) width += (p.kind == Piece::Swatch ? swatch_sz : measure_dc.GetTextExtent(p.text).x + gap); - // Phase 2 (draw): transparent background like the page-header chips. + // Phase 2 (draw): opaque, filled with the tab strip's background colour. Text goes through + // the plain DC on every platform (see the function comment for why the alpha route is + // avoided); chip bitmaps keep their own alpha and composite cleanly onto the fill. wxBitmap composite(width, swatch_sz); wxMemoryDC memdc; -#ifdef __WXOSX__ - composite.UseAlpha(); memdc.SelectObject(composite); -#else - { - wxImage img(width, swatch_sz); - img.InitAlpha(); - memset(img.GetAlpha(), 0, width * swatch_sz); - composite = wxBitmap(std::move(img)); - } - memdc.SelectObject(composite); -#endif - { -#ifdef __WXMSW__ - wxGCDC dc(memdc); -#else - wxDC& dc = memdc; -#endif - dc.SetBackgroundMode(wxTRANSPARENT); - dc.SetFont(::Label::Body_12); - dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#262E30"))); - int x = 0; - for (const Piece& p : pieces) { - if (p.kind == Piece::Swatch) { - dc.DrawBitmap(p.bmp, x, 0); - x += swatch_sz; - } else { - const wxSize tsz = measure_dc.GetTextExtent(p.text); - dc.DrawText(p.text, x + gap / 2, (swatch_sz - tsz.y) / 2); - x += tsz.x + gap; - } + memdc.SetBackground(wxBrush(tab_bg)); + memdc.Clear(); + memdc.SetBackgroundMode(wxTRANSPARENT); + memdc.SetFont(::Label::Body_12); + memdc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#262E30"))); + int x = 0; + for (const Piece& p : pieces) { + if (p.kind == Piece::Swatch) { + memdc.DrawBitmap(p.bmp, x, 0, true); + x += swatch_sz; + } else { + const wxSize tsz = measure_dc.GetTextExtent(p.text); + memdc.DrawText(p.text, x + gap / 2, (swatch_sz - tsz.y) / 2); + x += tsz.x + gap; } } memdc.SelectObject(wxNullBitmap); @@ -546,6 +536,10 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) SetSizerAndFit(w_sizer); fit_to_content(); // initial size only; the dialog is resizable wxGetApp().UpdateDlgDarkUI(this); + // The dark-UI walk re-colours the tab strips after the compositions were baked; rebuild so + // they carry the theme-resolved background (the sidebar resolves its colours at paint time, + // this dialog bakes, so the bake has to happen after the walk). + refresh_mixed_tab_bitmaps(); } // Size the window to its content: width follows the widest tab strip so no filament tab is @@ -1030,7 +1024,7 @@ size_t PublishSettingsDialog::category_index_for( // bitmap; the text is empty because a TabCtrl item cannot interleave images into // its text. const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); - const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full_cfg, source_index, FromDIP(20)); + const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full_cfg, source_index, FromDIP(20), target->GetBackgroundColour()); if (tab_bmp.IsOk()) target->AppendItem(wxString(), tab_bmp); else @@ -1891,28 +1885,46 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) for (size_t category_index = 0; category_index < m_categories.size(); ++category_index) { const Category& category = m_categories[category_index]; - if (category.section != Section::Material) + if (category.section != Section::Material || category.is_mixed) continue; - const SectionGroup& section = m_sections[category.group]; - if (category.is_mixed) { - // The tab carries the full composition bitmap (mix chip + components + percents); - // the page header has no chip/title to refresh. - const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full, category.filament_slot, FromDIP(20)); - if (tab_bmp.IsOk()) { - const auto iter = std::find(section.mixed_categories.begin(), section.mixed_categories.end(), category_index); - if (iter != section.mixed_categories.end() && section.mixed_tabs != nullptr) - section.mixed_tabs->SetItemBitmap(static_cast(iter - section.mixed_categories.begin()), tab_bmp); - } - } else if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), - std::to_string(category.filament_slot + 1), FromDIP(20), FromDIP(20))) { + if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), + std::to_string(category.filament_slot + 1), FromDIP(20), FromDIP(20))) { + const SectionGroup& section = m_sections[category.group]; const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); if (iter != section.categories.end()) m_sections[category.group].tabs->SetItemBitmap(static_cast(iter - section.categories.begin()), *chip); } } + refresh_mixed_tab_bitmaps(); fit_to_content(); // tab buttons' min widths grew with the DPI: re-fit (incl. resize floor) Refresh(); } +void PublishSettingsDialog::refresh_mixed_tab_bitmaps() +{ + // The mixed-tab composition bitmaps bake the tab strip's background colour and DPI-scaled + // text, so both a theme flip and a DPI change have to rebuild them. + const DynamicPrintConfig full = wxGetApp().preset_bundle->full_config(); + for (SectionGroup& section : m_sections) { + if (section.mixed_tabs == nullptr) + continue; + const wxColour tab_bg = section.mixed_tabs->GetBackgroundColour(); + for (size_t i = 0; i < section.mixed_categories.size(); ++i) { + const Category& category = m_categories[section.mixed_categories[i]]; + const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full, category.filament_slot, FromDIP(20), tab_bg); + if (tab_bmp.IsOk()) + section.mixed_tabs->SetItemBitmap(static_cast(i), tab_bmp); + } + } +} + +void PublishSettingsDialog::on_sys_color_changed() +{ + // The mixed-tab compositions bake the tab strip's background colour: rebuild them when the + // theme changes (on_dpi_changed covers rescales). + refresh_mixed_tab_bitmaps(); + Refresh(); +} + }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 8d59192309..f2dcf70bba 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -58,9 +58,11 @@ public: protected: void on_dpi_changed(const wxRect& suggested_rect) override; + void on_sys_color_changed() override; private: void fit_to_content(); + void refresh_mixed_tab_bitmaps(); // Which part of the settings the row/category came from. enum class Section { Print, Printer, Material }; From 49c4b09db62dae4d478d15edb21b9181de5ba194 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 14:56:03 +0800 Subject: [PATCH 42/60] Show alias instead of full name in publish dialog --- src/slic3r/GUI/PublishSettingsDialog.cpp | 47 +++++++++++++----------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 26ed31dfc3..fcc2816b06 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -93,8 +93,12 @@ wxString material_title(size_t slot, const PresetBundle* bundle, const DynamicPr { if (slot < bundle->filament_presets.size()) { const Preset* preset = bundle->filaments.find_preset(bundle->filament_presets[slot]); - if (preset != nullptr && !preset->name.empty()) - return from_u8(material_display_name(preset->name)); + if (preset != nullptr && !preset->name.empty()) { + // Display the name the sidebar does: the alias (already bare-name + variant-tail + // stripped for root filament presets). + const std::string& display = preset->alias.empty() ? get_preset_bare_name(preset->name) : preset->alias; + return from_u8(material_display_name(display)); + } } const PublishMaterialIdentity identity = material_identity(slot, full); if (!identity.type.empty()) @@ -250,7 +254,7 @@ wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, { const wxBitmap chip = mixed_filament_chip_bitmap(full, slot, swatch_sz); - has_lead = chip.IsOk(); + has_lead = chip.IsOk(); if (has_lead) pieces.push_back({Piece::Swatch, chip, wxString()}); } @@ -357,7 +361,8 @@ public: } auto* reason = new wxStaticText(this, wxID_ANY, - issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") : _L("material not published")); + issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") : + _L("material not published")); reason->SetFont(Label::Body_12); reason->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#989898"))); row->Add(reason, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8)); @@ -366,7 +371,8 @@ public: content->AddSpacer(FromDIP(6)); } - auto* hint = new wxStaticText(this, wxID_ANY, + auto* hint = new wxStaticText( + this, wxID_ANY, _L("To publish a mixed filament, enable every filament it uses and choose Full Publish or check its Type requirement.")); hint->SetFont(Label::Body_12); hint->Wrap(FromDIP(380)); @@ -1024,7 +1030,7 @@ size_t PublishSettingsDialog::category_index_for( // bitmap; the text is empty because a TabCtrl item cannot interleave images into // its text. const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); - const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full_cfg, source_index, FromDIP(20), target->GetBackgroundColour()); + const wxBitmap tab_bmp = mixed_filament_tab_bitmap(full_cfg, source_index, FromDIP(20), target->GetBackgroundColour()); if (tab_bmp.IsOk()) target->AppendItem(wxString(), tab_bmp); else @@ -1191,8 +1197,7 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV if (spec.tri_weights.size() == 3 && n == 3 && !spec.is_gradient) { // Ternary mix: read-only miniature of the MixedFilamentDialog's triangle picker. - const MixedTriangleTheme tri_theme{StateColor::darkModeColorFor(*wxWHITE), - StateColor::darkModeColorFor(wxColour("#CECECE")), + const MixedTriangleTheme tri_theme{StateColor::darkModeColorFor(*wxWHITE), StateColor::darkModeColorFor(wxColour("#CECECE")), StateColor::darkModeColorFor(wxColour("#262E30")), StateColor::darkModeColorFor(wxColour(107, 107, 107))}; draw_mixed_triangle_picker(pdc, rc.GetSize(), {spec.component_colours[0], spec.component_colours[1], spec.component_colours[2]}, @@ -1204,21 +1209,21 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV // solid segment per component. if (n == 2) { const int bar_h = FromDIP(27); - draw_mixed_ratio_blend_bar(pdc, wxRect(rc.x, rc.y, rc.width, bar_h), spec.component_colours[0], - spec.component_colours[1], spec.ratios[1]); + draw_mixed_ratio_blend_bar(pdc, wxRect(rc.x, rc.y, rc.width, bar_h), spec.component_colours[0], spec.component_colours[1], + spec.ratios[1]); // Read-only twin of the dialog's left/right percentage labels. pdc.SetFont(::Label::Body_12); pdc.SetTextForeground(StateColor::darkModeColorFor(wxColour(107, 107, 107))); const wxString la = wxString::Format("%d%%", int(std::lround(spec.ratios[0] * 100.0))); const wxString lb = wxString::Format("%d%%", int(std::lround(spec.ratios[1] * 100.0))); - const int lab_y = rc.y + bar_h + FromDIP(2); + const int lab_y = rc.y + bar_h + FromDIP(2); pdc.DrawText(la, rc.x, lab_y); pdc.DrawText(lb, rc.x + rc.width - pdc.GetTextExtent(lb).GetWidth(), lab_y); } else { draw_mixed_ratio_segments(pdc, rc, spec.component_colours, spec.ratios); // Percent label centred in each segment wide enough to hold it. std::vector shares = spec.ratios; - double total = 0.0; + double total = 0.0; for (double r : shares) total += r; if (total <= 0.0) { @@ -1228,10 +1233,10 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV pdc.SetFont(::Label::Body_12); int x0 = rc.x; for (size_t i = 0; i < n; ++i) { - const int x1 = (i + 1 < n) - ? rc.x + int(std::lround(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0) / total * double(rc.width))) - : rc.x + rc.width; - const int w = std::max(1, x1 - x0); + const int x1 = (i + 1 < n) ? rc.x + int(std::lround(std::accumulate(shares.begin(), shares.begin() + i + 1, 0.0) / + total * double(rc.width))) : + rc.x + rc.width; + const int w = std::max(1, x1 - x0); const wxString text = wxString::Format("%d%%", int(std::lround(shares[i] / total * 100.0))); const wxSize tsz = pdc.GetTextExtent(text); if (tsz.GetWidth() + FromDIP(4) <= w) { @@ -1254,10 +1259,10 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV StateColor::darkModeColorFor(wxColour(172, 172, 172)), StateColor::darkModeColorFor(*wxWHITE)}; std::vector curves; - std::vector anchors; + std::vector anchors; if (spec.gradient_samples.size() >= 2 && n >= 2) { const wxRect plot = mixed_gradient_plot_rect(rc.GetSize()); - auto lift_alpha = [](wxColour c) { + auto lift_alpha = [](wxColour c) { if (c.Alpha() == 0) c.Set(c.Red(), c.Green(), c.Blue(), 150); return c; @@ -1288,8 +1293,8 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV // with, so nothing else has to change). Sizes mirror the MixedFilamentDialog controls: the // gradient plot and triangle picker match the editor's 260x200 / 160x160, the ratio bar is // the dialog's 27px bar plus its label line. - int viz_h; - int viz_w; + int viz_h; + int viz_w; if (spec.is_gradient) { viz_w = 260; viz_h = 200; @@ -1890,7 +1895,7 @@ void PublishSettingsDialog::on_dpi_changed(const wxRect& suggested_rect) if (wxBitmap* chip = get_extruder_color_icon(filament_color_hex(full, category.filament_slot), std::to_string(category.filament_slot + 1), FromDIP(20), FromDIP(20))) { const SectionGroup& section = m_sections[category.group]; - const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); + const auto iter = std::find(section.categories.begin(), section.categories.end(), category_index); if (iter != section.categories.end()) m_sections[category.group].tabs->SetItemBitmap(static_cast(iter - section.categories.begin()), *chip); } From 7133d6b22557b7ba664c8b9005388867df4980f2 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 16:59:25 +0800 Subject: [PATCH 43/60] Added wiki and youtube guide link as placeholders. Add to recently opened in home screen after publishing. Show PUB badge. Add .published as a file save name hint. --- resources/web/homepage/css/home.css | 29 ++++++++++++++ resources/web/homepage/js/home.js | 5 ++- src/libslic3r/Format/bbs_3mf.cpp | 50 ++++++++++++++++++++++++ src/libslic3r/Format/bbs_3mf.hpp | 3 ++ src/slic3r/GUI/MainFrame.cpp | 15 ++++++- src/slic3r/GUI/MainFrame.hpp | 2 + src/slic3r/GUI/Plater.cpp | 39 +++++++++++------- src/slic3r/GUI/PublishSettingsDialog.cpp | 19 +++++++++ 8 files changed, 145 insertions(+), 17 deletions(-) diff --git a/resources/web/homepage/css/home.css b/resources/web/homepage/css/home.css index e99d22ec64..395dcc9356 100644 --- a/resources/web/homepage/css/home.css +++ b/resources/web/homepage/css/home.css @@ -572,6 +572,35 @@ body word-break: break-all; } +.FileNamePack +{ + display: flex; + align-items: center; + min-width: 0; + overflow: hidden; +} + +.FileNamePack .FileName +{ + flex: 1 1 auto; + min-width: 0; +} + +.FilePublishedBadge +{ + flex: 0 0 auto; + margin-right: 4px; + padding: 0 4px; + height: 16px; + line-height: 16px; + font-size: 10px; + font-weight: 600; + color: #FFFFFF; + background-color: #00AE42; + border-radius: 2px; + white-space: nowrap; +} + .FileDate { color: #A8A8A8; diff --git a/resources/web/homepage/js/home.js b/resources/web/homepage/js/home.js index f522c44bed..3cb5ff4fb3 100644 --- a/resources/web/homepage/js/home.js +++ b/resources/web/homepage/js/home.js @@ -219,15 +219,18 @@ function ShowRecentFileList( pList ) let sImg=OneFile["image"] || sImages[sPath]; let sTime=OneFile['time']; let sName=OneFile['project_name']; + let sPublished=OneFile['published'] == '1'; sImages[sPath] = sImg; //let index=sPath.lastIndexOf('\\')>0?sPath.lastIndexOf('\\'):sPath.lastIndexOf('\/'); //let sShortName=sPath.substring(index+1,sPath.length); + let sBadge=sPublished? 'PUB':''; + let TmpHtml='
'+ ''+ '
No Image
'+ - '
'+sName+'
'+ + '
'+sBadge+'
'+sName+'
'+ '
'+sTime+'
'+ '
'; diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index ed88511d9c..3a3ee20e6f 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -9205,6 +9205,56 @@ std::string bbs_3mf_get_thumbnail(const char *path) return data; } +bool bbs_3mf_is_published(const std::string &path) +{ + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + struct close_lock + { + mz_zip_archive *archive; + void close() + { + if (archive) { + close_zip_reader(archive); + archive = nullptr; + } + } + ~close_lock() { close(); } + } lock{&archive}; + + if (!open_zip_reader(&archive, path)) + return false; + + // Read just the model XML and locate the published metadata node; no geometry parsing. + int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0); + if (index < 0) + return false; + mz_zip_archive_file_stat stat; + if (!mz_zip_reader_file_stat(&archive, index, &stat)) + return false; + std::string xml(stat.m_uncomp_size, '\0'); + if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0)) + return false; + + const std::string needle = std::string(""; + size_t pos = xml.find(needle); + if (pos == std::string::npos) + return false; + pos += needle.size(); + size_t end = xml.find("", pos); + if (end == std::string::npos) + return false; + + size_t value_begin = pos, value_end = end; + while (value_begin < value_end && (xml[value_begin] == ' ' || xml[value_begin] == '\t' || xml[value_begin] == '\n' || xml[value_begin] == '\r')) + ++value_begin; + while (value_end > value_begin && (xml[value_end - 1] == ' ' || xml[value_end - 1] == '\t' || xml[value_end - 1] == '\n' || xml[value_end - 1] == '\r')) + --value_end; + + return is_published_3mf_flag(xml.substr(value_begin, value_end - value_begin)); +} + bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version) { CNumericLocalesSetter locales_setter; diff --git a/src/libslic3r/Format/bbs_3mf.hpp b/src/libslic3r/Format/bbs_3mf.hpp index 2e1600737b..768ae7bcfe 100644 --- a/src/libslic3r/Format/bbs_3mf.hpp +++ b/src/libslic3r/Format/bbs_3mf.hpp @@ -293,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub extern std::string bbs_3mf_get_thumbnail(const char * path); +// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node +extern bool bbs_3mf_is_published(const std::string &path); + extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list, Semver* file_version); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 3eb36154fc..7d73d884b0 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -4237,15 +4237,23 @@ std::wstring MainFrame::FileHistory::GetThumbnailUrl(int index) const return wss.str(); } +bool MainFrame::FileHistory::GetPublished(int index) const +{ + return index >= 0 && index < static_cast(m_published_files.size()) && m_published_files[index]; +} + void MainFrame::FileHistory::AddFileToHistory(const wxString &file) { if (this->m_fileMaxFiles == 0) return; wxFileHistory::AddFileToHistory(file); - if (m_load_called) + if (m_load_called) { m_thumbnails.push_front(bbs_3mf_get_thumbnail(into_u8(file).c_str())); - else + m_published_files.push_front(bbs_3mf_is_published(into_u8(file))); + } else { m_thumbnails.push_front(""); + m_published_files.push_front(false); + } } void MainFrame::FileHistory::RemoveFileFromHistory(size_t i) @@ -4254,6 +4262,7 @@ void MainFrame::FileHistory::RemoveFileFromHistory(size_t i) return; wxFileHistory::RemoveFileFromHistory(i); m_thumbnails.erase(m_thumbnails.begin() + i); + m_published_files.erase(m_published_files.begin() + i); } size_t MainFrame::FileHistory::FindFileInHistory(const wxString & file) @@ -4269,6 +4278,7 @@ void MainFrame::FileHistory::LoadThumbnails() if (!thumbnail.empty()) { m_thumbnails[i] = thumbnail; } + m_published_files[i] = bbs_3mf_is_published(into_u8(GetHistoryFile(i))); } }); m_load_called = true; @@ -4289,6 +4299,7 @@ void MainFrame::get_recent_projects(boost::property_tree::wptree &tree, int imag std::wstring proj = m_recent_projects.GetHistoryFile(i).ToStdWstring(); item.put(L"project_name", proj.substr(proj.find_last_of(L"/\\") + 1)); item.put(L"path", proj); + item.put(L"published", m_recent_projects.GetPublished(i) ? L"1" : L"0"); boost::system::error_code ec; std::time_t t = boost::filesystem::last_write_time(proj, ec); if (!ec) { diff --git a/src/slic3r/GUI/MainFrame.hpp b/src/slic3r/GUI/MainFrame.hpp index c09a29abb3..278c7e7f30 100644 --- a/src/slic3r/GUI/MainFrame.hpp +++ b/src/slic3r/GUI/MainFrame.hpp @@ -179,6 +179,7 @@ class MainFrame : public DPIFrame { FileHistory(int max) : wxFileHistory(max) {} std::wstring GetThumbnailUrl(int index) const; + bool GetPublished(int index) const; virtual void AddFileToHistory(const wxString &file); virtual void RemoveFileFromHistory(size_t i); @@ -189,6 +190,7 @@ class MainFrame : public DPIFrame void SetMaxFiles(int max); private: std::deque m_thumbnails; + std::deque m_published_files; // parallel to m_thumbnails: is it a published 3mf? bool m_load_called = false; }; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d2c9c3589f..7a3789bc96 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5596,11 +5596,11 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na // Count off filament_is_mixed, not filament_presets or the combos: the extruder-count spinner // reaches this before the sidebar has rebuilt, and update_multi_material_filament_presets() // can have grown filament_presets alone. - auto *bundle = wxGetApp().preset_bundle; - size_t insert_pos = bundle->num_physical_filaments(); - size_t total = insert_pos + bundle->num_mixed_filaments(); - int filament_count = (int)(total + 1); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + auto* bundle = wxGetApp().preset_bundle; + size_t insert_pos = bundle->num_physical_filaments(); + size_t total = insert_pos + bundle->num_mixed_filaments(); + int filament_count = (int) (total + 1); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); bundle->set_num_filaments(filament_count, new_color); // Maintain physical-first ordering: rotate the new slot from end to insert_pos. @@ -7018,7 +7018,7 @@ struct Plater::priv void handle_textured_mesh_import(Slic3r::Model& model, const std::vector& obj_idxs, std::function cancel_callback = {}); fs::path get_export_file_path(GUI::FileType file_type); - wxString get_export_file(GUI::FileType file_type, const wxString& title = {}); + wxString get_export_file(GUI::FileType file_type, const wxString& title = {}, bool published = false); // BBS void load_auxiliary_files(); @@ -9069,7 +9069,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const std::string& key : published_config.skipped_keys) message += "\n-" + key; // Informational: the load succeeded, these keys were skipped. - notify_manager->bbl_show_3mf_warn_notification(message, NotificationManager::NotificationLevel::WarningNotificationLevel); + notify_manager + ->bbl_show_3mf_warn_notification(message, + NotificationManager::NotificationLevel::WarningNotificationLevel); } // BBS: notify the user about slot materials that were replaced while @@ -9080,7 +9082,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ for (const std::string& replacement : published_config.material_replacements) message += "\n-" + replacement; // Informational: the load succeeded, the slots were adapted. - notify_manager->bbl_show_3mf_warn_notification(message, NotificationManager::NotificationLevel::WarningNotificationLevel); + notify_manager + ->bbl_show_3mf_warn_notification(message, + NotificationManager::NotificationLevel::WarningNotificationLevel); } ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type"); @@ -10026,7 +10030,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) return output_file; } -wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title) +wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title, bool published) { wxString wildcard; switch (file_type) { @@ -10060,7 +10064,10 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& break; } case FT_3MF: { - output_file.replace_extension("3mf"); + // A published export is suggested as ".published.3mf" so the role is visible in the + // dialog and in the recent-files list. This is only a pre-filled suggestion; the user's + // typed filename wins, keeping a plain ".3mf" output fully valid. + output_file.replace_extension(published ? "published.3mf" : "3mf"); dlg_title = title.empty() ? _L("Save file as") : title; break; } @@ -18262,7 +18269,7 @@ void Plater::export_core_3mf() int Plater::export_published_3mf(const std::vector& published_keys, const std::vector& material_keys) { - wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:")); + wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:"), true); if (path.empty() || path == "") return wxID_CANCEL; @@ -18397,6 +18404,10 @@ int Plater::export_published_3mf(const std::vector& published_keys, return wxID_CANCEL; } restore_now(); + + // Register the exported file in the "Recently opened" list + wxGetApp().mainframe->add_to_recent_projects(path); + return wxID_YES; } @@ -21869,9 +21880,9 @@ void Plater::show_object_info() int non_manifold_edges = 0; auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); - if (non_manifold_edges > 0) { - info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); - } + if (non_manifold_edges > 0) { + info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); + } info_manifold = "" + info_manifold + ""; info_text += into_u8(info_manifold); diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index fcc2816b06..6b641e34da 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -515,6 +516,24 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) msg->Wrap(-1); w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + // Guide link: opens the (still WIP) Publish 3MF docs in a new window/tab, keeping the dialog open. + wxStaticText* guide_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF Wiki Guide")); + guide_link->SetFont(Label::Body_13); + guide_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); + guide_link->SetCursor(wxCURSOR_HAND); + guide_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { + wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html", wxBROWSER_NEW_WINDOW); + }); + w_sizer->Add(guide_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + + // Placeholder guide link: a Publish 3MF video URL, right below the wiki guide link. + wxStaticText* video_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF YouTube Video (Placeholder)")); + video_link->SetFont(Label::Body_13); + video_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); + video_link->SetCursor(wxCURSOR_HAND); + video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { wxLaunchDefaultBrowser("https://www.youtube.com", wxBROWSER_NEW_WINDOW); }); + w_sizer->Add(video_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); + w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_tabs, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_host, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); From 90d5654db90adec6c1610b83936743e1cd74d238 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 17:56:47 +0800 Subject: [PATCH 44/60] Shifted the guide links to the bottom left of the publish dialog --- src/slic3r/GUI/PublishSettingsDialog.cpp | 41 ++++++++++++------------ 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 6b641e34da..0e8c47b799 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -511,29 +511,11 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) wxBoxSizer* w_sizer = new wxBoxSizer(wxVERTICAL); - wxStaticText* msg = new wxStaticText(this, wxID_ANY, _L("Select which settings to embed in the 3MF file")); + wxStaticText* msg = new wxStaticText(this, wxID_ANY, _L("Select which settings to be published in the 3MF file")); msg->SetFont(Label::Body_13); msg->Wrap(-1); w_sizer->Add(msg, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); - // Guide link: opens the (still WIP) Publish 3MF docs in a new window/tab, keeping the dialog open. - wxStaticText* guide_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF Wiki Guide")); - guide_link->SetFont(Label::Body_13); - guide_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); - guide_link->SetCursor(wxCURSOR_HAND); - guide_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { - wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html", wxBROWSER_NEW_WINDOW); - }); - w_sizer->Add(guide_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); - - // Placeholder guide link: a Publish 3MF video URL, right below the wiki guide link. - wxStaticText* video_link = new wxStaticText(this, wxID_ANY, _L("Publish 3MF YouTube Video (Placeholder)")); - video_link->SetFont(Label::Body_13); - video_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); - video_link->SetCursor(wxCURSOR_HAND); - video_link->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent&) { wxLaunchDefaultBrowser("https://www.youtube.com", wxBROWSER_NEW_WINDOW); }); - w_sizer->Add(video_link, 0, wxRIGHT | wxLEFT | wxTOP, FromDIP(10)); - w_sizer->Add(f_bar, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_tabs, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); w_sizer->Add(m_outer_host, 1, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10)); @@ -556,7 +538,26 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) }); dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); }); - w_sizer->Add(dlg_btns, 0, wxEXPAND); + // Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right). + auto make_link = [this](const wxString& label, const char* url) { + wxStaticText* link = new wxStaticText(this, wxID_ANY, label); + link->SetFont(Label::Body_13); + link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA)); + link->SetCursor(wxCURSOR_HAND); + link->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent&) { wxLaunchDefaultBrowser(url, wxBROWSER_NEW_WINDOW); }); + return link; + }; + wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL); + links_sizer->Add(make_link(_L("Publish 3MF Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, + wxALIGN_LEFT); + links_sizer->Add(make_link(_L("Publish 3MF YouTube Video (Placeholder)"), "https://www.youtube.com"), 0, wxTOP | wxALIGN_LEFT, + FromDIP(4)); + + wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL); + footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL); + footer->AddStretchSpacer(); + footer->Add(dlg_btns, 0, wxALIGN_CENTER_VERTICAL); + w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxBOTTOM | wxEXPAND, FromDIP(10)); SetSizerAndFit(w_sizer); fit_to_content(); // initial size only; the dialog is resizable From 6985075c5b5024ce795f4e28f01c61306d3776e4 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 18:17:28 +0800 Subject: [PATCH 45/60] Change the PUB badge to OrcaSlicer's color --- resources/web/homepage/css/home.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/web/homepage/css/home.css b/resources/web/homepage/css/home.css index 395dcc9356..ff04d3ba3d 100644 --- a/resources/web/homepage/css/home.css +++ b/resources/web/homepage/css/home.css @@ -596,7 +596,7 @@ body font-size: 10px; font-weight: 600; color: #FFFFFF; - background-color: #00AE42; + background-color: #009688; border-radius: 2px; white-space: nowrap; } From d00af63a6153bce963f01b791a593986f6f3047f Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Tue, 1 Sep 2026 18:18:33 +0800 Subject: [PATCH 46/60] Removes identity matching for when slots need to grow without writing type key --- src/libslic3r/PresetBundle.cpp | 99 +++++++++------------------------- 1 file changed, 24 insertions(+), 75 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 688d2b90b0..76211314b0 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5666,81 +5666,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, const size_t new_slot_idx = this->filament_presets.size(); std::string initial_preset; if (published_slots.count(static_cast(new_slot_idx)) != 0) { - // Prefer the best distinct candidate for the slot's published material - // (exact id, then vendor+type, then type only), searching compatible - // presets first and falling back to incompatible ones only when no - // compatible candidate exists... - for (const PublishedMaterialEntry& entry : published_config->material_keys) { - // Scored for every entry with an identity (name / ids / family), - // not only when a Type requirement was checked; candidate_score's - // family tiers fall back to the entry's own filament_type. - if (entry.slot != static_cast(new_slot_idx)) - continue; - const std::string resolved_name = resolved_name_for(entry.slot); - int best_score = -1; - auto scan = [&](bool compatible_only) -> std::pair { - int best_score = -1; - std::string best_name; - for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset& candidate = this->filaments.preset(i); - if (compatible_only && !candidate.is_compatible) - continue; - const int score = candidate_score(candidate, entry, resolved_name); - if (score > best_score) { - // Exact identity tiers (name / setting_id) win even when the - // preset is hidden or already referenced by another slot; the - // alias re-pointing pass below still de-aliases afterwards. - if (score < 3 && (!candidate.is_visible || used_preset_names.count(candidate.name) != 0)) - continue; - best_score = score; - best_name = candidate.name; - } - } - return {best_score, best_name}; - }; - const auto [compat_score, compat_name] = scan(true); - const auto [any_score, any_name] = scan(false); - if (compat_score >= 0) { - best_score = compat_score; - initial_preset = compat_name; - } else if (any_score >= 0) { - best_score = any_score; - initial_preset = any_name; - } - if (best_score >= 0) - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF grew slot " << new_slot_idx << " with " - << initial_preset << " (score " << best_score << ", preset_name \"" - << entry.preset_name << "\", type \"" << entry.publish_type_value << "\")"; - if (best_score >= 0 && best_score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty())) - published_config->material_replacements.emplace_back("slot " + std::to_string(new_slot_idx) + ": " + - initial_preset + " (substitute)"); - break; - } - // ...otherwise any visible preset not already used by another slot, - // preferring the published material's own family when it is known. - if (initial_preset.empty()) { - std::string slot_family; - for (const PublishedMaterialEntry& entry : published_config->material_keys) - if (entry.slot == static_cast(new_slot_idx)) { - slot_family = !entry.publish_type_value.empty() ? entry.publish_type_value : - normalize_filament_type(entry.filament_type); - break; - } - for (size_t i = first_candidate; i < this->filaments.size(); ++i) { - const Preset& candidate = this->filaments.preset(i); - if (!candidate.is_visible || used_preset_names.count(candidate.name) != 0) - continue; - if (!slot_family.empty()) { - const ConfigOptionStrings* types = candidate.config.opt("filament_type"); - const std::string cand_type = (types != nullptr && !types->values.empty()) ? types->get_at(0) : - std::string(); - if (normalize_filament_type(cand_type) != slot_family) - continue; - } - initial_preset = candidate.name; - break; - } - } + // Grow the slot the way the sidebar "add filament" does: seed it with + // the receiver's last preset. + if (!this->filament_presets.empty()) + initial_preset = this->filament_presets.back(); } if (initial_preset.empty()) // Unpublished filler slot, or every visible preset is already used: @@ -5764,6 +5693,26 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, for (size_t slot = 1; slot < this->filament_presets.size(); ++slot) { if (published_slots.count(static_cast(slot)) == 0 || !referenced_elsewhere(this->filament_presets[slot], slot)) continue; + // A slot whose published entry writes nothing into a shared preset in place + // keeps its own preset: de-aliasing would needlessly swap the material. Only + // keys routed to the slot's preset can leak across an aliased slot (colour is + // slot-scoped via project_config; a type requirement is handled by the + // type-gate below; full detaches separately). Mixed-definition keys go to the + // per-slot project arrays, never the preset. + bool writes_preset_keys = false; + for (const PublishedMaterialEntry& entry : published_config->material_keys) { + if (entry.slot != static_cast(slot)) + continue; + for (const std::string& key : entry.keys) + if (mixed_definitions.count(publish_base_key(key)) == 0) { + writes_preset_keys = true; + break; + } + if (writes_preset_keys) + break; + } + if (!writes_preset_keys) + continue; // Prefer the best distinct candidate for the slot's published material // (exact id, then vendor+type, then type only), compatible presets first... std::string replacement; From f85902b0cea6f741b31dd1c488ce1a894eb11ec8 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 2 Sep 2026 10:45:47 +0800 Subject: [PATCH 47/60] Preserving state of publish dialog --- src/slic3r/GUI/MainFrame.cpp | 8 ++- src/slic3r/GUI/Plater.cpp | 38 ++++++++++ src/slic3r/GUI/Plater.hpp | 4 ++ src/slic3r/GUI/PublishSettingsDialog.cpp | 91 +++++++++++++++++++++++- src/slic3r/GUI/PublishSettingsDialog.hpp | 15 +++- 5 files changed, 153 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 7d73d884b0..87e1271678 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1744,9 +1744,15 @@ void MainFrame::publish_project() { if (m_plater == nullptr) return; - PublishSettingsDialog dlg(this); + // Seed the dialog from the session selection (a remembered state or a freshly loaded + // published 3MF); a null pointer means "fresh", keeping the dirty defaults. + std::vector pending_keys; + std::vector pending_material; + const bool has_prior = m_plater->get_pending_published(pending_keys, pending_material); + PublishSettingsDialog dlg(this, has_prior ? &pending_keys : nullptr, has_prior ? &pending_material : nullptr); if (dlg.ShowModal() != wxID_OK) return; + m_plater->set_pending_published(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys()); m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys()); } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 7a3789bc96..b5233b123b 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6750,6 +6750,12 @@ struct Plater::priv SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr; PublishDialog* m_publish_dlg = nullptr; + // Session-level stash of the last published selection. Written on publish and on + // loading a published 3MF; read when the Publish dialog is opened. + bool m_has_pending_published{false}; + std::vector m_pending_published_keys; + std::vector m_pending_material_keys; + // Data Slic3r::DynamicPrintConfig* config; // FIXME: leak? Slic3r::Print fff_print; @@ -9087,6 +9093,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ NotificationManager::NotificationLevel::WarningNotificationLevel); } + // Remember the imported published selection so the Publish dialog is + // pre-seeded with the file's settings. Stored after the load so any + // per-slot relocations are already reflected in material_keys. + if (published_config.published) { + this->m_has_pending_published = true; + this->m_pending_published_keys = published_config.published_keys; + this->m_pending_material_keys = published_config.material_keys; + } + 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(); @@ -10290,6 +10305,11 @@ void Plater::priv::reset(bool apply_presets_change) clear_warnings(); + // A new project must not inherit the previous project's published selection (Feature A/B). + m_has_pending_published = false; + m_pending_published_keys.clear(); + m_pending_material_keys.clear(); + set_project_filename(""); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty"; @@ -18411,6 +18431,24 @@ int Plater::export_published_3mf(const std::vector& published_keys, return wxID_YES; } +bool Plater::get_pending_published(std::vector& out_keys, + std::vector& out_material) const +{ + if (!p->m_has_pending_published) + return false; + out_keys = p->m_pending_published_keys; + out_material = p->m_pending_material_keys; + return true; +} + +void Plater::set_pending_published(const std::vector& published_keys, + const std::vector& material_keys) +{ + p->m_has_pending_published = true; + p->m_pending_published_keys = published_keys; + p->m_pending_material_keys = material_keys; +} + Preset* get_printer_preset(const MachineObject* obj) { if (!obj) diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 2f83da1cb9..26cd06978b 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -520,6 +520,10 @@ public: // Export a "published" 3MF embedding the author-selected settings in the file metadata; a // pure export that leaves the in-memory project untouched. int export_published_3mf(const std::vector& published_keys, const std::vector& material_keys); + // Session-level stash of the last published selection, seeded into the Publish dialog on + // open and written on publish or on loading a published 3MF + bool get_pending_published(std::vector& out_keys, std::vector& out_material) const; + void set_pending_published(const std::vector& published_keys, const std::vector& material_keys); static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function notify_func = {}); void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL); //BBS: remove amf diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 0e8c47b799..d4d9b5d5d2 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -434,7 +434,8 @@ PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_ return spec; } -PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) +PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, const std::vector* published_keys, + const std::vector* material_keys) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, _L("Publish 3MF..."), @@ -522,6 +523,11 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent) build_option_model(); + // Seed from a remembered session selection or a freshly loaded published 3MF (authoritative). + if (published_keys != nullptr || material_keys != nullptr) + apply_selection(published_keys != nullptr ? *published_keys : std::vector(), + material_keys != nullptr ? *material_keys : std::vector()); + auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { @@ -1691,6 +1697,89 @@ void PublishSettingsDialog::show_menu(wxMouseEvent& evt) PopupMenu(&m, local_pos); } +void PublishSettingsDialog::apply_selection(const std::vector& published_keys, + const std::vector& material_keys) +{ + // The supplied selection is authoritative: clear the dirty-default pre-check first so a + // dirty key the user deselected stays off, then re-select exactly what the selection names. + for (Row& row : m_rows) + if (row.check != nullptr) + row.check->SetValue(false); + for (Category& cat : m_categories) { + if (cat.section != Section::Material) + continue; + if (cat.enable_check != nullptr) + cat.enable_check->SetValue(false); + if (cat.full_check != nullptr) + cat.full_check->SetValue(false); + } + + // Print / printer rows: match by exact row key (the full "#N" opt_id where present). + for (const std::string& key : published_keys) + for (Row& row : m_rows) + if ((row.section == Section::Print || row.section == Section::Printer) && row.check != nullptr && row.key == key) { + row.check->SetValue(true); + break; + } + + // Per-slot material selections, applied positionally by slot. + for (const Slic3r::PublishedMaterialEntry& entry : material_keys) { + if (entry.slot < 0) + continue; + bool entry_mixed = false; + for (const std::string& k : entry.keys) + if (publish_mixed_keys().count(publish_base_key(k)) != 0) { + entry_mixed = true; + break; + } + size_t cat_idx = size_t(-1); + for (size_t c = 0; c < m_categories.size(); ++c) { + const Category& cat = m_categories[c]; + if (cat.section != Section::Material || cat.is_mixed != entry_mixed || cat.filament_slot != size_t(entry.slot)) + continue; + cat_idx = c; + break; + } + if (cat_idx == size_t(-1)) + continue; // slot not present in the receiver (out of range / skipped) + Category& cat = m_categories[cat_idx]; + if (cat.enable_check != nullptr) { + cat.enable_check->SetValue(true); + on_enable_toggle(cat_idx); + } + if (entry.full && cat.full_check != nullptr) { + cat.full_check->SetValue(true); + on_full_toggle(cat_idx); + } else { + // Setting rows are keyed by the base key. + for (const std::string& key : entry.keys) { + const std::string base = publish_base_key(key); + for (const size_t r : cat.rows) { + Row& row = m_rows[r]; + if (row.kind == RowKind::Setting && row.key == base) { + row.check->SetValue(true); + break; + } + } + } + if (entry.publish_type && !entry.publish_type_value.empty()) + for (const size_t r : cat.rows) + if (m_rows[r].kind == RowKind::Type && m_rows[r].check != nullptr) { + m_rows[r].check->SetValue(true); + break; + } + if (entry.publish_color && !entry.color.empty()) + for (const size_t r : cat.rows) + if (m_rows[r].kind == RowKind::Color && m_rows[r].check != nullptr) { + m_rows[r].check->SetValue(true); + break; + } + } + } + + apply_visibility(); +} + std::vector PublishSettingsDialog::GetPublishedKeys() const { std::vector out; diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index f2dcf70bba..8d9342fadb 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -46,7 +46,14 @@ struct MixedDependencyIssue class PublishSettingsDialog : public DPIDialog { public: - PublishSettingsDialog(wxWindow* parent = nullptr); + // Optional published selection (Feature A/B): when the caller supplies one (either a + // remembered session selection or the payload of a freshly loaded published 3MF) the dialog + // is seeded from it, overriding the dirty-default pre-check. A non-null pointer to an empty + // selection means "publish nothing" (an intentional empty state); a null pointer means "no + // remembered selection" (keep the dirty defaults). + PublishSettingsDialog(wxWindow* parent = nullptr, + const std::vector* published_keys = nullptr, + const std::vector* material_keys = nullptr); ~PublishSettingsDialog(); // The selected print/printer setting keys (in display order); printer keys carry a '#N' @@ -179,6 +186,12 @@ private: }; void build_option_model(); + // Seed the dialog from a published selection (print/printer keys + per-slot material keys): + // the supplied selection is authoritative - it is applied after the dirty pre-check and + // overrides it, so deselected dirty keys stay off. Rows/slots not present in the selection + // are left unselected. Unknown or out-of-range entries are skipped gracefully. + void apply_selection(const std::vector& published_keys, + const std::vector& material_keys); // 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 From 4654d24f6f1191b30f7a3fd472b763e30994039b Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 2 Sep 2026 12:38:27 +0800 Subject: [PATCH 48/60] Add a OrcaSlicer badge in the thumbnail preview for published 3MF projects. Add a visual indicator in the publish dialog to show that something in the section is toggled --- resources/web/homepage/css/home.css | 16 +++++++++- resources/web/homepage/js/home.js | 3 +- src/slic3r/GUI/PublishSettingsDialog.cpp | 39 ++++++++++++++++++++++++ src/slic3r/GUI/PublishSettingsDialog.hpp | 5 +++ src/slic3r/GUI/Widgets/Button.cpp | 35 +++++++++++++++++++++ src/slic3r/GUI/Widgets/Button.hpp | 7 +++++ src/slic3r/GUI/Widgets/TabCtrl.cpp | 8 +++++ src/slic3r/GUI/Widgets/TabCtrl.hpp | 3 ++ 8 files changed, 114 insertions(+), 2 deletions(-) diff --git a/resources/web/homepage/css/home.css b/resources/web/homepage/css/home.css index ff04d3ba3d..f4ef1d5420 100644 --- a/resources/web/homepage/css/home.css +++ b/resources/web/homepage/css/home.css @@ -552,7 +552,8 @@ body background-color: #E4E4E4; border-radius: 8px; width: 184px; - height: 184px; + height: 184px; + position: relative; } .FileItem img @@ -564,6 +565,19 @@ body object-fit: cover; } +.FileImg .FileLogoBadge +{ + position: absolute; + top: 8px; + left: 8px; + width: 26px; + height: 26px; + padding: 2px; + box-sizing: border-box; + background-color: rgba(255,255,255,0.85); + border-radius: 50%; +} + .FileName { white-space: nowrap; diff --git a/resources/web/homepage/js/home.js b/resources/web/homepage/js/home.js index 3cb5ff4fb3..20eb96474b 100644 --- a/resources/web/homepage/js/home.js +++ b/resources/web/homepage/js/home.js @@ -226,10 +226,11 @@ function ShowRecentFileList( pList ) //let sShortName=sPath.substring(index+1,sPath.length); let sBadge=sPublished? 'PUB':''; + let sLogoBadge=sPublished? '':''; let TmpHtml='
'+ ''+ - '
No Image
'+ + '
No Image'+sLogoBadge+'
'+ '
'+sBadge+'
'+sName+'
'+ '
'+sTime+'
'+ '
'; diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index d4d9b5d5d2..340ca8d02e 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -881,6 +881,7 @@ void PublishSettingsDialog::build_option_model() m_outer_tabs->SelectItem(0); show_outer_page(0); } + refresh_tab_indicators(); } size_t PublishSettingsDialog::section_group_for(Section kind) @@ -1127,6 +1128,7 @@ 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); + current.check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { refresh_tab_indicators(); }); 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 @@ -1161,6 +1163,7 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index) const bool full = cat.full_check->GetValue(); for (size_t r : cat.rows) m_rows[r].check->Enable(!full); + refresh_tab_indicators(); } void PublishSettingsDialog::on_enable_toggle(size_t category_index) @@ -1195,6 +1198,7 @@ void PublishSettingsDialog::on_enable_toggle(size_t category_index) apply_visibility(); if (cat.page != nullptr) cat.page->GetSizer()->Layout(); + refresh_tab_indicators(); } void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedVisualSpec& spec) @@ -1606,6 +1610,7 @@ void PublishSettingsDialog::select_all(bool value) if (m_categories[c].section == Section::Material) on_enable_toggle(c); apply_visibility(); + refresh_tab_indicators(); } bool PublishSettingsDialog::row_is_visible(const Row& row) const @@ -1637,6 +1642,7 @@ void PublishSettingsDialog::select_visible(bool value) m_filter_ctrl->ChangeValue(""); apply_filter(""); // resync visibility and the All/None bar } + refresh_tab_indicators(); } void PublishSettingsDialog::show_menu(wxMouseEvent& evt) @@ -1778,6 +1784,7 @@ void PublishSettingsDialog::apply_selection(const std::vector& publ } apply_visibility(); + refresh_tab_indicators(); } std::vector PublishSettingsDialog::GetPublishedKeys() const @@ -1932,6 +1939,38 @@ std::vector PublishSettingsDialog::GetPublishedM return out; } +bool PublishSettingsDialog::category_has_selection(const Category& cat) const +{ + // A material slot publishes (or not) as a whole, gated by "Enable". + if (cat.section == Section::Material) + return cat.enable_check != nullptr && cat.enable_check->GetValue(); + // Print/Printer categories have no gate: any checked row counts. + for (const size_t r : cat.rows) + if (m_rows[r].check->GetValue()) + return true; + return false; +} + +void PublishSettingsDialog::refresh_tab_indicators() +{ + for (size_t s = 0; s < m_sections.size(); ++s) { + SectionGroup& section = m_sections[s]; + bool any = false; + for (size_t i = 0; i < section.categories.size(); ++i) { + const bool on = category_has_selection(m_categories[section.categories[i]]); + section.tabs->SetItemIndicator(static_cast(i), on); + any = any || on; + } + if (section.mixed_tabs != nullptr) + for (size_t i = 0; i < section.mixed_categories.size(); ++i) { + const bool on = category_has_selection(m_categories[section.mixed_categories[i]]); + section.mixed_tabs->SetItemIndicator(static_cast(i), on); + any = any || on; + } + m_outer_tabs->SetItemIndicator(static_cast(s), any); + } +} + std::vector PublishSettingsDialog::full_keys_for_slot() const { // The canonical filament preset keys minus the structural keys the published overlay must diff --git a/src/slic3r/GUI/PublishSettingsDialog.hpp b/src/slic3r/GUI/PublishSettingsDialog.hpp index 8d9342fadb..f41b3e73a3 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.hpp +++ b/src/slic3r/GUI/PublishSettingsDialog.hpp @@ -213,6 +213,11 @@ private: // "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); + // Whether a category currently publishes something, driving its tab's indicator dot. + // Print/Printer: any row checked. Material: the slot's "Enable" is on. + bool category_has_selection(const Category& cat) const; + // Recompute the indicator dot on every outer/inner tab from the current selection state. + void refresh_tab_indicators(); // Unmet dependencies of enabled mixed-filament slots, one record per (mix, component) pair: // "Enable" not checked on the component, or enabled with neither "Full Publish" nor the // "Type" requirement row checked. Colour is deliberately ignored (the receiver renders the diff --git a/src/slic3r/GUI/Widgets/Button.cpp b/src/slic3r/GUI/Widgets/Button.cpp index 5a5bd89403..22b1c34cab 100644 --- a/src/slic3r/GUI/Widgets/Button.cpp +++ b/src/slic3r/GUI/Widgets/Button.cpp @@ -162,6 +162,15 @@ void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; } +void Button::SetIndicator(bool on) +{ + if (m_show_indicator == on) + return; + m_show_indicator = on; + messureSize(); + Refresh(); +} + void Button::SetVertical(bool vertical) { this->vertical = vertical; @@ -324,6 +333,13 @@ void Button::render(wxDC& dc) szContent.x -= d; } } + if (m_show_indicator) { + const int dot = FromDIP(6); + if (vertical) + szContent.y += dot + FromDIP(6); + else + szContent.x += dot + FromDIP(6); + } // move to center wxRect rcContent = { {0, 0}, size }; if (isCenter) { @@ -364,6 +380,17 @@ void Button::render(wxDC& dc) #endif dc.DrawText(text, pt); } + if (m_show_indicator) { + const int dot = FromDIP(6); // diameter + wxPoint dot_pt; + dot_pt.x = pt.x + (text.IsEmpty() ? 0 : textSize.x) + FromDIP(6) + dot / 2; + // Centre on the content vertically; a bitmap-only (empty-label) tab has no text row. + dot_pt.y = text.IsEmpty() ? rcContent.y + rcContent.height / 2 : pt.y + textSize.y / 2; + const wxColour c = StateColor::darkModeColorFor(m_indicator_color); + dc.SetBrush(wxBrush(c)); + dc.SetPen(wxPen(c)); + dc.DrawCircle(dot_pt, dot / 2); + } } void Button::messureSize() @@ -388,6 +415,14 @@ void Button::messureSize() if (szIcon.y > szContent.y) szContent.y = szIcon.y; } } + if (m_show_indicator) { + // Indicator dot sits to the right of the label: its diameter plus the gap from the text. + const int dot = FromDIP(6); + if (vertical) + szContent.y += dot + FromDIP(6); + else + szContent.x += dot + FromDIP(6); + } wxSize size = szContent + paddingSize * 2; if (minSize.GetHeight() > 0) size.SetHeight(minSize.GetHeight()); diff --git a/src/slic3r/GUI/Widgets/Button.hpp b/src/slic3r/GUI/Widgets/Button.hpp index 19dcd24938..758a9a4009 100644 --- a/src/slic3r/GUI/Widgets/Button.hpp +++ b/src/slic3r/GUI/Widgets/Button.hpp @@ -4,6 +4,7 @@ #include "../wxExtensions.hpp" #include "StaticBox.hpp" #include +#include class ButtonProps { @@ -44,6 +45,8 @@ class Button : public StaticBox bool canFocus = true; bool isCenter = true; bool vertical = false; + bool m_show_indicator = false; + wxColour m_indicator_color = wxColour("#009688"); static const int buttonWidth = 200; static const int buttonHeight = 50; @@ -75,6 +78,10 @@ public: void SetSelected(bool selected = true) { m_selected = selected; } + // Show a small coloured dot to the right of the label (used by TabCtrl tabs to flag that + // the tab's category has a selected/toggled setting). + void SetIndicator(bool on); + // Only meant to be used by inspector, not public API ButtonStyle GetStyle() const { return m_style; } ButtonType GetType() const { return m_type; } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 4090bf9c1b..34de109b8f 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -171,6 +171,14 @@ void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap) relayout(); } +void TabCtrl::SetItemIndicator(unsigned int item, bool on) +{ + if (item >= btns.size()) + return; + btns[item]->SetIndicator(on); + relayout(); +} + bool TabCtrl::GetItemBold(unsigned int item) const { if (item >= btns.size()) diff --git a/src/slic3r/GUI/Widgets/TabCtrl.hpp b/src/slic3r/GUI/Widgets/TabCtrl.hpp index 0d3606aca7..493c4edee5 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.hpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.hpp @@ -46,6 +46,9 @@ public: void SetItemText(unsigned int item, wxString const& value); void SetItemBitmap(unsigned int item, const wxBitmap& bitmap); + // Show/hide the small "has selection" dot next to a tab's text. + void SetItemIndicator(unsigned int item, bool on); + bool GetItemBold(unsigned int item) const; void SetItemBold(unsigned int item, bool bold); From f1719b558064c571485a7c544cef4f3be8141d08 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 2 Sep 2026 14:29:15 +0800 Subject: [PATCH 49/60] Fixes mixed filament growth bug. Fixes unit test --- src/libslic3r/PresetBundle.cpp | 25 ++ tests/libslic3r/test_3mf.cpp | 141 +++++++---- .../libslic3r/test_preset_bundle_loading.cpp | 232 +++++++++++++++--- 3 files changed, 329 insertions(+), 69 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 76211314b0..f1535fca90 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5595,6 +5595,12 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, for (const PublishedMaterialEntry& entry : published_config->material_keys) if (entry.slot >= 0) published_slots.insert(entry.slot); + // Grown slots the receiver creates that carry no published content of their own + // (e.g. an unpublished mixed slot left as a gap by a published mix's authored + // position) at or beyond the receiver's physical capacity. They must not become + // physical filaments (that would overflow the nozzle count): finalized as empty + // mixed placeholders below, like the surplus-material placeholders. + std::set virtual_gap_slots; // Exact-name resolution of each published slot's preset through the collection's // own name machinery: find_preset2 follows renamed_from (vendor profile renames) // and canonical bundle names, and auto-matches removed vendor-generic profiles @@ -5665,6 +5671,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, while (this->filament_presets.size() < target_slots) { const size_t new_slot_idx = this->filament_presets.size(); std::string initial_preset; + if (new_slot_idx >= physical_capacity && published_slots.count(static_cast(new_slot_idx)) == 0) + // An unpublished grown slot past the printer's physical capacity cannot + // host a real filament: finalize it as a virtual placeholder below. + virtual_gap_slots.insert(static_cast(new_slot_idx)); if (published_slots.count(static_cast(new_slot_idx)) != 0) { // Grow the slot the way the sidebar "add filament" does: seed it with // the receiver's last preset. @@ -5927,6 +5937,10 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, for (size_t i = 0; i < this->filament_presets.size(); ++i) if (this->is_mixed_filament(i)) mixed_final_slots.insert(int(i)); + // Unpublished gap slots past the capacity are virtual too: count them in the + // final layout so a published mix whose components collide with one is caught. + for (int gap_slot : virtual_gap_slots) + mixed_final_slots.insert(gap_slot); const size_t mixed_final_slot_count = this->filament_presets.size(); // Finalize a slot as an empty mixed-filament placeholder: mark it virtual with // an intentionally empty definition, and add it to the final-layout set so a @@ -6320,6 +6334,17 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, apply_slot_keys(write_config, preset_keys, entry.slot, material_label); } } + // Unpublished gap slots past the printer's physical capacity: the receiver grew + // them only to reach a published definition, so they must not become physical + // filaments (that would overflow the nozzle count). Finalize each as an empty + // mixed placeholder, exactly like the surplus-material placeholders above. + for (int gap_slot : virtual_gap_slots) + if (!this->is_mixed_filament(size_t(gap_slot))) { + finalize_mixed_placeholder(size_t(gap_slot)); + published_config->material_replacements.emplace_back( + "slot " + std::to_string(gap_slot) + + ": unassigned mixed filament (printer supports only " + std::to_string(physical_capacity) + " filaments)"); + } } } diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index f595926699..d30b40004d 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -866,10 +866,12 @@ SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer ta } } -// 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]") { +// An entry masks the non-published slots to their defaults so publishing slot 1 never leaks slot +// 0's value into the file. Both a full entry (the whole-slot key list) and a partial entry (a +// per-slot key) go through the same masking path in filter_published_config (keys and full_keys +// are filtered identically), so the two forms are exercised together. +SCENARIO("Published entries mask the other slots to their defaults", "[3mf]") { + const bool full = GENERATE(true, false); GIVEN("a full print configuration with two filament slots") { DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); full_cfg.opt("filament_diameter")->values = { 1.75, 1.75 }; @@ -878,45 +880,18 @@ SCENARIO("Full-publish entries filter the whole slot and mask the other slots", // mask can restore it on the non-published slot. full_cfg.opt("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_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); + WHEN("filtering with a published entry for slot 1") { + PublishedMaterialEntry entry; + entry.slot = 1; + if (full) { + entry.full = true; + entry.full_keys = { "filament_flow_ratio" }; + } else { + entry.keys = { "filament_flow_ratio" }; } - THEN("the non-published slot is masked to its default") { - REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[0], Catch::Matchers::WithinAbs(1.0, 1e-6)); - } - THEN("the identity keys stay present") { - REQUIRE(filtered_cfg.option("filament_colour") != nullptr); - } - } - } -} + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { entry }); -// A partial-publish entry (per-slot keys) is masked to the author's slot exactly like a full -// entry, so publishing one slot's retraction does not ship the other slots' values in the file. -SCENARIO("Partial-publish entries mask the other slots like full entries", "[3mf]") { - GIVEN("a full print configuration with two filament slots") { - DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); - full_cfg.opt("filament_diameter")->values = { 1.75, 1.75 }; - full_cfg.opt("filament_colour")->values = { "#111111", "#222222" }; - full_cfg.opt("filament_flow_ratio", true)->values = { 1.02, 0.98 }; - - PublishedMaterialEntry partial_entry; - partial_entry.slot = 1; - partial_entry.keys = { "filament_flow_ratio" }; - - WHEN("filtering with a partial entry for slot 1") { - DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, {}, { partial_entry }); - - THEN("the partial key is present with the author's slot value") { + THEN("the selected key is present with the author's slot value") { REQUIRE(filtered_cfg.option("filament_flow_ratio") != nullptr); REQUIRE_THAT(filtered_cfg.opt("filament_flow_ratio")->values[1], Catch::Matchers::WithinAbs(0.98, 1e-6)); } @@ -1075,3 +1050,87 @@ SCENARIO("Published mixed-filament keys are masked to the author's slot", "[3mf] } } +// The published flag is gated on the exact string "1": any other serialized value means "not +// published", so a receiver never treats a file as published on a loose truthiness check. +TEST_CASE("is_published_3mf_flag accepts only the literal \"1\"", "[3mf]") { + CHECK(is_published_3mf_flag("1")); + CHECK_FALSE(is_published_3mf_flag("0")); + CHECK_FALSE(is_published_3mf_flag("false")); + CHECK_FALSE(is_published_3mf_flag("true")); + CHECK_FALSE(is_published_3mf_flag("")); + CHECK_FALSE(is_published_3mf_flag("YES")); +} + +// bbs_3mf_is_published is the lightweight metadata probe used to decide whether a file was +// produced by the publish feature (GUI "recently published" tracking). It must return true only +// for a file whose metadata carries the flag set to "1", and false for legacy files and for a +// file whose flag is present but not "1" (which loads as a normal, non-published 3MF). +SCENARIO("bbs_3mf_is_published detects only genuinely published 3MFs", "[3mf]") { + auto store_model = [](const std::string &path, const std::string &flag_value, const std::string &keys_value) { + 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(); + model.model_info = std::make_shared(); + // An empty flag_value means "don't write the flag at all" (a legacy file). + if (!flag_value.empty()) + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = flag_value; + model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = keys_value; + ScopedTemporaryDir backup_dir("orca_is_pub"); + model.set_backup_path(backup_dir.string()); + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + StoreParams store_params; + store_params.path = path.c_str(); + store_params.model = &model; + store_params.config = &config; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + REQUIRE(store_bbs_3mf(store_params)); + }; + + GIVEN("a minimal published 3MF whose flag is \"1\"") { + ScopedTemporaryFile temp(".3mf"); + store_model(temp.string(), "1", R"(["layer_height"])"); + WHEN("probed by bbs_3mf_is_published") { + THEN("it is recognized as published") { + CHECK(bbs_3mf_is_published(temp.string())); + } + } + } + GIVEN("a legacy 3MF without any published flag") { + ScopedTemporaryFile temp(".3mf"); + store_model(temp.string(), "", R"(["layer_height"])"); + WHEN("probed by bbs_3mf_is_published") { + THEN("it is not recognized as published") { + CHECK_FALSE(bbs_3mf_is_published(temp.string())); + } + } + } + GIVEN("a 3MF carrying the flag set to \"0\"") { + ScopedTemporaryFile temp(".3mf"); + store_model(temp.string(), "0", R"(["layer_height"])"); + WHEN("probed and loaded") { + THEN("it is not recognized as published") { + CHECK_FALSE(bbs_3mf_is_published(temp.string())); + } + THEN("it loads as a normal, non-published 3MF") { + Model dst_model; + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector project_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + REQUIRE(load_bbs_3mf(temp.string().c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, + &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); + REQUIRE(dst_model.model_info != nullptr); + // The key is present but not "1", so nothing treats the file as published; the + // stored keys still round-trip verbatim. + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "0"); + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] == R"(["layer_height"])"); + release_PlateData_list(dst_plates); + } + } + } +} + diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 17e33f3320..d2d0560be2 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1212,13 +1212,14 @@ TEST_CASE("Published 3MF imports a full material under the author's stripped nam REQUIRE(bundle.filament_presets.size() == 2); CHECK(bundle.filament_presets[0] == "My PETG"); - // The grown slot lands on a freshly created copy carrying the author's slot-1 value; - // the library preset that seeded it stays untouched. + // The grown slot seeds the receiver's last preset ("My PETG"), then the full material + // detaches onto a copy carrying the author's slot-1 value; "Generic PLA @System" is + // left untouched. CHECK(bundle.filament_presets[1] == "Generic PLA"); check_double_vector(bundle.filaments.find_preset("Generic PLA", false, true)->config.opt("filament_retraction_length")->values, { 0.8 }); check_double_vector(bundle.filaments.find_preset("Generic PLA @System", false, true)->config.opt("filament_retraction_length")->values, { 0.5 }); REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 1: Generic PLA @System -> Generic PLA"); + CHECK(pub.material_replacements[0] == "slot 1: My PETG -> Generic PLA"); CHECK(pub.skipped_keys.empty()); } } @@ -1922,7 +1923,7 @@ TEST_CASE("Published 3MF grows the receiver's slots only as far as the published // A published slot is seeded from an unused library preset and the values are written onto it // in place, so the receiver's own material (slot 0) is never overwritten. -TEST_CASE("Published 3MF seeds published slots from unused presets and mutates them in place", "[Preset][Bundle][Published]") +TEST_CASE("Published 3MF grows published slots to the receiver's last preset and recolors it in place", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -1937,8 +1938,9 @@ TEST_CASE("Published 3MF seeds published slots from unused presets and mutates t }; // Receiver with its own material plus one more library preset; author publishes only slot 4 - // (Red). The grown slot is seeded from the unused library preset, so the published red - // recolors that preset in place and never the receiver's own material. + // (Red). Growth always repeats the receiver's last filament ("Add one filament"), so the + // grown slot references the shared "My PLA" preset and the published red recolors it in + // place; the unused "Other PLA" preset is left untouched. PresetBundle bundle; Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); mine.config.opt_string("filament_type", 0u) = "PLA"; @@ -1960,14 +1962,14 @@ TEST_CASE("Published 3MF seeds published slots from unused presets and mutates t bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 4); - // Unpublished filler slots repeat the receiver's last preset ("Add one filament"). + // Every grown slot (published or filler) repeats the receiver's last preset. CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "My PLA"); - // The published slot was seeded from the unused library preset; the published colour was - // written onto it in place, never onto the receiver's own material. - CHECK(bundle.filament_presets[3] == "Other PLA"); - CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#123456" }); - CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + // The published red recolors the shared "My PLA" preset in place; the unused "Other PLA" + // preset is left untouched. + CHECK(bundle.filament_presets[3] == "My PLA"); + CHECK(bundle.filaments.find_preset("My PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#ABCDEF" }); + CHECK(bundle.filaments.find_preset("Other PLA", false, true)->config.opt("filament_colour")->values == std::vector{ "#654321" }); // The project-level colours are sized and seeded for every grown slot. CHECK(bundle.project_config.opt("filament_colour")->values.size() == 4); CHECK(bundle.project_config.opt("filament_colour")->values[1] == "#123456"); @@ -1977,10 +1979,11 @@ TEST_CASE("Published 3MF seeds published slots from unused presets and mutates t CHECK(bundle.project_config.opt("filament_map")->values.size() == 4); } -// Without a checked Type row, a grown published slot is still seeded from the published -// material's identity - an exact filament_id outranks any arbitrary unused preset, and an -// entry carrying only a family constrains the pick to that family. -TEST_CASE("Published 3MF seeds a grown slot by published identity or family without a type requirement", "[Preset][Bundle][Published]") +// Growth always repeats the receiver's last preset; a published slot only lands on its +// material identity when the aliased grown slot is re-pointed (de-alias fires on a preset +// key). Lock the identity priority there: an exact filament_id outranks an arbitrary unused +// preset, and an entry carrying only a family constrains the pick to that family. +TEST_CASE("Published 3MF re-points an aliased grown slot by published identity or family without a type requirement", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -1998,6 +2001,7 @@ TEST_CASE("Published 3MF seeds a grown slot by published identity or family with entry.slot = 2; entry.filament_type = "PLA"; entry.filament_vendor = "Generic"; + entry.keys = { "filament_retraction_length" }; // An unused preset sorting before everything else: an unconstrained pick would take it. PresetBundle bundle; Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); @@ -2022,8 +2026,9 @@ TEST_CASE("Published 3MF seeds a grown slot by published identity or family with REQUIRE(bundle.filament_presets.size() == 3); CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "Zzz PLA"); - // An exact identity match is not a substitute, so nothing is reported. - CHECK(pub.material_replacements.empty()); + // The exact-id preset outranks the type-only "Aaa PLA"; the re-point is reported. + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: My PLA -> Zzz PLA"); } SECTION("a family-only entry picks an unused preset of that family") { @@ -2039,7 +2044,11 @@ TEST_CASE("Published 3MF seeds a grown slot by published identity or family with bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 3); + // The family pick lands on the only PETG preset (the PLA presets and the receiver's own + // material lose), and the re-point is reported. CHECK(bundle.filament_presets[2] == "Bbb PETG"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: My PLA -> Bbb PETG"); } } @@ -3797,6 +3806,103 @@ TEST_CASE("Published 3MF finalizes a mixed filament rejected over a placeholder })); } +// The receiver must not overflow its physical capacity when it grows slots to reach a published +// mixed definition: an unpublished mixed slot that lands as a gap past the nozzle count becomes +// an empty mixed placeholder, not a physical filament. An author with six physical slots (0-5) +// and two tail mixes (slots 6 and 7) publishes only 0-5 and 7; the receiver has four nozzles. +// Slots 4 and 5 become surplus placeholders, slot 7 keeps its authored mix position, and the +// unpublished gap slot 6 is finalized as a virtual placeholder - never a fifth physical slot. +TEST_CASE("Published 3MF turns an unpublished gap slot past the printer's capacity into an empty mixed placeholder", "[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.assign(4, "My PLA"); + bundle.set_num_filaments(4, "#123456"); + auto &printer_config = bundle.printers.get_edited_preset().config; + printer_config.opt("single_extruder_multi_material", true)->value = false; + printer_config.opt("nozzle_diameter", true)->values.assign(4, 0.4); + + // 8 authored slots: 0-5 physical, 6 unpublished mixed, 7 published mixed. The payload masks + // the unpublished slot's mixed flag (filter_published_config), so slot 6 reads as physical. + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = std::vector(8, 1.75); + config.opt("filament_self_index")->values = { 1, 2, 3, 4, 5, 6, 7, 8 }; + config.opt("filament_extruder_variant")->values = std::vector(8, "Direct Drive Standard"); + config.opt("filament_colour")->values = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00", + "#FF00FF", "#00FFFF", "#800080", "#804000" }; + config.opt("filament_type")->values.assign(8, "PLA"); + config.opt("filament_vendor")->values.assign(8, "Generic"); + config.opt("filament_is_mixed")->values = { 0, 0, 0, 0, 0, 0, 0, 1 }; + config.opt("filament_mixed_components")->values = { "", "", "", "", "", "", "", "1,2" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "", "", "", "", "", "0.5,0.5" }; + config.opt("filament_mixed_gradient")->values = std::vector(8, 0); + config.opt("filament_mixed_gradient_range")->values.assign(8, ""); + config.opt("filament_mixed_gradient_curve")->values.assign(8, ""); + config.opt("filament_mixed_gradient_per_part")->values = std::vector(8, 0); + + auto make_full_entry = [](int slot) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.full = true; + entry.full_keys = { "filament_retraction_length" }; + return entry; + }; + auto make_mix_entry = [](int slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.publish_color = true; + entry.color = color; + 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" }; + return entry; + }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_full_entry(0), make_full_entry(1), make_full_entry(2), make_full_entry(3), + make_full_entry(4), make_full_entry(5), make_mix_entry(7, "#804000") }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 8); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 8); + // Slots 0-3 stay physical; 4 and 5 are surplus placeholders; 6 is the unpublished gap; 7 is + // the published mix. All four tail slots are virtual. + for (size_t i = 0; i < 4; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[4]); + CHECK(is_mixed[5]); + CHECK(is_mixed[6]); + CHECK(is_mixed[7]); + // Surplus physical slots (4,5) and the unpublished gap (6) carry no definition; the + // published mix on slot 7 keeps its own. + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 8); + CHECK(components[4].empty()); + CHECK(components[5].empty()); + CHECK(components[6].empty()); + CHECK(components[7] == "1,2"); + // Exactly four physical slots remain (never a fifth past the nozzle count). + size_t physical_count = 0; + for (bool mixed : is_mixed) + if (!mixed) + ++physical_count; + CHECK(physical_count == 4); + // The unpublished gap's conversion is surfaced through the post-import notice. + bool gap_reported = false; + for (const std::string &message : pub.material_replacements) + if (message.find("slot 6: unassigned mixed filament") != std::string::npos) + gap_reported = true; + CHECK(gap_reported); + CHECK(pub.skipped_keys.empty()); +} + // The relocation shifts cells inside the file's per-slot mixed arrays; a payload too short to // actually carry the definition degrades to empty cells, which the definition validation then // reports - an empty mix must not ship as a virtual slot. @@ -3859,10 +3965,12 @@ TEST_CASE("Published 3MF reports a relocated mixed filament whose payload cells bundle.project_config.opt("filament_colour")->values.begin())); } -// A grown slot's material is chosen by identity tiers: exact preset name, then the bare -// name/alias form, then exact setting_id, then exact filament_id, then vendor+type, then type -// only. Each section pits two adjacent tiers against each other. -TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Preset][Bundle][Published]") +// A grown published slot always repeats the receiver's last preset; it can only move to the +// published material's identity when a replacement is warranted (an aliased slot that would +// otherwise leak keys, or a type mismatch). The tier priority candidate_score uses is locked +// here: exact preset name > bare name > exact setting_id > exact filament_id > vendor+type, +// with the lower tiers reported as a substitute. +TEST_CASE("Published 3MF re-points an aliased grown slot's material by identity tiers", "[Preset][Bundle][Published]") { auto make_file_config = [] { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); @@ -3894,6 +4002,7 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr bundle.filament_presets = { "My PLA" }; entry.preset_name = "Authored PLA @Vendor"; + entry.keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; pub.material_keys = { entry }; @@ -3903,8 +4012,11 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr REQUIRE(bundle.filament_presets.size() == 3); CHECK(bundle.filament_presets[1] == "My PLA"); + // The aliased grown slot is re-pointed at the exact-name preset; the bare-name and the + // receiver's own preset lose. CHECK(bundle.filament_presets[2] == "Authored PLA @Vendor"); - CHECK(pub.material_replacements.empty()); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: My PLA -> Authored PLA @Vendor"); } SECTION("a bare name outranks an exact setting_id") @@ -3921,6 +4033,7 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr entry.preset_name = "Authored PLA @Vendor"; // no library preset carries this name entry.setting_id = "SID123"; + entry.keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; pub.material_keys = { entry }; @@ -3931,6 +4044,8 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr REQUIRE(bundle.filament_presets.size() == 3); CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "Authored PLA"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: My PLA -> Authored PLA"); } SECTION("an exact setting_id outranks an exact filament_id") @@ -3949,6 +4064,7 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr entry.preset_name = "Authored PLA @Vendor"; // no library preset carries this name entry.setting_id = "SID123"; entry.filament_id = "GFA00"; + entry.keys = { "filament_retraction_length" }; PublishedConfig pub; pub.published = true; pub.material_keys = { entry }; @@ -3959,22 +4075,26 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr REQUIRE(bundle.filament_presets.size() == 3); CHECK(bundle.filament_presets[1] == "My PLA"); CHECK(bundle.filament_presets[2] == "Bbb PLA"); + REQUIRE(pub.material_replacements.size() == 1); + CHECK(pub.material_replacements[0] == "slot 2: My PLA -> Bbb PLA"); } SECTION("a vendor+type match is reported as a substitute") { PresetBundle bundle; - Preset &mine = add_inmemory_preset(bundle.filaments, "My PLA"); - mine.config.opt_string("filament_type", 0u) = "PLA"; + Preset &mine = add_inmemory_preset(bundle.filaments, "My PETG"); + mine.config.opt_string("filament_type", 0u) = "PETG"; Preset &exact_vendor = add_inmemory_preset(bundle.filaments, "Aaa PLA"); exact_vendor.config.opt_string("filament_type", 0u) = "PLA"; exact_vendor.config.opt_string("filament_vendor", 0u) = "Generic"; Preset &other_vendor = add_inmemory_preset(bundle.filaments, "Zzz PLA"); other_vendor.config.opt_string("filament_type", 0u) = "PLA"; other_vendor.config.opt_string("filament_vendor", 0u) = "Other"; - bundle.filament_presets = { "My PLA" }; + bundle.filament_presets = { "My PETG" }; entry.filament_vendor = "Generic"; // no name or id identity: the family tiers decide + entry.publish_type = true; + entry.publish_type_value = "PLA"; // the grown slot seeds "My PETG" -> the gate reads a mismatch PublishedConfig pub; pub.published = true; pub.material_keys = { entry }; @@ -3983,12 +4103,12 @@ TEST_CASE("Published 3MF scores a grown slot's material by identity tiers", "[Pr bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); REQUIRE(bundle.filament_presets.size() == 3); - CHECK(bundle.filament_presets[1] == "My PLA"); + CHECK(bundle.filament_presets[1] == "My PETG"); // The same-vendor PLA outranks the type-only candidate... CHECK(bundle.filament_presets[2] == "Aaa PLA"); // ...and since it is not an exact material match, the load says so. REQUIRE(pub.material_replacements.size() == 1); - CHECK(pub.material_replacements[0] == "slot 2: Aaa PLA (substitute)"); + CHECK(pub.material_replacements[0] == "slot 2: My PETG -> Aaa PLA (substitute)"); } } @@ -4203,3 +4323,59 @@ TEST_CASE("Published 3MF applies duplicate entries for one slot last-wins", "[Pr CHECK(pub.skipped_keys.empty()); CHECK(pub.material_replacements.empty()); } + +// normalize_filament_type maps "PLA High Speed" onto the canonical family "PLA" (a space- +// separated modifier is dropped) but leaves dash-separated composite types like "PA-CF" intact, +// and passes through unknown types and the empty string unchanged. +TEST_CASE("normalize_filament_type strips a space modifier but keeps dash types", "[Preset][Bundle][Published]") +{ + CHECK(normalize_filament_type("PLA High Speed") == "PLA"); + CHECK(normalize_filament_type("PA-CF") == "PA-CF"); + CHECK(normalize_filament_type("PETG-CF") == "PETG-CF"); + CHECK(normalize_filament_type("PLA") == "PLA"); + CHECK(normalize_filament_type("ABC") == "ABC"); + CHECK(normalize_filament_type("") == ""); +} + +// collect_dirty_settings_keys feeds the Publish dialog's pre-check: it must be the set union of +// the dirty options across the edited print, printer and filament presets. +TEST_CASE("collect_dirty_settings_keys unions the dirty settings from all three presets", "[Preset][Bundle][Published]") +{ + PresetBundle bundle; + // The edited preset is initialised as a copy of the selected (default) preset, so a single + // edit makes exactly that option dirty. deep_diff reports scalar keys by name but per-element + // vector keys as "key#", so a vector edit surfaces as "key#0". + bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.28; + bundle.filaments.get_edited_preset().config.opt("filament_type", true)->values = { "ABS" }; + bundle.printers.get_edited_preset().config.opt("nozzle_diameter", true)->values = { 0.6 }; + + const std::vector dirty = collect_dirty_settings_keys(bundle); + for (const char *key : { "layer_height", "filament_type#0", "nozzle_diameter#0" }) + CHECK(contains_key(dirty, key)); +} + +// The publish denylist and the mixed-key list are single sources of truth for the import path: +// lock their members so a silent edit to either cannot drift away from the contract the import +// and export masks rely on. +TEST_CASE("Published 3MF denylist and mixed-key sets match the import/export contract", "[Preset][Bundle][Published]") +{ + const std::set& structural = publish_structural_keys(); + // Structural / inheritance keys must never be applied onto a receiver's presets. + for (const char *key : { "printer_settings_id", "filament_settings_id", "print_settings_id", + "compatible_printers", "compatible_prints", "compatible_printers_condition", + "compatible_prints_condition", "default_filament_profile", "default_print_profile", + "inherits", "extruder_count", "printer_model", "filament_ids" }) + CHECK(structural.count(key) == 1); + // ...but a per-slot publishable material key is not structural. + CHECK(structural.count("filament_retraction_length") == 0); + CHECK(structural.count("filament_colour") == 0); + + const std::set& mixed = publish_mixed_keys(); + CHECK(mixed == std::set{ + "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" }); + // Mixed keys are project-level arrays, not material-preset keys, so none is structural. + for (const std::string &key : mixed) + CHECK(structural.count(key) == 0); +} From e17f932aff0362199385af9dd1d78ccc77016629 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 2 Sep 2026 17:22:06 +0800 Subject: [PATCH 50/60] Update Publish Guide Links --- src/slic3r/GUI/PublishSettingsDialog.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 340ca8d02e..ed0b00aa29 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -434,7 +434,8 @@ PublishSettingsDialog::MixedVisualSpec PublishSettingsDialog::make_mixed_visual_ return spec; } -PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, const std::vector* published_keys, +PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, + const std::vector* published_keys, const std::vector* material_keys) : DPIDialog(parent ? parent : static_cast(wxGetApp().mainframe), wxID_ANY, @@ -554,10 +555,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent, const std::vector return link; }; wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL); - links_sizer->Add(make_link(_L("Publish 3MF Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, - wxALIGN_LEFT); - links_sizer->Add(make_link(_L("Publish 3MF YouTube Video (Placeholder)"), "https://www.youtube.com"), 0, wxTOP | wxALIGN_LEFT, - FromDIP(4)); + links_sizer->Add(make_link(_L("Publish 3MF Wiki"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, wxALIGN_LEFT); + links_sizer->Add(make_link(_L("Publish 3MF Video Guide"), "https://www.youtube.com/@OfficialOrcaSlicer/videos"), 0, + wxTOP | wxALIGN_LEFT, FromDIP(4)); wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL); footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL); From 85f14673f09939827c210b63629bc5027d2a070a Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Wed, 2 Sep 2026 18:09:23 +0800 Subject: [PATCH 51/60] Only show the currently in used filaments for publishing --- src/slic3r/GUI/PublishSettingsDialog.cpp | 119 ++++++++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index ed0b00aa29..16b0b6201c 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -17,6 +17,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PublishSettings.hpp" #include "libslic3r/FilamentMixer.hpp" +#include "libslic3r/Model.hpp" #include #include @@ -307,6 +308,114 @@ wxBitmap mixed_filament_tab_bitmap(const DynamicPrintConfig& full, size_t slot, return composite; } +// The filament slots (0-based) the current project actually uses, across every plate: base and +// MMU-painted volume extruders, per-object layer-range and support/wall/infill filament +// overrides, and custom-gcode tool changes. Used mixed slots are preserved as themselves (so an +// in-use mix is publishable) while their physical components are added too - a mix always ships +// with the materials of its components even if a component is not referenced directly anywhere. +std::set project_used_filament_slots(const PresetBundle& bundle, const DynamicPrintConfig& full, const Model& model) +{ + std::set used; // 0-based used slots, mixed slots not yet expanded + if (model.objects.empty()) + return used; + + auto add_1based = [&used](int id) { + if (id > 0) + used.insert(size_t(id - 1)); + }; + + const DynamicPrintConfig& print_cfg = bundle.prints.get_edited_preset().config; + const int glb_support_intf = print_cfg.opt_int("support_interface_filament"); + const int glb_support = print_cfg.opt_int("support_filament"); + const int glb_outer_wall = print_cfg.opt_int("outer_wall_filament_id"); + const int glb_inner_wall = print_cfg.opt_int("inner_wall_filament_id"); + const int glb_sparse_infill = print_cfg.opt_int("sparse_infill_filament_id"); + const int glb_internal_solid = print_cfg.opt_int("internal_solid_filament_id"); + const int glb_top_surface = print_cfg.opt_int("top_surface_filament_id"); + const int glb_bottom_surface = print_cfg.opt_int("bottom_surface_filament_id"); + const bool glb_support_on = print_cfg.opt_bool("enable_support") || print_cfg.opt_int("raft_layers") > 0; + + for (ModelObject* mo : model.objects) { + for (ModelVolume* mv : mo->volumes) + for (int e : mv->get_extruders()) + add_1based(e); + + for (const auto& range_entry : mo->layer_config_ranges) + if (const ConfigOption* ext_opt = range_entry.second.option("extruder")) + add_1based(ext_opt->getInt()); + + bool obj_support = glb_support_on; + if (const ConfigOption* s_opt = mo->config.option("enable_support"); s_opt != nullptr) + obj_support = s_opt->getBool(); + else if (const ConfigOption* r_opt = mo->config.option("raft_layers"); r_opt != nullptr) + obj_support = r_opt->getInt() > 0; + + if (obj_support) { + if (const ConfigOption* opt = mo->config.option("support_interface_filament"); opt != nullptr && opt->getInt() != 0) + add_1based(opt->getInt()); + else + add_1based(glb_support_intf); + + if (const ConfigOption* opt = mo->config.option("support_filament"); opt != nullptr && opt->getInt() != 0) + add_1based(opt->getInt()); + else + add_1based(glb_support); + } + + auto obj_id = [&mo](const char* key) { + const ConfigOption* opt = mo->config.option(key); + return (opt != nullptr) ? opt->getInt() : 0; + }; + + int obj_outer_wall = obj_id("outer_wall_filament_id"); + if (obj_outer_wall == 0) + obj_outer_wall = obj_id("inner_wall_filament_id"); + add_1based(obj_outer_wall != 0 ? obj_outer_wall : glb_outer_wall); + + int obj_inner_wall = obj_id("inner_wall_filament_id"); + if (obj_inner_wall == 0) + obj_inner_wall = obj_id("outer_wall_filament_id"); + add_1based(obj_inner_wall != 0 ? obj_inner_wall : glb_inner_wall); + + const int obj_sparse = obj_id("sparse_infill_filament_id"); + add_1based(obj_sparse != 0 ? obj_sparse : glb_sparse_infill); + + const int obj_internal_solid = obj_id("internal_solid_filament_id"); + add_1based(obj_internal_solid != 0 ? obj_internal_solid : glb_internal_solid); + + int obj_top = obj_id("top_surface_filament_id"); + if (obj_top == 0) + obj_top = obj_internal_solid; + add_1based(obj_top != 0 ? obj_top : glb_top_surface); + + int obj_bottom = obj_id("bottom_surface_filament_id"); + if (obj_bottom == 0) + obj_bottom = obj_internal_solid; + add_1based(obj_bottom != 0 ? obj_bottom : glb_bottom_surface); + } + + for (const auto& plate_entry : model.plates_custom_gcodes) + for (const CustomGCode::Item& item : plate_entry.second.gcodes) + if (item.type == CustomGCode::Type::ToolChange) + add_1based(item.extruder); + + // Expand used mixed slots to their physical components and keep each mixed slot itself so the + // dialog shows the mix. A component both used directly and pulled in via a mix is deduped. + const auto* is_mixed_opt = full.opt("filament_is_mixed"); + const auto* comp_opt = full.opt("filament_mixed_components"); + if (is_mixed_opt != nullptr && comp_opt != nullptr && has_any_mixed_filament(is_mixed_opt->values)) { + const std::vector raw(used.begin(), used.end()); + const std::vector expanded = expand_mixed_filaments(raw, is_mixed_opt->values, comp_opt->values); + std::set result(expanded.begin(), expanded.end()); + for (unsigned int s : raw) + if (s < is_mixed_opt->values.size() && is_mixed_opt->values[s]) + result.insert(s); + return result; + } + + return used; +} + } // namespace // Warning shown on OK when an enabled mixed-filament slot relies on a filament that would ship @@ -732,9 +841,15 @@ void PublishSettingsDialog::build_option_model() // settings; their "Enable" toggle always embeds the mix definition (components, // ratios, gradient). Detect them via the project-level flag. const ConfigOptionBools* is_mixed_opt = full.opt("filament_is_mixed"); - // One section per filament slot (a 4-slot printer shows 4 pages), each - // disambiguated by its colour chip and slot identity while showing the bare name. + // Only the slots the project actually uses: base/MMU-painted volume extruders, + // support/wall/infill overrides and custom-gcode tool changes, plus the physical + // components of any in-use mixed slot. Unused slots get no section at all (and are + // therefore never published). One section per used slot, disambiguated by its + // colour chip and slot identity while showing the bare name. + const std::set used_slots = project_used_filament_slots(*bundle, full, wxGetApp().plater()->model()); for (size_t slot = 0; slot < bundle->filament_presets.size(); ++slot) { + if (used_slots.find(slot) == used_slots.end()) + continue; const bool is_mixed = is_mixed_opt != nullptr && slot < is_mixed_opt->size() && is_mixed_opt->values[slot]; const PublishMaterialIdentity identity = material_identity(slot, full); // A mixed slot's title is its component composition (e.g. "1 (60%) + 2 From 8c7160079e73626061eaa6f157c84e9c501461cd Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 3 Sep 2026 13:17:13 +0800 Subject: [PATCH 52/60] Revert clang-format changes then reapplied chagnes for Plater/PresetBundle. Fixed extruder masking incorrectness. Fix warning notifications stacking --- src/libslic3r/Format/bbs_3mf.cpp | 12 +- src/libslic3r/Preset.cpp | 2 + src/libslic3r/PresetBundle.cpp | 3706 ++++---- src/libslic3r/PublishSettings.cpp | 44 +- src/slic3r/GUI/NotificationManager.cpp | 36 +- src/slic3r/GUI/NotificationManager.hpp | 5 +- src/slic3r/GUI/Plater.cpp | 10596 ++++++++++++----------- tests/libslic3r/test_3mf.cpp | 93 + 8 files changed, 7286 insertions(+), 7208 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 3a3ee20e6f..4b9c666ce0 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -6997,8 +6997,16 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // receivers onto a geometry-only fallback whose baked-in popup misreports the // file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify // as From_Other and import the geometry silently. - if (!m_minimal_published) + if (m_minimal_published) { + // metadata_item_map is seeded from the input file's metadata_items above, so a + // project opened from a regular Orca/BBS 3MF still carries the Application / + // OrcaSlicer tags it came with. Erase them: skipping the overwrite is not enough, + // and an empty value would still emit a "present-looking" tag to old receivers. + metadata_item_map.erase(BBL_APPLICATION_TAG); + metadata_item_map.erase(ORCASLICER_TAG); + } else { metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str(); + } } metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF); @@ -7025,7 +7033,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) << xml_escape(item.second) << "\n"; if (item.first == BBL_APPLICATION_TAG) { // The OrcaSlicer tag is only written for files that carry the Application - // tag, which a minimal published 3MF omits (see the map assignment above): + // tag, which a minimal published 3MF erases (see the map assignment above): // the branch below is unreachable in minimal mode. stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" << xml_escape(SoftFever_VERSION) << "\n"; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 6646d98e71..460276c420 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -3096,6 +3096,8 @@ std::string PresetCollection::add_detached_preset(const std::string &name_base, // effects or project-embedded path. lock(); const auto it = this->find_preset_internal(final_name); + if (m_presets.begin() + m_idx_selected >= it) + ++m_idx_selected; Preset &preset = *m_presets.insert(it, stored); preset.name = final_name; preset.vendor = nullptr; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 3e88d67ea6..e4bafd0071 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -43,7 +43,7 @@ // Store the print/filament/printer presets into a "presets" subdirectory of the Slic3rPE config dir. // This breaks compatibility with the upstream Slic3r if the --datadir is used to switch between the two versions. -// #define SLIC3R_PROFILE_USE_PRESETS_SUBDIR +//#define SLIC3R_PROFILE_USE_PRESETS_SUBDIR namespace Slic3r { @@ -51,29 +51,45 @@ namespace Slic3r { // below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync. // s_project_options_published additionally carries wipe_tower_rotation_angle, which normal // loads do not import (it is not listed here): published-only plate geometry. -static std::vector s_project_options{"flush_volumes_vector", "flush_volumes_matrix", - // BBS - "filament_colour", "filament_colour_type", "filament_multi_colour", "wipe_tower_x", - "wipe_tower_y", "curr_bed_type", "flush_multiplier", - // Fast-purge mode: project-level purge control, inert at Default. - "flush_multiplier_fast", "prime_volume_mode", "nozzle_volume_type", "filament_map_mode", - "filament_map", - // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament - // slot resolution survives preset switches. - "filament_volume_map", - // Per-filament physical-nozzle choice the grouping engine writes back; project-level so a - // saved project round-trips the assignment alongside filament_map/filament_volume_map. - "filament_nozzle_map", - // Filament Track Switch device state: whether the switch is installed and ready, and - // whether dynamic per-nozzle filament mapping is active. Persisted with the project and - // restored from a saved 3mf; reset to false on load and set true only by live device sync. - "has_filament_switcher", "enable_filament_dynamic_map", - // Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour: - // which slots are virtual mixes, their component filaments, blend ratios and the optional - // Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup. - "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"}; +static std::vector s_project_options { + "flush_volumes_vector", + "flush_volumes_matrix", + // BBS + "filament_colour", + "filament_colour_type", + "filament_multi_colour", + "wipe_tower_x", + "wipe_tower_y", + "curr_bed_type", + "flush_multiplier", + // Fast-purge mode: project-level purge control, inert at Default. + "flush_multiplier_fast", + "prime_volume_mode", + "nozzle_volume_type", + "filament_map_mode", + "filament_map", + // Per-filament nozzle-volume choice; project-level like filament_map so the per-filament + // slot resolution survives preset switches. + "filament_volume_map", + // Per-filament physical-nozzle choice the grouping engine writes back; project-level so a + // saved project round-trips the assignment alongside filament_map/filament_volume_map. + "filament_nozzle_map", + // Filament Track Switch device state: whether the switch is installed and ready, and + // whether dynamic per-nozzle filament mapping is active. Persisted with the project and + // restored from a saved 3mf; reset to false on load and set true only by live device sync. + "has_filament_switcher", + "enable_filament_dynamic_map", + // Mixed-color filament slots. Project-level parallel arrays indexed like filament_colour: + // which slots are virtual mixes, their component filaments, blend ratios and the optional + // Z-gradient description. Kept with the project so a saved 3mf round-trips the mix setup. + "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" +}; // Project options applied when loading a "published" 3MF project: s_project_options minus the // filament/purge keys, plus wipe_tower_rotation_angle (plate geometry that only published @@ -90,24 +106,25 @@ static std::vector s_project_options{"flush_volumes_vector", "flush // assertions in tests/libslic3r/test_preset_bundle_loading.cpp guard both directions. static std::vector s_project_options_published{"wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle"}; -// Orca: add custom as default -const char* PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; -const char* PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; -const char* PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT = "0.4"; -const char* PresetBundle::ORCA_DEFAULT_FILAMENT = "Generic PLA @System"; -const char* PresetBundle::ORCA_FILAMENT_LIBRARY = "OrcaFilamentLibrary"; -const char* PresetBundle::ORCA_DEFAULT_FILAMENT_PLACEHOLDER = "Default Filament"; +//Orca: add custom as default +const char *PresetBundle::ORCA_DEFAULT_BUNDLE = "Custom"; +const char *PresetBundle::ORCA_DEFAULT_PRINTER_MODEL = "MyKlipper 0.4 nozzle"; +const char *PresetBundle::ORCA_DEFAULT_PRINTER_VARIANT = "0.4"; +const char *PresetBundle::ORCA_DEFAULT_FILAMENT = "Generic PLA @System"; +const char *PresetBundle::ORCA_FILAMENT_LIBRARY = "OrcaFilamentLibrary"; +const char *PresetBundle::ORCA_DEFAULT_FILAMENT_PLACEHOLDER = "Default Filament"; -DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset, - Preset& in_print_preset, - const DynamicPrintConfig& project_config, - std::vector& in_filament_presets, - bool apply_extruder, - std::optional> filament_maps_new, - std::optional> filament_volume_maps_new) +DynamicPrintConfig PresetBundle::construct_full_config( + Preset& in_printer_preset, + Preset& in_print_preset, + const DynamicPrintConfig& project_config, + std::vector& in_filament_presets, + bool apply_extruder, + std::optional> filament_maps_new, + std::optional> filament_volume_maps_new) { - DynamicPrintConfig& printer_config = in_printer_preset.config; - DynamicPrintConfig& print_config = in_print_preset.config; + DynamicPrintConfig &printer_config = in_printer_preset.config; + DynamicPrintConfig &print_config = in_print_preset.config; DynamicPrintConfig out; out.apply(FullPrintConfig::defaults()); @@ -119,7 +136,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset size_t num_filaments = in_filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; - std::vector filament_volume_maps(num_filaments, (int) nvtStandard); + std::vector filament_volume_maps(num_filaments, (int)nvtStandard); ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) @@ -137,7 +154,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset filament_volume_maps.resize(num_filaments, nvtStandard); } - auto* extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); + auto *extruder_diameter = dynamic_cast(out.option("nozzle_diameter")); // Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector. std::vector compatible_printers_condition; std::vector compatible_prints_condition; @@ -146,12 +163,11 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset std::vector print_compatible_printers; // BBS: add logic for settings check between different system presets std::vector different_settings; - std::string different_print_settings, different_printer_settings; + std::string different_print_settings, different_printer_settings; compatible_printers_condition.emplace_back(in_print_preset.compatible_printers_condition()); - const ConfigOptionStrings* compatible_printers = print_config.option("compatible_printers", false); - if (compatible_printers) - print_compatible_printers = compatible_printers->values; + const ConfigOptionStrings *compatible_printers = print_config.option("compatible_printers", false); + if (compatible_printers) print_compatible_printers = compatible_printers->values; // BBS: add logic for settings check between different system presets std::string print_inherits = in_print_preset.inherits(); inherits.emplace_back(print_inherits); @@ -161,7 +177,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset int extruder_count = 1, extruder_volume_type_count = 1; bool different_extruder = false; if (apply_extruder) { - different_extruder = out.support_different_extruders(extruder_count); + different_extruder = out.support_different_extruders(extruder_count); extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); if ((extruder_count > 1) || different_extruder) { @@ -173,13 +189,10 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset // per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing // path composes variant_2 first and is unaffected; changing the order here would alter // long-standing composed values, so any fix must re-baseline them. - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); // update print config related with variants - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); } } @@ -187,9 +200,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset // BBS: update filament config related with variants DynamicPrintConfig filament_config = in_filament_presets[0].config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - filament_options_with_variant, "", "filament_extruder_variant", 1, - filament_maps[0], (NozzleVolumeType) filament_volume_maps[0]); + filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition()); compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition()); @@ -197,13 +208,13 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset inherits.emplace_back(filament_inherits); filament_ids.emplace_back(in_filament_presets[0].filament_id); - std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + std::vector &filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); } else { - std::vector filament_configs; - std::vector filament_presets; - for (const Preset& preset : in_filament_presets) { + std::vector filament_configs; + std::vector filament_presets; + for (const Preset & preset : in_filament_presets) { filament_presets.emplace_back(&preset); filament_configs.emplace_back(&(preset.config)); } @@ -213,45 +224,37 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, - nozzle_volume_types, filament_options_with_variant, "", - "filament_extruder_variant", 1, filament_maps[i], - (NozzleVolumeType) filament_volume_maps[i]); + filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } // loop through options and apply them to the resulting config. std::vector filament_variant_count(num_filaments, 1); - for (const t_config_option_key& key : in_filament_presets[0].config.keys()) { - if (key == "compatible_prints" || key == "compatible_printers") - continue; + for (const t_config_option_key &key : in_filament_presets[0].config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") continue; // Get a destination option. - ConfigOption* opt_dst = out.option(key, false); + ConfigOption *opt_dst = out.option(key, false); if (opt_dst->is_scalar()) { // Get an option, do not create if it does not exist. - const ConfigOption* opt_src = filament_temp_configs.front().option(key); - if (opt_src != nullptr) - opt_dst->set(opt_src); + const ConfigOption *opt_src = filament_temp_configs.front().option(key); + if (opt_src != nullptr) opt_dst->set(opt_src); } else { // BBS - ConfigOptionVectorBase* opt_vec_dst = static_cast(opt_dst); + ConfigOptionVectorBase *opt_vec_dst = static_cast(opt_dst); { if (apply_extruder) { - std::vector filament_opts(num_filaments, nullptr); + std::vector filament_opts(num_filaments, nullptr); // Setting a vector value from all filament_configs. - for (size_t i = 0; i < filament_opts.size(); ++i) - filament_opts[i] = filament_temp_configs[i].option(key); + for (size_t i = 0; i < filament_opts.size(); ++i) filament_opts[i] = filament_temp_configs[i].option(key); opt_vec_dst->set(filament_opts); } else { for (size_t i = 0; i < num_filaments; ++i) { - const ConfigOptionVectorBase* filament_option = static_cast( - filament_temp_configs[i].option(key)); + const ConfigOptionVectorBase *filament_option = static_cast(filament_temp_configs[i].option(key)); if (i == 0) opt_vec_dst->set(filament_option); else opt_vec_dst->append(filament_option); - if (key == "filament_extruder_variant") - filament_variant_count[i] = filament_option->size(); + if (key == "filament_extruder_variant") filament_variant_count[i] = filament_option->size(); } } } @@ -260,14 +263,12 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset if (!apply_extruder) { // append filament_self_index - std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + std::vector &filament_self_indice = out.option("filament_self_index", true)->values; + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); int k = 0; for (size_t i = 0; i < num_filaments; i++) { - for (size_t j = 0; j < filament_variant_count[i]; j++) { - filament_self_indice[k++] = i + 1; - } + for (size_t j = 0; j < filament_variant_count[i]; j++) { filament_self_indice[k++] = i + 1; } } } } @@ -281,10 +282,10 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset // BBS: add logic for settings check between different system presets out.erase("different_settings_to_system"); - static const char* keys[] = {"support_filament", "support_interface_filament"}; + static const char *keys[] = {"support_filament", "support_interface_filament"}; for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { std::string key = std::string(keys[i]); - auto* opt = dynamic_cast(out.option(key, false)); + auto *opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); } @@ -300,28 +301,27 @@ DynamicPrintConfig PresetBundle::construct_full_config(Preset& in_printer_preset out.option("filament_map", true)->values = filament_maps; out.option("filament_volume_map", true)->values = filament_volume_maps; - auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { bool nonempty = false; - for (const std::string& v : values) + for (const std::string &v : values) if (!v.empty()) { nonempty = true; break; } - if (nonempty) - out.set_key_value(key, new ConfigOptionStrings(std::move(values))); + if (nonempty) out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); add_if_some_non_empty(std::move(inherits), "inherits_group"); // BBS: add logic for settings check between different system presets - // add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + //add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); out.option("printer_technology", true)->value = ptFFF; return out; } -std::string PresetBundle::find_preset_vendor(const std::string& preset_name, Preset::Type type) +std::string PresetBundle::find_preset_vendor(const std::string &preset_name, Preset::Type type) { // Get the resources preset directory (contains all bundled vendor profiles) fs::path system_dir = fs::path(Slic3r::resources_dir()) / PRESET_PROFILES_DIR; @@ -347,9 +347,10 @@ std::string PresetBundle::find_preset_vendor(const std::string& preset_name, Pre // instead of the raw profile JSONs, by its cache alone. for (const std::string& vendor_name : vendor_names_in(system_dir)) { const fs::path vendor_json = system_dir / (vendor_name + ".json"); - if (!fs::exists(vendor_json)) { + if (! fs::exists(vendor_json)) { if (VendorCacheFile::carries_preset((system_dir / (vendor_name + ".opc")).string(), vendor_name, type, preset_name)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name << " in vendor cache " << vendor_name; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << preset_name + << " in vendor cache " << vendor_name; return vendor_name; } continue; @@ -385,11 +386,13 @@ std::string PresetBundle::find_preset_vendor(const std::string& preset_name, Pre continue; // Found the preset! Get the vendor name and install the entire bundle - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << p_name << " in vendor bundle " << vendor_name; - + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found preset " << p_name + << " in vendor bundle " << vendor_name; + return vendor_name; } - } catch (const std::exception& e) { + } + catch (const std::exception &e) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to find vendor name for " << preset_name << ": " << e.what(); return ""; } @@ -400,21 +403,11 @@ std::string PresetBundle::find_preset_vendor(const std::string& preset_name, Pre } PresetBundle::PresetBundle() - : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) - , filaments(Preset::TYPE_FILAMENT, - Preset::filament_options(), - static_cast(FullPrintConfig::defaults()), - ORCA_DEFAULT_FILAMENT_PLACEHOLDER) - , sla_materials(Preset::TYPE_SLA_MATERIAL, - Preset::sla_material_options(), - static_cast(SLAFullPrintConfig::defaults())) - , sla_prints(Preset::TYPE_SLA_PRINT, - Preset::sla_print_options(), - static_cast(SLAFullPrintConfig::defaults())) - , printers(Preset::TYPE_PRINTER, - Preset::printer_options(), - static_cast(FullPrintConfig::defaults()), - "Default Printer") + : prints(Preset::TYPE_PRINT, Preset::print_options(), static_cast(FullPrintConfig::defaults())) + , filaments(Preset::TYPE_FILAMENT, Preset::filament_options(), static_cast(FullPrintConfig::defaults()), ORCA_DEFAULT_FILAMENT_PLACEHOLDER) + , sla_materials(Preset::TYPE_SLA_MATERIAL, Preset::sla_material_options(), static_cast(SLAFullPrintConfig::defaults())) + , sla_prints(Preset::TYPE_SLA_PRINT, Preset::sla_print_options(), static_cast(SLAFullPrintConfig::defaults())) + , printers(Preset::TYPE_PRINTER, Preset::printer_options(), static_cast(FullPrintConfig::defaults()), "Default Printer") , physical_printers(PhysicalPrinter::printer_options()) { // The following keys are handled by the UI, they do not have a counterpart in any StaticPrintConfig derived classes, @@ -436,12 +429,12 @@ PresetBundle::PresetBundle() // Set all the nullable values to nils. { auto& default_config = this->filaments.default_preset().config; - for (const std::string& opt_key : default_config.keys()) { - ConfigOption* opt = default_config.optptr(opt_key, false); + for(const std::string& opt_key : default_config.keys()){ + ConfigOption* opt = default_config.optptr(opt_key, false); bool is_override_key = is_filament_extruder_override_key(opt_key); - if (!is_override_key || !opt->nullable()) + if(!is_override_key || !opt->nullable()) continue; - opt->deserialize("nil", ForwardCompatibilitySubstitutionRule::Disable); + opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable); } } @@ -454,16 +447,15 @@ PresetBundle::PresetBundle() this->sla_prints.default_preset().compatible_printers_condition(); this->sla_prints.default_preset().inherits(); - // this->printers.add_default_preset(Preset::sla_printer_options(), static_cast(SLAFullPrintConfig::defaults()), "- default SLA -"); this->printers.preset(1).printer_technology_ref() = ptSLA; + //this->printers.add_default_preset(Preset::sla_printer_options(), static_cast(SLAFullPrintConfig::defaults()), "- default SLA -"); + //this->printers.preset(1).printer_technology_ref() = ptSLA; for (size_t i = 0; i < 1; ++i) { // The following ugly switch is to avoid printers.preset(0) to return the edited instance, as the 0th default is the current one. - Preset& preset = this->printers.default_preset(i); - for (const char* key : {"printer_settings_id", "printer_model", "printer_variant", "thumbnails"}) - preset.config.optptr(key, true); - // if (i == 0) { - preset.config.optptr("default_print_profile", true); - preset.config.option("default_filament_profile", true); + Preset &preset = this->printers.default_preset(i); + for (const char *key : {"printer_settings_id", "printer_model", "printer_variant", "thumbnails"}) preset.config.optptr(key, true); + //if (i == 0) { + preset.config.optptr("default_print_profile", true); + preset.config.option("default_filament_profile", true); //} else { // preset.config.optptr("default_sla_print_profile", true); // preset.config.optptr("default_sla_material_profile", true); @@ -482,29 +474,32 @@ PresetBundle::PresetBundle() this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); } -PresetBundle::PresetBundle(const PresetBundle& rhs) { *this = rhs; } - -PresetBundle& PresetBundle::operator=(const PresetBundle& rhs) +PresetBundle::PresetBundle(const PresetBundle &rhs) { - prints = rhs.prints; - sla_prints = rhs.sla_prints; - filaments = rhs.filaments; - sla_materials = rhs.sla_materials; - printers = rhs.printers; - physical_printers = rhs.physical_printers; + *this = rhs; +} - filament_presets = rhs.filament_presets; - project_config = rhs.project_config; - vendors = rhs.vendors; - obsolete_presets = rhs.obsolete_presets; - m_errors = rhs.m_errors; +PresetBundle& PresetBundle::operator=(const PresetBundle &rhs) +{ + prints = rhs.prints; + sla_prints = rhs.sla_prints; + filaments = rhs.filaments; + sla_materials = rhs.sla_materials; + printers = rhs.printers; + physical_printers = rhs.physical_printers; + + filament_presets = rhs.filament_presets; + project_config = rhs.project_config; + vendors = rhs.vendors; + obsolete_presets = rhs.obsolete_presets; + m_errors = rhs.m_errors; // Adjust Preset::vendor pointers to point to the copied vendors map. - prints.update_vendor_ptrs_after_copy(this->vendors); - sla_prints.update_vendor_ptrs_after_copy(this->vendors); - filaments.update_vendor_ptrs_after_copy(this->vendors); + prints .update_vendor_ptrs_after_copy(this->vendors); + sla_prints .update_vendor_ptrs_after_copy(this->vendors); + filaments .update_vendor_ptrs_after_copy(this->vendors); sla_materials.update_vendor_ptrs_after_copy(this->vendors); - printers.update_vendor_ptrs_after_copy(this->vendors); + printers .update_vendor_ptrs_after_copy(this->vendors); return *this; } @@ -513,13 +508,13 @@ void PresetBundle::reset(bool delete_files) { // Clear the existing presets, delete their respective files. this->vendors.clear(); - this->prints.reset(delete_files); - this->sla_prints.reset(delete_files); - this->filaments.reset(delete_files); + this->prints .reset(delete_files); + this->sla_prints .reset(delete_files); + this->filaments .reset(delete_files); this->sla_materials.reset(delete_files); - this->printers.reset(delete_files); + this->printers .reset(delete_files); // BBS: filament_presets is load from project config, not handled here - // this->filament_presets.clear(); + //this->filament_presets.clear(); if (this->filament_presets.empty()) this->filament_presets.emplace_back(this->filaments.get_selected_preset_name()); this->obsolete_presets.prints.clear(); @@ -532,25 +527,26 @@ void PresetBundle::reset(bool delete_files) void PresetBundle::setup_directories() { boost::filesystem::path data_dir = boost::filesystem::path(Slic3r::data_dir()); - // BBS: change directoties by design + //BBS: change directoties by design std::initializer_list paths = { data_dir, data_dir / "ota", - data_dir / PRESET_SYSTEM_DIR, + data_dir / PRESET_SYSTEM_DIR, data_dir / PRESET_USER_DIR, // Store the print/filament/printer presets at the same location as the upstream Slic3r. - // data_dir / PRESET_SYSTEM_DIR / PRESET_PRINT_NAME, - // data_dir / PRESET_SYSTEM_DIR / PRESET_FILAMENT_NAME, - // data_dir / PRESET_SYSTEM_DIR / PRESET_PRINTER_NAME + //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINT_NAME, + //data_dir / PRESET_SYSTEM_DIR / PRESET_FILAMENT_NAME, + //data_dir / PRESET_SYSTEM_DIR / PRESET_PRINTER_NAME }; - for (const boost::filesystem::path& path : paths) { - boost::filesystem::path subdir = path; + for (const boost::filesystem::path &path : paths) { + boost::filesystem::path subdir = path; subdir.make_preferred(); - if (!boost::filesystem::is_directory(subdir) && !boost::filesystem::create_directory(subdir)) { + if (! boost::filesystem::is_directory(subdir) && + ! boost::filesystem::create_directory(subdir)) { if (boost::filesystem::is_directory(subdir)) { - BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!") % - subdir.string(); - } else + BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!")%subdir.string(); + } + else throw Slic3r::RuntimeError(std::string("Unable to create directory ") + subdir.string()); } } @@ -559,7 +555,7 @@ void PresetBundle::setup_directories() // recursively copy all files and dirs in from_dir to to_dir static void copy_dir(const boost::filesystem::path& from_dir, const boost::filesystem::path& to_dir) { - if (!boost::filesystem::is_directory(from_dir)) + if(!boost::filesystem::is_directory(from_dir)) return; // i assume to_dir.parent surely exists if (!boost::filesystem::is_directory(to_dir)) @@ -583,33 +579,33 @@ void PresetBundle::copy_files(const std::string& from) // list of searched paths based on current directory system in setup_directories() // do not copy cache and snapshots boost::filesystem::path from_data_dir = boost::filesystem::path(from); - // BBS: change directoties by design - std::initializer_list from_dirs = - {// from_data_dir / "vendor", - // Store the print/filament/printer presets at the same location as the upstream Slic3r. - from_data_dir / PRESET_PRINT_NAME, from_data_dir / PRESET_FILAMENT_NAME, from_data_dir / PRESET_PRINTER_NAME}; + //BBS: change directoties by design + std::initializer_list from_dirs= { + //from_data_dir / "vendor", + // Store the print/filament/printer presets at the same location as the upstream Slic3r. + from_data_dir / PRESET_PRINT_NAME, + from_data_dir / PRESET_FILAMENT_NAME, + from_data_dir / PRESET_PRINTER_NAME + }; // copy recursively all files - // BBS: change directoties by design + //BBS: change directoties by design for (const boost::filesystem::path& from_dir : from_dirs) { - copy_dir(from_dir, data_dir / "old" / from_dir.filename()); + copy_dir(from_dir, data_dir /"old"/from_dir.filename()); } } -PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig& config, - ForwardCompatibilitySubstitutionRule substitution_rule, - const PresetPreferences& preferred_selection /* = PresetPreferences()*/) +PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, + const PresetPreferences& preferred_selection/* = PresetPreferences()*/) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%") % substitution_rule % - preferred_selection.printer_model_id; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; const auto startup_t0 = std::chrono::steady_clock::now(); - // BBS: change system config to json + //BBS: change system config to json std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); // BBS load preset from user's folder, load system default if @@ -635,26 +631,35 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig& config, set_calibrate_printer(""); { - const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - startup_t0).count(); + const auto total_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - startup_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms"; } - // BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%") % substitutions.size(); + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); return substitutions; } -// BBS: add function to generate differed preset for save -// the pointer should be freed by the caller +//BBS: add function to generate differed preset for save +//the pointer should be freed by the caller Preset* PresetBundle::get_preset_differed_for_save(Preset& preset) { PresetCollection* preset_collection; - switch (preset.type) { - case Preset::TYPE_PRINT: preset_collection = &(this->prints); break; - case Preset::TYPE_PRINTER: preset_collection = &(this->printers); break; - case Preset::TYPE_FILAMENT: preset_collection = &(this->filaments); break; - default: BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly") % preset.type; return nullptr; + switch(preset.type) { + case Preset::TYPE_PRINT: + preset_collection = &(this->prints); + break; + case Preset::TYPE_PRINTER: + preset_collection = &(this->printers); + break; + case Preset::TYPE_FILAMENT: + preset_collection = &(this->filaments); + break; + default: + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; + return nullptr; } return preset_collection->get_preset_differed_for_save(preset); @@ -664,17 +669,25 @@ int PresetBundle::get_differed_values_to_update(Preset& preset, std::mapprints); break; - case Preset::TYPE_PRINTER: preset_collection = &(this->printers); break; - case Preset::TYPE_FILAMENT: preset_collection = &(this->filaments); break; - default: BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly") % preset.type; return -1; + switch(preset.type) { + case Preset::TYPE_PRINT: + preset_collection = &(this->prints); + break; + case Preset::TYPE_PRINTER: + preset_collection = &(this->printers); + break; + case Preset::TYPE_FILAMENT: + preset_collection = &(this->filaments); + break; + default: + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" invalid type %1%, return directly")%preset.type; + return -1; } return preset_collection->get_differed_values_to_update(preset, key_values); } -// BBS: get vendor's current version +//BBS: get vendor's current version Semver PresetBundle::get_vendor_profile_version(std::string vendor_name) { Semver result_ver; @@ -689,11 +702,12 @@ Semver PresetBundle::get_vendor_profile_version(std::string vendor_name) VendorType PresetBundle::get_current_vendor_type() { - auto t = VendorType::Unknown; - auto config = &printers.get_edited_preset().config; + auto t = VendorType::Unknown; + auto config = &printers.get_edited_preset().config; const auto* printer_model = config->opt("printer_model"); if (printer_model == nullptr) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is " << (config->has("printer_model") ? "not a string" : "missing") + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": printer_model is " + << (config->has("printer_model") ? "not a string" : "missing") << ", vendor type is Unknown"; return t; } @@ -709,11 +723,12 @@ VendorType PresetBundle::get_current_vendor_type() if (!vendor_name.empty()) break; } - if (!vendor_name.empty()) { - if (vendor_name.compare("BBL") == 0) + if (!vendor_name.empty()) + { + if(vendor_name.compare("BBL") == 0) t = VendorType::Marlin_BBL; - - if (vendor_name.compare("Qidi") == 0) + + if(vendor_name.compare("Qidi") == 0) t = VendorType::Klipper_Qidi; } return t; @@ -726,8 +741,7 @@ bool PresetBundle::use_bbl_network() return use_bbl_network; } -bool PresetBundle::use_bbl_device_tab() -{ +bool PresetBundle::use_bbl_device_tab() { if (!is_bbl_vendor()) { return false; } @@ -737,7 +751,7 @@ bool PresetBundle::use_bbl_device_tab() } const auto cfg = printers.get_edited_preset().config; - // Use bbl device tab if printhost webui url is not set + // Use bbl device tab if printhost webui url is not set return cfg.opt_string("print_host_webui").empty(); } @@ -764,8 +778,7 @@ bool PresetBundle::backup_user_folder() const } } -std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, - const std::string& printer_name) const +std::optional PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const { if (filament_id.empty()) return std::nullopt; @@ -775,11 +788,11 @@ std::optional PresetBundle::get_filament_by_filament_id(const for (auto iter = filaments.begin(); iter != filaments.end(); ++iter) { const Preset& filament_preset = *iter; - const auto& config = filament_preset.config; + const auto& config = filament_preset.config; if (filament_preset.filament_id == filament_id) { FilamentBaseInfo info; - info.filament_id = filament_id; - info.is_system = filament_preset.is_system; + info.filament_id = filament_id; + info.is_system = filament_preset.is_system; info.filament_name = filament_preset.alias; if (config.has("filament_is_support")) info.is_support = config.option("filament_is_support")->values[0]; @@ -791,7 +804,7 @@ std::optional PresetBundle::get_filament_by_filament_id(const info.nozzle_temp_range_high = config.option("nozzle_temperature_range_high")->values[0]; if (config.has("nozzle_temperature_range_low")) info.nozzle_temp_range_low = config.option("nozzle_temperature_range_low")->values[0]; - if (config.has("temperature_vitrification")) + if(config.has("temperature_vitrification")) info.temperature_vitrification = config.option("temperature_vitrification")->values[0]; if (!printer_name.empty()) { @@ -800,11 +813,11 @@ std::optional PresetBundle::get_filament_by_filament_id(const if (iter != compatible_printers.end() && config.has("filament_printable")) { info.filament_printable = config.option("filament_printable")->values[0]; if (config.has("filament_extruder_compatibility")) - info.set_filament_extruder_compatibility( - config.option("filament_extruder_compatibility")->values[0]); + info.set_filament_extruder_compatibility(config.option("filament_extruder_compatibility")->values[0]); return info; } - } else { + } + else { return info; } } @@ -812,47 +825,44 @@ std::optional PresetBundle::get_filament_by_filament_id(const return std::nullopt; } -// BBS: load project embedded presets -PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, - ForwardCompatibilitySubstitutionRule substitution_rule) +//BBS: load project embedded presets +PresetsConfigSubstitutions PresetBundle::load_project_embedded_presets(std::vector project_presets, ForwardCompatibilitySubstitutionRule substitution_rule) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(" enter, substitution_rule %1%, preset toltal count %2%") % substitution_rule % - project_presets.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule% project_presets.size(); try { this->prints.load_project_embedded_presets(project_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { this->filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } try { this->printers.load_project_embedded_presets(project_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { errors_cummulative += err.what(); } - // this->update_multi_material_filament_presets(); - // this->update_compatible(PresetSelectCompatibleType::Never); - // Rewrite renamed compatible references before the caller (Plater) selects the project presets. + //this->update_multi_material_filament_presets(); + //this->update_compatible(PresetSelectCompatibleType::Never); + // Rewrite renamed compatible references before the caller (Plater) selects the project presets. this->normalize_compatible_presets(); - if (!errors_cummulative.empty()) + if (! errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - // this->load_selections(config, ""); + //this->load_selections(config, ""); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%") % substitutions.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); return substitutions; } -// BBS: get current project embedded presets +//BBS: get current project embedded presets std::vector PresetBundle::get_current_project_embedded_presets() { std::vector project_presets; @@ -866,38 +876,39 @@ std::vector PresetBundle::get_current_project_embedded_presets() if (!printer_presets.empty()) std::copy(printer_presets.begin(), printer_presets.end(), std::back_inserter(project_presets)); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned project_presets count %1%") % project_presets.size(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, returned project_presets count %1%")%project_presets.size(); return project_presets; } -// BBS: reset project embedded presets +//BBS: reset project embedded presets void PresetBundle::reset_project_embedded_presets() { std::string prefer_printer; Preset& current_printer = this->printers.get_selected_preset(); - ConfigOption* inherits = current_printer.config.option("inherits"); + ConfigOption* inherits = current_printer.config.option("inherits"); if (inherits) { - prefer_printer = dynamic_cast(inherits)->value; + prefer_printer = dynamic_cast(inherits)->value; } - // first printer, then filament, then print - bool printer_reselect = this->printers.reset_project_embedded_presets(); + //first printer, then filament, then print + bool printer_reselect = this->printers.reset_project_embedded_presets(); bool filament_reselect = this->filaments.reset_project_embedded_presets(); - bool print_reselect = this->prints.reset_project_embedded_presets(); + bool print_reselect = this->prints.reset_project_embedded_presets(); if (printer_reselect) { if (!prefer_printer.empty()) - this->printers.select_preset_by_name(prefer_printer, true); + this->printers.select_preset_by_name(prefer_printer, true); else - this->printers.select_preset(this->printers.first_visible_idx()); + this->printers.select_preset(this->printers.first_visible_idx()); - // this->update_multi_material_filament_presets(); + //this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - } else if (filament_reselect || print_reselect) { - // Preset& current_printer = this->printers.get_selected_preset(); + } + else if (filament_reselect || print_reselect) { + //Preset& current_printer = this->printers.get_selected_preset(); /*if (filament_reselect) { - const std::vector &prefered_filament_profiles = - current_printer.config.option("default_filament_profile")->values; const std::string prefered_filament_profile - = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); if (!prefered_filament_profile.empty()) + const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); + if (!prefered_filament_profile.empty()) this->filaments.select_preset_by_name(prefered_filament_profile, true); else this->filaments.select_preset(this->filaments.first_visible_idx()); @@ -908,18 +919,17 @@ void PresetBundle::reset_project_embedded_presets() this->update_compatible(PresetSelectCompatibleType::Never); } - // this->update_multi_material_filament_presets(); + //this->update_multi_material_filament_presets(); - // update filament_presets - for (size_t i = 0; i < filament_presets.size(); ++i) { + //update filament_presets + for (size_t i = 0; i < filament_presets.size(); ++ i) + { Preset* selected_filament = this->filaments.find_preset(filament_presets[i], false); if (!selected_filament) { - // it should be the project embedded presets + //it should be the project embedded presets Preset& current_printer = this->printers.get_selected_preset(); - const std::vector& prefered_filament_profiles = - current_printer.config.option("default_filament_profile")->values; - const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : - prefered_filament_profiles.front(); + const std::vector &prefered_filament_profiles = current_printer.config.option("default_filament_profile")->values; + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); if (!prefered_filament_profile.empty()) { // Check if preferred filament exists and is visible const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profile, false); @@ -935,22 +945,26 @@ void PresetBundle::reset_project_embedded_presets() } } -// BBS: get bed texture for printer model +//BBS: get bed texture for printer model std::string PresetBundle::get_texture_for_printer_model(std::string model_name) { std::string texture_name, vendor_name, out; - for (auto vendor_profile : this->vendors) { - for (auto vendor_model : vendor_profile.second.models) { - if (vendor_model.name == model_name || vendor_model.id == model_name) { + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name || vendor_model.id == model_name) + { texture_name = vendor_model.bed_texture; - vendor_name = vendor_profile.first; + vendor_name = vendor_profile.first; break; } } } - if (!texture_name.empty()) { + if (!texture_name.empty()) + { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + texture_name; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + texture_name; @@ -959,22 +973,26 @@ std::string PresetBundle::get_texture_for_printer_model(std::string model_name) return out; } -// BBS: get stl model for printer model +//BBS: get stl model for printer model std::string PresetBundle::get_stl_model_for_printer_model(std::string model_name) { std::string stl_name, vendor_name, out; - for (auto vendor_profile : this->vendors) { - for (auto vendor_model : vendor_profile.second.models) { - if (vendor_model.name == model_name) { - stl_name = vendor_model.bed_model; + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name) + { + stl_name = vendor_model.bed_model; vendor_name = vendor_profile.first; break; } } } - if (!stl_name.empty()) { + if (!stl_name.empty()) + { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + stl_name; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + stl_name; @@ -987,23 +1005,27 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n { std::string hotend_stl, vendor_name, out; - for (auto vendor_profile : this->vendors) { - for (auto vendor_model : vendor_profile.second.models) { - if (vendor_model.name == model_name) { - hotend_stl = vendor_model.hotend_model; + for (auto vendor_profile: this->vendors) + { + for (auto vendor_model: vendor_profile.second.models) + { + if (vendor_model.name == model_name) + { + hotend_stl = vendor_model.hotend_model; vendor_name = vendor_profile.first; break; } } } - if (!hotend_stl.empty()) { + if (!hotend_stl.empty()) + { out = Slic3r::data_dir() + "/vendor/" + vendor_name + "/" + hotend_stl; if (!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/" + vendor_name + "/" + hotend_stl; } - if (out.empty() || !boost::filesystem::exists(boost::filesystem::path(out))) + if (out.empty() ||!boost::filesystem::exists(boost::filesystem::path(out))) out = Slic3r::resources_dir() + "/profiles/hotend.stl"; return out; @@ -1016,13 +1038,11 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For std::string errors_cummulative; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); - if (!fs::exists(user_folder)) - fs::create_directory(user_folder); + if (!fs::exists(user_folder)) fs::create_directory(user_folder); std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user; - fs::path folder(user_folder / user); - if (!fs::exists(folder)) - fs::create_directory(folder); + fs::path folder(user_folder / user); + if (!fs::exists(folder)) fs::create_directory(folder); bundles.WriteLock(); bundles.m_bundles.clear(); @@ -1035,36 +1055,30 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For if (fs::exists(local_dir)) { dir_user_presets_local = local_dir; for (auto& entry : fs::directory_iterator(local_dir)) { - if (!fs::is_directory(entry.path())) - continue; + if (!fs::is_directory(entry.path())) continue; std::string bundle_dir = entry.path().string(); fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; - if (!fs::exists(metadata_file)) - continue; + if (!fs::exists(metadata_file)) continue; BundleMetadata metadata; - if (!metadata.load_from_json(metadata_file.string())) - continue; + if (!metadata.load_from_json(metadata_file.string())) continue; metadata.print_presets.clear(); metadata.filament_presets.clear(); metadata.printer_presets.clear(); - this->prints.load_presets( - bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); - this->filaments.load_presets( - bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); - this->printers.load_presets( - bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.print_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.filament_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.printer_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); metadata.bundle_type = BundleType::Local; - metadata.path = metadata_file.string(); + metadata.path = metadata_file.string(); bundles.WriteLock(); bundles.m_bundles[metadata.id] = metadata; @@ -1076,38 +1090,32 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For fs::path subscribed_dir(folder / PRESET_SUBSCRIBED_DIR); if (fs::exists(subscribed_dir)) { for (auto& entry : fs::directory_iterator(subscribed_dir)) { - if (!fs::is_directory(entry.path())) - continue; + if (!fs::is_directory(entry.path())) continue; std::string bundle_dir = entry.path().string(); fs::path metadata_file = entry.path() / PRESET_BUNDLE_METADATA; - if (!fs::exists(metadata_file)) - continue; + if (!fs::exists(metadata_file)) continue; BundleMetadata metadata; - if (!metadata.load_from_json(metadata_file.string())) - continue; + if (!metadata.load_from_json(metadata_file.string())) continue; metadata.print_presets.clear(); metadata.filament_presets.clear(); metadata.printer_presets.clear(); metadata.is_subscribed = true; - this->prints.load_presets( - bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.print_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - this->filaments.load_presets( - bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - this->printers.load_presets( - bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, - [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); }, - PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.print_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.filament_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { + metadata.printer_presets.push_back(preset.name); + }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); - metadata.bundle_type = BundleType::Subscribed; - metadata.path = metadata_file.string(); + metadata.bundle_type = BundleType::Subscribed; + metadata.path = metadata_file.string(); metadata.update_available = false; bundles.WriteLock(); @@ -1125,32 +1133,27 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For std::string sel = prints.get_selected_preset().name; this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule); prints.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { - errors_cummulative += err.what(); - } + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { std::string sel = filaments.get_selected_preset().name; this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule); filaments.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { - errors_cummulative += err.what(); - } + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } try { std::string sel = printers.get_selected_preset().name; this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule); printers.select_preset_by_name(sel, false); - } catch (const std::runtime_error& err) { - errors_cummulative += err.what(); - } - if (!errors_cummulative.empty()) - throw Slic3r::RuntimeError(errors_cummulative); + } catch (const std::runtime_error& err) { errors_cummulative += err.what(); } + if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - const auto json_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - json_t0).count(); + const auto json_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - json_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms"; } { - const auto ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - user_load_t0).count(); + const auto ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - user_load_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms"; } @@ -1161,70 +1164,62 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For return PresetsConfigSubstitutions(); } -PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig& config, - std::map>& my_presets, - ForwardCompatibilitySubstitutionRule substitution_rule) +PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & config, + std::map> &my_presets, + ForwardCompatibilitySubstitutionRule substitution_rule) { // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; bool process_added = false, filament_added = false, machine_added = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" enter, substitution_rule %1%, preset toltal count %2%") % substitution_rule % - my_presets.size(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" print's selected_idx %1%, selected_name %2%") % prints.get_selected_idx() % - prints.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" filament's selected_idx %1%, selected_name %2%") % filaments.get_selected_idx() % - filaments.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" printers's selected_idx %1%, selected_name %2%") % printers.get_selected_idx() % - printers.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preset toltal count %2%")%substitution_rule%my_presets.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" print's selected_idx %1%, selected_name %2%") %prints.get_selected_idx() %prints.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" filament's selected_idx %1%, selected_name %2%") %filaments.get_selected_idx() %filaments.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" printers's selected_idx %1%, selected_name %2%") %printers.get_selected_idx() %printers.get_selected_preset_name(); // Sync removing remove_users_preset(config, &my_presets); std::map>::iterator it; for (int pass = 0; pass < 2; ++pass) - for (it = my_presets.begin(); it != my_presets.end(); it++) { - std::string name = it->first; - std::map& value_map = it->second; - // Load user root presets at first pass - std::map::iterator inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); - if ((pass == 1) == (inherits_iter == value_map.end() || inherits_iter->second.empty())) - continue; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " start load from cloud: " << name; - // get the type first - std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); - if (type_iter == value_map.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%") % name; - continue; + for (it = my_presets.begin(); it != my_presets.end(); it++) { + std::string name = it->first; + std::map& value_map = it->second; + // Load user root presets at first pass + std::map::iterator inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); + if ((pass == 1) == (inherits_iter == value_map.end() || inherits_iter->second.empty())) + continue; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " start load from cloud: " << name; + //get the type first + std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); + if (type_iter == value_map.end()) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; + continue; + } + try { + PresetCollection *preset_collection = nullptr; + if (type_iter->second == PRESET_IOT_PRINT_TYPE) { + preset_collection = &(this->prints); + process_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); } - try { - PresetCollection* preset_collection = nullptr; - if (type_iter->second == PRESET_IOT_PRINT_TYPE) { - preset_collection = &(this->prints); - process_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, - PresetOrigin(PresetOrigin::Kind::User)); - } else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { - preset_collection = &(this->filaments); - filament_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, - PresetOrigin(PresetOrigin::Kind::User)); - } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { - preset_collection = &(this->printers); - machine_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, - PresetOrigin(PresetOrigin::Kind::User)); - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format("invalid type %1% for setting %2%") % type_iter->second % name; - continue; - } - } catch (const std::runtime_error& err) { - errors_cummulative += err.what(); + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + preset_collection = &(this->filaments); + filament_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + preset_collection = &(this->printers); + machine_added |= preset_collection->load_user_preset(name, value_map, substitutions, substitution_rule, PresetOrigin(PresetOrigin::Kind::User)); + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; + continue; } } + catch (const std::runtime_error& err) { + errors_cummulative += err.what(); + } + } /*if (process_added) { this->prints.update_after_user_presets_loaded(); } @@ -1240,38 +1235,37 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig& config, this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); - // this->load_selections(config, PresetPreferences()); + //this->load_selections(config, PresetPreferences()); set_calibrate_printer(""); - if (!errors_cummulative.empty()) + if (! errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(" finished, process_added %1%, filament_added %2%, machine_added %3%") % process_added % - filament_added % machine_added; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, process_added %1%, filament_added %2%, machine_added %3%")%process_added %filament_added %machine_added; return substitutions; } -bool PresetBundle::apply_vendor_config(const std::map>>& new_vendors, - const std::map& new_filaments, - AppConfig* app_config, - bool overwrite, - const std::string& preferred_printer_model, - const std::string& preferred_printer_variant, - const std::string& preferred_filament) +bool PresetBundle::apply_vendor_config( + const std::map>>& new_vendors, + const std::map& new_filaments, + AppConfig* app_config, + bool overwrite, + const std::string& preferred_printer_model, + const std::string& preferred_printer_variant, + const std::string& preferred_filament) { namespace fs = boost::filesystem; // Get current configuration from AppConfig - const auto old_vendors = app_config->vendors(); - const auto old_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) ? - app_config->get_section(AppConfig::SECTION_FILAMENTS) : - std::map(); + const auto old_vendors = app_config->vendors(); + const auto old_filaments = app_config->has_section(AppConfig::SECTION_FILAMENTS) + ? app_config->get_section(AppConfig::SECTION_FILAMENTS) + : std::map(); // Find vendors that need installation std::vector install_bundles; - for (const auto& it : new_vendors) { + for (const auto &it : new_vendors) { if (it.second.size() > 0) { if (!is_vendor_installed(it.first)) { install_bundles.emplace_back(it.first); @@ -1295,9 +1289,9 @@ bool PresetBundle::apply_vendor_config(const std::mapsecond.empty(); + static const std::string system_suffix = " @System"; + auto it_default = new_vendors.find(PresetBundle::ORCA_DEFAULT_BUNDLE); + bool has_default_bundle_printer = it_default != new_vendors.end() && !it_default->second.empty(); // Check if any non-default vendor has selected printers bool has_vendor_printer = false; @@ -1325,8 +1319,7 @@ bool PresetBundle::apply_vendor_config(const std::mapvendors.end()) { // Vendor is loaded, check if the filament exists for (auto f : vendor_it->second.default_filaments) { - BOOST_LOG_TRIVIAL(info) - << " checking if vendor filament " << f << " matches " << short_name << "(" << name << ")"; + BOOST_LOG_TRIVIAL(info) << " checking if vendor filament " << f << " matches " << short_name << "(" << name << ")"; if (f.find(short_name) != std::string::npos) { BOOST_LOG_TRIVIAL(info) << name << " has filament from vendor: " << vendor; has_vendor_filament = true; @@ -1356,7 +1349,8 @@ bool PresetBundle::apply_vendor_config(const std::mapset_section(AppConfig::SECTION_FILAMENTS, supplemented_filaments); app_config->set_vendors(new_vendors); - } else { + } + else { // Merge filaments std::map merged_filaments = old_filaments; for (const auto& [name, value] : supplemented_filaments) { @@ -1378,7 +1372,7 @@ bool PresetBundle::apply_vendor_config(const std::mapload_presets(*app_config, ForwardCompatibilitySubstitutionRule::Enable, - {preferred_printer_model, preferred_printer_variant, preferred_filament, std::string()}); + {preferred_printer_model, preferred_printer_variant, preferred_filament, std::string()}); // Ensure active filament compatibility // If the active filament is not in the wizard-selected filaments, switch to the first @@ -1404,56 +1398,49 @@ bool PresetBundle::apply_vendor_config(const std::map& files, - std::function override_confirm, - ForwardCompatibilitySubstitutionRule rule, - AppConfig& config) +PresetsConfigSubstitutions PresetBundle::import_presets(std::vector & files, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + AppConfig& config) { bundles.PauseRead(); // Pause threads from reading BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " entry"; PresetsConfigSubstitutions substitutions; int overwrite = 0; - std::vector result; - std::string user_id = config.get("preset_folder"); + std::vector result; + std::string user_id = config.get("preset_folder"); if (user_id.empty()) user_id = DEFAULT_USER_FOLDER_NAME; this->update_user_presets_directory(user_id); - for (auto& file : files) { + for (auto &file : files) { if (Slic3r::is_json_file(file)) { import_json_presets(substitutions, file, override_confirm, rule, overwrite, result); } // Determine if it is a preset bundle - if (boost::iends_with(file, ".orca_printer") || boost::iends_with(file, ".orca_bundle") || - boost::iends_with(file, ".orca_filament") || boost::iends_with(file, ".zip")) { + if (boost::iends_with(file, ".orca_printer") || boost::iends_with(file, ".orca_bundle") || boost::iends_with(file, ".orca_filament") || boost::iends_with(file, ".zip")) { boost::system::error_code ec; // create user folder fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); - if (!fs::exists(user_folder)) - fs::create_directory(user_folder, ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + if (!fs::exists(user_folder)) fs::create_directory(user_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); // create default folder fs::path configs_folder(user_folder / user_id); - if (!fs::exists(configs_folder)) - fs::create_directory(configs_folder, ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); - // create temp folder - // std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp"; + if (!fs::exists(configs_folder)) fs::create_directory(configs_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + //create temp folder + //std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp"; fs::path temp_folder(configs_folder / "temp"); std::string user_default_temp_dir = temp_folder.make_preferred().string(); - if (fs::exists(temp_folder)) - fs::remove_all(temp_folder); + if (fs::exists(temp_folder)) fs::remove_all(temp_folder); fs::create_directory(temp_folder, ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message(); file = boost::filesystem::path(file).make_preferred().string(); mz_zip_archive zip_archive; mz_zip_zero_struct(&zip_archive); mz_bool status; - FILE* zipFile = boost::nowide::fopen(file.c_str(), "rb"); + FILE *zipFile = boost::nowide::fopen(file.c_str(), "rb"); status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH); if (MZ_FALSE == status) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive"; @@ -1467,19 +1454,17 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector bool has_bundle_structure = false; BundleMetadata metadata; fs::path metadata_path = temp_folder / BUNDLE_STRUCTURE_JSON_NAME; - status = mz_zip_reader_extract_file_to_file(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, - encode_path(metadata_path.string().c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); + status = mz_zip_reader_extract_file_to_file(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, encode_path(metadata_path.string().c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); if (status) { if (metadata.load_from_json(metadata_path.string())) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found bundle_id: " << metadata.id << " from " - << BUNDLE_STRUCTURE_JSON_NAME; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Found bundle_id: " << metadata.id << " from " << BUNDLE_STRUCTURE_JSON_NAME; has_bundle_structure = true; } } if (has_bundle_structure && metadata.id.empty()) { boost::uuids::uuid uuid = boost::uuids::random_generator()(); - metadata.id = to_string(uuid); + metadata.id = to_string(uuid); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " bundle_id was empty, so generating a UUID: " << metadata.id; } @@ -1491,11 +1476,9 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector if (!fs::exists(bundle_base_dir)) fs::create_directories(bundle_base_dir, ec); if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to create bundle directory: " << bundle_base_dir.string() - << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " Failed to create bundle directory: " << bundle_base_dir.string() << " error: " << ec.message(); } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << " No bundle_structure.json found, importing presets into the user preset directory"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No bundle_structure.json found, importing presets into the user preset directory"; } // Extract Files @@ -1510,21 +1493,18 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector if (std::string::npos != index) { file_name = file_name.substr(index + 1); } - if (BUNDLE_STRUCTURE_JSON_NAME == file_name) - continue; + if (BUNDLE_STRUCTURE_JSON_NAME == file_name) continue; // create target file path std::string target_file_path = boost::filesystem::path(temp_folder / file_name).make_preferred().string(); - status = mz_zip_reader_extract_to_file(&zip_archive, i, encode_path(target_file_path.c_str()).c_str(), - MZ_ZIP_FLAG_CASE_SENSITIVE); + status = mz_zip_reader_extract_to_file(&zip_archive, i, encode_path(target_file_path.c_str()).c_str(), MZ_ZIP_FLAG_CASE_SENSITIVE); // target file is opened if (MZ_FALSE == status) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to open target file: " << target_file_path; } else { bool is_success = import_json_presets(substitutions, target_file_path, override_confirm, rule, overwrite, result, has_bundle_structure ? bundle_base_dir.string() : std::string()); - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success; } } } @@ -1542,8 +1522,9 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Saved bundle metadata to: " << metadata_save_path.string(); metadata.bundle_type = BundleType::Local; - metadata.path = metadata_save_path.string(); + metadata.path = metadata_save_path.string(); // Store the bundle metadata in m_bundles for tracking + bundles.WriteLock(); bundles.m_bundles[metadata.id] = metadata; @@ -1554,10 +1535,8 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector } fclose(zipFile); - if (fs::exists(temp_folder)) - fs::remove_all(temp_folder, ec); - if (ec) - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " remove directory failed: " << ec.message(); + if (fs::exists(temp_folder)) fs::remove_all(temp_folder, ec); + if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " remove directory failed: " << ec.message(); } } bundles.UnpauseRead(); @@ -1565,37 +1544,38 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector return substitutions; } -bool PresetBundle::import_json_presets(PresetsConfigSubstitutions& substitutions, - std::string& file, - std::function override_confirm, - ForwardCompatibilitySubstitutionRule rule, - int& overwrite, - std::vector& result, - const std::string& bundle_dir) +bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & substitutions, + std::string & file, + std::function override_confirm, + ForwardCompatibilitySubstitutionRule rule, + int & overwrite, + std::vector & result, + const std::string & bundle_dir) { try { DynamicPrintConfig config; // BBS: change to json format // ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule); std::map key_values; - std::string reason; - ConfigSubstitutions config_substitutions = config.load_from_json(file, rule, key_values, reason); - std::string name = key_values[BBL_JSON_KEY_NAME]; - std::string version_str = key_values[BBL_JSON_KEY_VERSION]; - boost::optional version = Semver::parse(version_str); - if (!version) - return false; + std::string reason; + ConfigSubstitutions config_substitutions = config.load_from_json(file, rule, key_values, reason); + std::string name = key_values[BBL_JSON_KEY_NAME]; + std::string version_str = key_values[BBL_JSON_KEY_VERSION]; + boost::optional version = Semver::parse(version_str); + if (!version) return false; - std::string type_subdir; // also note the type subdir for bundles - PresetCollection* collection = nullptr; + std::string type_subdir; // also note the type subdir for bundles + PresetCollection *collection = nullptr; if (config.has("printer_settings_id")) { - collection = &printers; + collection = &printers; type_subdir = PRESET_PRINTER_NAME; - } else if (config.has("print_settings_id")) { - collection = &prints; + } + else if (config.has("print_settings_id")) { + collection = &prints; type_subdir = PRESET_PRINT_NAME; - } else if (config.has("filament_settings_id")) { - collection = &filaments; + } + else if (config.has("filament_settings_id")) { + collection = &filaments; type_subdir = PRESET_FILAMENT_NAME; } if (collection == nullptr) { @@ -1603,17 +1583,15 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions& substitutions return false; } const PresetOrigin load_origin = detect_origin_from_path(boost::filesystem::path(bundle_dir)); - const std::string preset_name = get_preset_canonical_name(name, load_origin); + const std::string preset_name = get_preset_canonical_name(name, load_origin); - if (overwrite == 0) - overwrite = 1; + if (overwrite == 0) overwrite = 1; if (auto p = collection->find_preset(preset_name, false)) { if (p->is_default || p->is_system) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present and is system preset, not loading: " << preset_name; return false; } - if (overwrite != 2 && overwrite != 3) - overwrite = override_confirm(preset_name); // 3: yes to all 2: no to all + if (overwrite != 2 && overwrite != 3) overwrite = override_confirm(preset_name); //3: yes to all 2: no to all } if (overwrite == 0 || overwrite == 2) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Preset already present, not loading: " << preset_name; @@ -1621,61 +1599,55 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions& substitutions } DynamicPrintConfig new_config; - Preset* inherit_preset = nullptr; - ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS); - std::string inherits_value; + Preset * inherit_preset = nullptr; + ConfigOption * inherits_config = config.option(BBL_JSON_KEY_INHERITS); + std::string inherits_value; if (inherits_config) { - ConfigOptionString* option_str = dynamic_cast(inherits_config); + ConfigOptionString *option_str = dynamic_cast(inherits_config); inherits_value = option_str->value; inherit_preset = collection->find_preset2(inherits_value, true); Preset::normalize_inherits(config, inherit_preset); if (inherit_preset) - inherits_value = inherit_preset->name; // keep the base_id redo below in sync + inherits_value = inherit_preset->name; // keep the base_id redo below in sync } if (inherit_preset) { new_config = inherit_preset->config; new_config.apply(std::move(config)); } else { // We support custom root preset now - auto inherits_config2 = dynamic_cast(inherits_config); + auto inherits_config2 = dynamic_cast(inherits_config); if (inherits_config2 && !inherits_config2->value.empty()) { // we should skip this preset here - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(", can not find inherit preset for user preset %1%, just skip") % name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", can not find inherit preset for user preset %1%, just skip") % name; return false; } - // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the - // "printer_technology" field. - const Preset& default_preset = collection->default_preset_for(config); + // Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field. + const Preset &default_preset = collection->default_preset_for(config); new_config = default_preset.config; new_config.apply(std::move(config)); extend_default_config_length(new_config, true, default_preset.config); } - Preset& preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, - std::move(new_config), false); + Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, std::move(new_config), false); preset.bundle_id = load_origin.bundle_id; if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end()) preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID]; preset.is_external = true; preset.version = *version; inherit_preset = collection->find_preset(inherits_value, false, true); // pointer maybe wrong after insert, redo find - if (inherit_preset) - preset.base_id = inherit_preset->setting_id; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " have filament_id: " << preset.filament_id - << " and base_id: " << preset.base_id; + if (inherit_preset) preset.base_id = inherit_preset->setting_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << preset.name << " have filament_id: " << preset.filament_id << " and base_id: " << preset.base_id; Preset::normalize(preset.config); // Report configuration fields, which are misplaced into a wrong group. - const Preset& default_preset = collection->default_preset_for(new_config); - std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); + const Preset &default_preset = collection->default_preset_for(new_config); + std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config); if (!incorrect_keys.empty()) { ++m_errors; BOOST_LOG_TRIVIAL(error) << "Error in a preset file: The preset \"" << preset.file << "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed"; } if (!config_substitutions.empty()) - substitutions.push_back( - {name, collection->type(), PresetConfigSubstitutions::Source::UserFile, file, std::move(config_substitutions)}); + substitutions.push_back({name, collection->type(), PresetConfigSubstitutions::Source::UserFile, file, std::move(config_substitutions)}); collection->set_custom_preset_alias(preset); // If bundle_dir is provided, use it for the save operation @@ -1689,26 +1661,26 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions& substitutions } result.push_back(file); - } catch (const std::ifstream::failure& err) { + } catch (const std::ifstream::failure &err) { ++m_errors; BOOST_LOG_TRIVIAL(error) << boost::format("The config cannot be loaded: %1%. Reason: %2%") % file % err.what(); - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { ++m_errors; BOOST_LOG_TRIVIAL(error) << boost::format("Failed importing config file: %1%. Reason: %2%") % file % err.what(); } return true; } -// BBS save user preset to user_id preset folder +//BBS save user preset to user_id preset folder void PresetBundle::save_user_presets(AppConfig& config, std::map& need_to_delete_list) { std::string user_sub_folder = DEFAULT_USER_FOLDER_NAME; if (!config.get("preset_folder").empty()) user_sub_folder = config.get("preset_folder"); - // BBS: change directory by design - const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user_sub_folder; + //BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ user_sub_folder; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, save to %1%") % dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, save to %1%")%dir_user_presets; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); if (!fs::exists(user_folder)) @@ -1740,7 +1712,7 @@ void PresetBundle::check_and_fix_user_presets_syncinfo(const std::string& user_i process_collection(this->printers); } -// Orca: Import subscribed bundle presets (load and save to disk in one operation) +//Orca: Import subscribed bundle presets (load and save to disk in one operation) PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( AppConfig& config, const std::map>& bundle_presets, @@ -1751,8 +1723,7 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( std::string errors_cumulative; bool process_added = false, filament_added = false, machine_added = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " enter, substitution_rule " << substitution_rule << ", bundle_id: " << remote_metadata.id - << ", preset count: " << bundle_presets.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " enter, substitution_rule " << substitution_rule << ", bundle_id: " << remote_metadata.id << ", preset count: " << bundle_presets.size(); BundleMetadata merged_metadata; auto existing_it = bundles.m_bundles.find(remote_metadata.id); @@ -1762,16 +1733,16 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( merged_metadata.imported_time = std::time(nullptr); } - merged_metadata.id = remote_metadata.id; - merged_metadata.name = remote_metadata.name; - merged_metadata.version = remote_metadata.version; - merged_metadata.description = remote_metadata.description; - merged_metadata.author = remote_metadata.author; - merged_metadata.updated_time = remote_metadata.updated_time; - merged_metadata.bundle_type = BundleType::Subscribed; - merged_metadata.is_subscribed = true; + merged_metadata.id = remote_metadata.id; + merged_metadata.name = remote_metadata.name; + merged_metadata.version = remote_metadata.version; + merged_metadata.description = remote_metadata.description; + merged_metadata.author = remote_metadata.author; + merged_metadata.updated_time = remote_metadata.updated_time; + merged_metadata.bundle_type = BundleType::Subscribed; + merged_metadata.is_subscribed = true; merged_metadata.update_available = false; - merged_metadata.unauthorized = false; + merged_metadata.unauthorized = false; const PresetOrigin subscribed_origin(PresetOrigin::Kind::SubscribedBundle, remote_metadata.id); @@ -1795,8 +1766,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( remote_printers.insert(subscribed_name); } - auto remove_obsolete_bundle_presets = [&](PresetCollection& collection, const std::unordered_set& remote_names, - const char* type_name) -> int { + auto remove_obsolete_bundle_presets = + [&](PresetCollection& collection, const std::unordered_set& remote_names, const char* type_name) -> int { int removed_count = 0; std::vector to_delete; @@ -1810,12 +1781,10 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( for (const std::string& preset_name : to_delete) { if (collection.delete_preset(preset_name, true)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << type_name << " preset '" << preset_name - << "' no longer in remote bundle, deleted"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << type_name << " preset '" << preset_name << "' no longer in remote bundle, deleted"; ++removed_count; } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to delete obsolete " << type_name << " preset '" << preset_name - << "'"; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to delete obsolete " << type_name << " preset '" << preset_name << "'"; } } @@ -1833,8 +1802,7 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // Get current user ID for path construction std::string user_id = config.get("preset_folder"); - if (user_id.empty()) - user_id = DEFAULT_USER_FOLDER_NAME; + if (user_id.empty()) user_id = DEFAULT_USER_FOLDER_NAME; // Create the subscribed directory base path boost::filesystem::path user_folder(Slic3r::data_dir() + "/" + PRESET_USER_DIR); @@ -1845,8 +1813,7 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!boost::filesystem::exists(subscribed_base)) boost::filesystem::create_directories(subscribed_base, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create subscribed directory: " << subscribed_base.string() - << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create subscribed directory: " << subscribed_base.string() << " error: " << ec.message(); return substitutions; } @@ -1857,8 +1824,7 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!boost::filesystem::exists(bundle_dir)) boost::filesystem::create_directories(bundle_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() - << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); return substitutions; } @@ -1868,8 +1834,8 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // Load each preset from the bundle and save to disk for (const auto& preset_entry : bundle_presets) { - const std::string& preset_name = preset_entry.first; - const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); + const std::string& preset_name = preset_entry.first; + const std::string subscribed_name = get_preset_canonical_name(preset_name, subscribed_origin); std::map value_map = preset_entry.second; // Make a copy since we might modify it BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " importing preset: " << preset_name << " from bundle: " << remote_metadata.id; @@ -1884,8 +1850,7 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( // If this preset inherits from another preset inside the same bundle, rewrite the // reference to the canonical (bundle-prefixed) name so the lookup matches the stored identity. auto inherits_iter = value_map.find(BBL_JSON_KEY_INHERITS); - if (inherits_iter != value_map.end() && !inherits_iter->second.empty() && - bundle_presets.find(inherits_iter->second) != bundle_presets.end()) + if (inherits_iter != value_map.end() && !inherits_iter->second.empty() && bundle_presets.find(inherits_iter->second) != bundle_presets.end()) inherits_iter->second = get_preset_canonical_name(inherits_iter->second, subscribed_origin); try { @@ -1895,23 +1860,23 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (type_iter->second == PRESET_IOT_PRINT_TYPE) { preset_collection = &(this->prints); - type_subdir = PRESET_PRINT_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, - subscribed_origin); + type_subdir = PRESET_PRINT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); process_added |= preset_added; - } else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { + } + else if (type_iter->second == PRESET_IOT_FILAMENT_TYPE) { preset_collection = &(this->filaments); - type_subdir = PRESET_FILAMENT_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, - subscribed_origin); + type_subdir = PRESET_FILAMENT_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); filament_added |= preset_added; - } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - type_subdir = PRESET_PRINTER_NAME; - preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, - subscribed_origin); + type_subdir = PRESET_PRINTER_NAME; + preset_added = preset_collection->load_user_preset(preset_name, value_map, substitutions, substitution_rule, subscribed_origin); machine_added |= preset_added; - } else { + } + else { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid type " << type_iter->second << " for preset " << preset_name; continue; } @@ -1933,19 +1898,20 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) merged_metadata.printer_presets.push_back(preset->name); } - } catch (const std::runtime_error& err) { + } + catch (const std::runtime_error& err) { errors_cumulative += err.what(); BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " error importing preset " << preset_name << ": " << err.what(); } } boost::filesystem::path metadata_save_path = bundle_dir / PRESET_BUNDLE_METADATA; - merged_metadata.path = metadata_save_path.string(); - bundles.m_bundles[remote_metadata.id] = merged_metadata; + merged_metadata.path = metadata_save_path.string(); + bundles.m_bundles[remote_metadata.id] = merged_metadata; if (bundles.m_bundles[remote_metadata.id].save_to_json(metadata_save_path.string())) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved bundle metadata to: " << metadata_save_path.string(); - } else { + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to save bundle metadata to: " << metadata_save_path.string(); } @@ -1960,18 +1926,15 @@ PresetsConfigSubstitutions PresetBundle::update_subscribed_presets( if (!errors_cumulative.empty()) throw Slic3r::RuntimeError(errors_cumulative); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " finished, process_added " << process_added << ", filament_added " << filament_added - << ", machine_added " << machine_added; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " finished, process_added " << process_added << ", filament_added " << filament_added << ", machine_added " << machine_added; return substitutions; } // Helper function: save preset to bundle directory with common logic // This function extracts the common code used by both import_json_presets and import_subscribed_presets -bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, - PresetCollection* collection, - const std::string& bundle_id, - const std::string& type_subdir, - const std::string& bundle_base_dir) +bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection, + const std::string& bundle_id, const std::string& type_subdir, + const std::string& bundle_base_dir) { if (bundle_base_dir.empty()) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " bundle_base_dir is empty, cannot save preset " << preset.name; @@ -1988,8 +1951,7 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, if (!boost::filesystem::exists(bundle_dir)) boost::filesystem::create_directories(bundle_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() - << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create bundle directory: " << bundle_dir.string() << " error: " << ec.message(); return false; } @@ -1998,8 +1960,7 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, if (!boost::filesystem::exists(type_dir)) { boost::filesystem::create_directories(type_dir, ec); if (ec) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create type directory: " << type_dir.string() - << " error: " << ec.message(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed to create type directory: " << type_dir.string() << " error: " << ec.message(); return false; } } @@ -2009,8 +1970,8 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, // Bundle preset names may include the subscribed/local prefix path. // Persist the file under the type directory using only the base preset name. const std::string preset_filename = boost::filesystem::path(preset.name).filename().string(); - const std::string file_name = boost::iends_with(preset_filename, ".json") ? preset_filename : (preset_filename + ".json"); - preset.file = (type_dir / file_name).make_preferred().string(); + const std::string file_name = boost::iends_with(preset_filename, ".json") ? preset_filename : (preset_filename + ".json"); + preset.file = (type_dir / file_name).make_preferred().string(); // Save with parent config if inherits from another preset std::string inherits = Preset::inherits(preset.config); @@ -2026,8 +1987,10 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, Preset::normalize_inherits(preset.config, parent_preset); if (preset.base_id.empty()) preset.base_id = parent_preset->setting_id; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved preset " << preset.name << " filament_id: " << preset.filament_id - << " base_id: " << preset.base_id << " bundle: " << bundle_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " saved preset " << preset.name + << " filament_id: " << preset.filament_id + << " base_id: " << preset.base_id + << " bundle: " << bundle_id; preset.save(&(parent_preset->config)); } } @@ -2043,13 +2006,13 @@ bool PresetBundle::save_preset_to_bundle_dir(Preset& preset, } } -// BBS: save user preset to user_id preset folder +//BBS: save user preset to user_id preset folder void PresetBundle::update_user_presets_directory(const std::string preset_folder) { - // BBS: change directory by design - const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + preset_folder; + //BBS: change directory by design + const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/"+ preset_folder; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, update directory to %1%") % dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, update directory to %1%")%dir_user_presets; fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR); if (!fs::exists(user_folder)) @@ -2070,8 +2033,7 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder const std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + preset_folder; if (preset_folder.empty()) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(": preset_folder is empty, no need to remove directory : %1%") % dir_user_presets; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": preset_folder is empty, no need to remove directory : %1%") % dir_user_presets; return; } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets; @@ -2083,69 +2045,72 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder void PresetBundle::update_system_preset_setting_ids(std::map>& system_presets) { - for (auto iterator : system_presets) { - std::string name = iterator.first; + for (auto iterator: system_presets) + { + std::string name = iterator.first; std::map& value_map = iterator.second; - // get the type first + //get the type first std::map::iterator type_iter = value_map.find(BBL_JSON_KEY_TYPE); if (type_iter == value_map.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%") % name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find type for setting %1%")%name; continue; } - PresetCollection* preset_collection = nullptr; + PresetCollection *preset_collection = nullptr; if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { + } + else if (type_iter->second == PRESET_IOT_PRINTER_TYPE) { preset_collection = &(this->printers); - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") % type_iter->second % name; + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("invalid type %1% for setting %2%") %type_iter->second %name; continue; } std::string setting_id; if (value_map.count(BBL_JSON_KEY_SETTING_ID) > 0) setting_id = value_map[BBL_JSON_KEY_SETTING_ID]; else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find setting_id for setting %1%") % name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(" can not find setting_id for setting %1%")%name; continue; } Preset* preset = preset_collection->find_preset(name, false, true); if (preset) { if (!preset->setting_id.empty() && (preset->setting_id.compare(setting_id) != 0)) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << boost::format("name %1%, local setting_id %2% is different with remote id %3%") % preset->name % - preset->setting_id % setting_id; - } else if (preset->setting_id.empty()) + BOOST_LOG_TRIVIAL(error) << boost::format("name %1%, local setting_id %2% is different with remote id %3%") + %preset->name %preset->setting_id %setting_id; + } + else if (preset->setting_id.empty()) preset->setting_id = setting_id; - } else { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format("can not find setting %1% in system presets, type %2%") % name % type_iter->second; + } + else { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format("can not find setting %1% in system presets, type %2%") %name %type_iter->second; continue; } } return; } -// BBS: validate printers from previous project -static std::set gcodes_key_set = {"filament_end_gcode", "filament_start_gcode", "change_filament_gcode", - "layer_change_gcode", "machine_end_gcode", "machine_pause_gcode", - "machine_start_gcode", "template_custom_gcode", "printing_by_object_gcode", - "before_layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode"}; -int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintConfig& config, std::set& different_gcodes) +//BBS: validate printers from previous project +static std::set gcodes_key_set = {"filament_end_gcode", "filament_start_gcode", "change_filament_gcode", "layer_change_gcode", "machine_end_gcode", "machine_pause_gcode", "machine_start_gcode", + "template_custom_gcode", "printing_by_object_gcode", "before_layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode"}; +int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintConfig& config, std::set& different_gcodes) { - bool validated = false; - std::vector inherits_values = config.option("inherits_group", true)->values; - std::vector filament_preset_name = config.option("filament_settings_id", true)->values; - std::string printer_preset = config.option("printer_settings_id", true)->value; - bool has_different_settings_to_system = config.option("different_settings_to_system") ? true : false; + bool validated = false; + std::vector inherits_values = config.option("inherits_group", true)->values; + std::vector filament_preset_name = config.option("filament_settings_id", true)->values; + std::string printer_preset = config.option("printer_settings_id", true)->value; + bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; std::vector different_values; - int ret = VALIDATE_PRESETS_SUCCESS; + int ret = VALIDATE_PRESETS_SUCCESS; if (has_different_settings_to_system) different_values = config.option("different_settings_to_system", true)->values; - // PrinterTechnology printer_technology = Preset::printer_technology(config); + //PrinterTechnology printer_technology = Preset::printer_technology(config); size_t filament_count = config.option("filament_diameter")->values.size(); inherits_values.resize(filament_count + 2, std::string()); different_values.resize(filament_count + 2, std::string()); @@ -2155,14 +2120,14 @@ int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintCon validated = this->printers.validate_preset(printer_preset, printer_inherits); if (!validated) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(":file_name %1%, found the printer preset not inherit from system") % file_name; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the printer preset not inherit from system") % file_name; different_gcodes.emplace(printer_preset); ret = VALIDATE_PRESETS_PRINTER_NOT_FOUND; } - for (unsigned int index = 0; index < filament_count; index++) { - std::string filament_preset = filament_preset_name[index]; - std::string filament_inherits = inherits_values[index + 1]; + for(unsigned int index = 0; index < filament_count; index ++) + { + std::string filament_preset = filament_preset_name[index]; + std::string filament_inherits = inherits_values[index+1]; // filament_preset_name is padded up to filament_count from filament_diameter. Unfilled // slots have no assigned preset, so there's nothing to validate or warn about. @@ -2171,23 +2136,21 @@ int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintCon validated = this->filaments.validate_preset(filament_preset, filament_inherits); if (!validated) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(":file_name %1%, found the filament %2% preset not inherit from system") % - file_name % (index + 1); + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found the filament %2% preset not inherit from system") % file_name %(index+1); different_gcodes.emplace(filament_preset); ret = VALIDATE_PRESETS_FILAMENTS_NOT_FOUND; } } - // self defined presets, return directly - if (ret) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(":file_name %1%, found self defined presets, count %2%") % file_name % - different_gcodes.size(); + //self defined presets, return directly + if (ret) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found self defined presets, count %2%") %file_name %different_gcodes.size(); return ret; } - for (unsigned int index = 1; index < filament_count; index++) { + for(unsigned int index = 1; index < filament_count; index ++) + { std::string different_settingss = different_values[index]; std::vector different_keys; @@ -2197,16 +2160,14 @@ int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintCon for (unsigned int j = 0; j < different_keys.size(); j++) { if (gcodes_key_set.find(different_keys[j]) != gcodes_key_set.end()) { different_gcodes.emplace(different_keys[j]); - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(":preset index %1%, different key %2%") % index % different_keys[j]; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":preset index %1%, different key %2%") %index %different_keys[j]; } } } - if (!different_gcodes.empty()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(":file_name %1%, found different gcodes count %2%") % file_name % - different_gcodes.size(); + if (!different_gcodes.empty()) + { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":file_name %1%, found different gcodes count %2%") %file_name %different_gcodes.size(); return VALIDATE_PRESETS_MODIFIED_GCODES; } @@ -2215,7 +2176,7 @@ int PresetBundle::validate_presets(const std::string& file_name, DynamicPrintCon return VALIDATE_PRESETS_SUCCESS; } -void PresetBundle::remove_users_preset(AppConfig& config, std::map>* my_presets) +void PresetBundle::remove_users_preset(AppConfig &config, std::map> *my_presets) { auto check_removed = [my_presets](Preset &preset) -> bool { if (my_presets == nullptr) return true; @@ -2230,18 +2191,17 @@ void PresetBundle::remove_users_preset(AppConfig& config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(":printers erase %1%, type %2%, user_id %3%") % it->name % - Preset::get_type_string(it->type) % it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":printers erase %1%, type %2%, user_id %3%") % it->name % Preset::get_type_string(it->type) % it->user_id; if (it->name == printer_selected_preset_name) need_reset_printer_preset = true; it = printers.erase(it); - } else { + } + else { it++; } } @@ -2263,17 +2223,16 @@ void PresetBundle::remove_users_preset(AppConfig& config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(":prints erase %1%, type %2%, user_id %3%") % it->name % - Preset::get_type_string(it->type) % it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":prints erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; if (it->name == selected_print_name) need_reset_print_preset = true; it = prints.erase(it); - } else { + } + else { it++; } } @@ -2285,22 +2244,20 @@ void PresetBundle::remove_users_preset(AppConfig& config, std::mapis_user() && it->user_id.compare(preset_folder_user_id) == 0 && check_removed(*it)) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(":filaments erase %1%, type %2%, user_id %3%") % it->name % - Preset::get_type_string(it->type) % it->user_id; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":filaments erase %1%, type %2%, user_id %3%")%it->name %Preset::get_type_string(it->type) %it->user_id; if (it->name == selected_filament_name) need_reset_filament_preset = true; it = filaments.erase(it); - } else { + } + else { it++; } } if (need_reset_filament_preset && printers.get_selected_preset().config.has("default_filament_profile")) { - const std::vector& prefered_filament_profiles = - printers.get_selected_preset().config.option("default_filament_profile")->values; + const std::vector& prefered_filament_profiles = printers.get_selected_preset().config.option("default_filament_profile")->values; if (prefered_filament_profiles.size() > 0) filaments.select_preset_by_name(prefered_filament_profiles[0], true); } else { @@ -2310,7 +2267,8 @@ void PresetBundle::remove_users_preset(AppConfig& config, std::mapfilaments.find_preset(filament_presets[i]); if (preset == nullptr) filament_presets[i] = filaments.get_selected_preset_name(); @@ -2328,12 +2286,11 @@ void PresetBundle::clear_printer_hold_aliases() this->printers.m_printer_hold_alias.clear(); } -// BBS: add json related logic, load system presets from json -std::pair PresetBundle::load_system_presets_from_json( - ForwardCompatibilitySubstitutionRule compatibility_rule) +//BBS: add json related logic, load system presets from json +std::pair PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) { - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) // Loading system presets, don't log substitutions. compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent; @@ -2342,8 +2299,8 @@ std::pair PresetBundle::load_system_pre compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - // BBS: change directory by design - boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + //BBS: change directory by design + boost::filesystem::path dir = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); @@ -2352,10 +2309,10 @@ std::pair PresetBundle::load_system_pre // The vendors below are loaded whole and against each other — the filament // library first, then every other vendor with it as the base — so each parse // is complete enough to be worth caching. - m_generate_vendor_caches = m_generate_vendor_caches || !validation_mode; + m_generate_vendor_caches = m_generate_vendor_caches || ! validation_mode; - PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; + PresetsConfigSubstitutions substitutions; + std::string errors_cummulative; bool first = true; // Sorted, so any duplicate-preset warning below comes out in the same order on // every run. @@ -2375,17 +2332,16 @@ std::pair PresetBundle::load_system_pre } // Step 1: Load ORCA_FILAMENT_LIBRARY into `this` synchronously. - if (!orca_lib_vendor.empty()) { + if (! orca_lib_vendor.empty()) { try { // Match a fresh launch before parsing: hold aliases and the error // counter survive reset(), and would otherwise carry prior-cycle // state into this load. this->clear_printer_hold_aliases(); this->m_errors = 0; - append(substitutions, - this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); + append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first); first = false; - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { if (validation_mode) throw err; errors_cummulative += err.what(); @@ -2396,25 +2352,26 @@ std::pair PresetBundle::load_system_pre // Step 2: Load remaining vendors in parallel. Each gets its own // PresetBundle and uses `this` (which contains ORCA_FILAMENT_LIBRARY) // as the base_bundle for cross-bundle inheritance lookups. - std::vector> parallel_bundles(other_vendors.size()); - std::vector parallel_substitutions(other_vendors.size()); - std::vector parallel_errors(other_vendors.size()); + std::vector> parallel_bundles(other_vendors.size()); + std::vector parallel_substitutions(other_vendors.size()); + std::vector parallel_errors(other_vendors.size()); - tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), [&](const tbb::blocked_range& range) { - for (size_t i = range.begin(); i < range.end(); ++i) { - auto bundle = std::make_unique(); - bundle->set_is_validation_mode(validation_mode); - bundle->set_generate_vendor_caches(m_generate_vendor_caches); - try { - auto result = bundle->load_vendor_configs_from_json(dir.string(), other_vendors[i], PresetBundle::LoadSystem, - compatibility_rule, this); - parallel_substitutions[i] = std::move(result.first); - parallel_bundles[i] = std::move(bundle); - } catch (const std::runtime_error& err) { - parallel_errors[i] = err.what(); + tbb::parallel_for(tbb::blocked_range(0, other_vendors.size()), + [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + auto bundle = std::make_unique(); + bundle->set_is_validation_mode(validation_mode); + bundle->set_generate_vendor_caches(m_generate_vendor_caches); + try { + auto result = bundle->load_vendor_configs_from_json( + dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this); + parallel_substitutions[i] = std::move(result.first); + parallel_bundles[i] = std::move(bundle); + } catch (const std::runtime_error &err) { + parallel_errors[i] = err.what(); + } } - } - }); + }); // Step 3: Sequentially merge the parallel-loaded bundles into `this`. // The merge order is the original vendor order so any duplicate-warning @@ -2433,7 +2390,7 @@ std::pair PresetBundle::load_system_pre const std::string& vendor_name = other_vendors[i]; append(substitutions, std::move(parallel_substitutions[i])); std::vector duplicates = this->merge_presets(std::move(*parallel_bundles[i])); - first = false; + first = false; if (!duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; for (size_t j = 0; j < duplicates.size(); ++j) { @@ -2447,22 +2404,22 @@ std::pair PresetBundle::load_system_pre } if (first) { - // No config bundle loaded, reset. - this->reset(false); - } + // No config bundle loaded, reset. + this->reset(false); + } - this->update_system_maps(); + this->update_system_maps(); - const auto load_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - load_t0).count(); + const auto load_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - load_t0).count(); BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << vendor_names.size() << " vendor(s) loaded in " << load_ms << " ms"; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%") % errors_cummulative; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; return std::make_pair(std::move(substitutions), errors_cummulative); } -std::pair PresetBundle::load_system_models_from_json( - ForwardCompatibilitySubstitutionRule compatibility_rule) +std::pair PresetBundle::load_system_models_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) @@ -2473,10 +2430,10 @@ std::pair PresetBundle::load_system_mod compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; - for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string errors_cummulative; + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { std::string vendor_file = dir_entry.path().string(); if (Slic3r::is_json_file(vendor_file)) { std::string vendor_name = dir_entry.path().filename().string(); @@ -2484,9 +2441,8 @@ std::pair PresetBundle::load_system_mod vendor_name.erase(vendor_name.size() - 5); try { // Load the config bundle, flatten it. - append(substitutions, - load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); - } catch (const std::runtime_error& err) { + append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first); + } catch (const std::runtime_error &err) { errors_cummulative += err.what(); errors_cummulative += "\n"; } @@ -2497,8 +2453,7 @@ std::pair PresetBundle::load_system_mod return std::make_pair(std::move(substitutions), errors_cummulative); } -std::pair PresetBundle::load_system_filaments_json( - ForwardCompatibilitySubstitutionRule compatibility_rule) +std::pair PresetBundle::load_system_filaments_json(ForwardCompatibilitySubstitutionRule compatibility_rule) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%") % compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) @@ -2509,11 +2464,11 @@ std::pair PresetBundle::load_system_fil compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable; // Here the vendor specific read only Config Bundles are stored. - boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); PresetsConfigSubstitutions substitutions; - std::string errors_cummulative; - bool first = true; - for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { + std::string errors_cummulative; + bool first = true; + for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) { std::string vendor_file = dir_entry.path().string(); if (Slic3r::is_json_file(vendor_file)) { std::string vendor_name = dir_entry.path().filename().string(); @@ -2522,31 +2477,23 @@ std::pair PresetBundle::load_system_fil try { if (first) { // Reset this PresetBundle and load the first vendor config. - append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, - PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, - compatibility_rule) - .first); + append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); first = false; } else { // Load the other vendor configs, merge them with this PresetBundle. // Report duplicate profiles. PresetBundle other; - append(substitutions, - other - .load_vendor_configs_from_json(dir.string(), vendor_name, - PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule) - .first); + append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first); std::vector duplicates = this->merge_presets(std::move(other)); if (!duplicates.empty()) { errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: "; for (size_t i = 0; i < duplicates.size(); ++i) { - if (i > 0) - errors_cummulative += ", "; + if (i > 0) errors_cummulative += ", "; errors_cummulative += duplicates[i]; } } } - } catch (const std::runtime_error& err) { + } catch (const std::runtime_error &err) { errors_cummulative += err.what(); errors_cummulative += "\n"; } @@ -2562,19 +2509,17 @@ VendorProfile PresetBundle::get_custom_vendor_models() const VendorProfile vendor; vendor.name = PRESET_CUSTOM_VENDOR; vendor.id = PRESET_CUSTOM_VENDOR; - for (auto& preset : printers.get_presets()) { - if (preset.is_system) - continue; - if (printers.get_preset_base(preset) != &preset) - continue; - if (preset.is_default) - continue; - auto model = preset.config.opt_string("printer_model"); - auto variant = preset.config.opt_string("printer_variant"); - auto iter_model = std::find_if(vendor.models.begin(), vendor.models.end(), - [model](VendorProfile::PrinterModel& m) { return m.name == model; }); + for (auto &preset : printers.get_presets()) { + if (preset.is_system) continue; + if (printers.get_preset_base(preset) != &preset) continue; + if (preset.is_default) continue; + auto model = preset.config.opt_string("printer_model"); + auto variant = preset.config.opt_string("printer_variant"); + auto iter_model = std::find_if(vendor.models.begin(), vendor.models.end(), [model](VendorProfile::PrinterModel &m) { + return m.name == model; + }); if (iter_model == vendor.models.end()) { - iter_model = vendor.models.emplace(vendor.models.end(), VendorProfile::PrinterModel{}); + iter_model = vendor.models.emplace(vendor.models.end(), VendorProfile::PrinterModel{}); iter_model->id = model; iter_model->name = model; iter_model->variants = {VendorProfile::PrinterVariant(variant)}; @@ -2586,21 +2531,21 @@ VendorProfile PresetBundle::get_custom_vendor_models() const } // Merge one vendor's presets with the other vendor's presets, report duplicates. -std::vector PresetBundle::merge_presets(PresetBundle&& other) +std::vector PresetBundle::merge_presets(PresetBundle &&other) { this->vendors.insert(other.vendors.begin(), other.vendors.end()); - std::vector duplicate_prints = this->prints.merge_presets(std::move(other.prints), this->vendors); - std::vector duplicate_sla_prints = this->sla_prints.merge_presets(std::move(other.sla_prints), this->vendors); - std::vector duplicate_filaments = this->filaments.merge_presets(std::move(other.filaments), this->vendors); + std::vector duplicate_prints = this->prints .merge_presets(std::move(other.prints), this->vendors); + std::vector duplicate_sla_prints = this->sla_prints .merge_presets(std::move(other.sla_prints), this->vendors); + std::vector duplicate_filaments = this->filaments .merge_presets(std::move(other.filaments), this->vendors); std::vector duplicate_sla_materials = this->sla_materials.merge_presets(std::move(other.sla_materials), this->vendors); - std::vector duplicate_printers = this->printers.merge_presets(std::move(other.printers), this->vendors); - append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); - append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); - append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); + std::vector duplicate_printers = this->printers .merge_presets(std::move(other.printers), this->vendors); + append(this->obsolete_presets.prints, std::move(other.obsolete_presets.prints)); + append(this->obsolete_presets.sla_prints, std::move(other.obsolete_presets.sla_prints)); + append(this->obsolete_presets.filaments, std::move(other.obsolete_presets.filaments)); append(this->obsolete_presets.sla_materials, std::move(other.obsolete_presets.sla_materials)); - append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); - append(duplicate_prints, std::move(duplicate_sla_prints)); - append(duplicate_prints, std::move(duplicate_filaments)); + append(this->obsolete_presets.printers, std::move(other.obsolete_presets.printers)); + append(duplicate_prints, std::move(duplicate_sla_prints)); + append(duplicate_prints, std::move(duplicate_filaments)); append(duplicate_prints, std::move(duplicate_sla_materials)); append(duplicate_prints, std::move(duplicate_printers)); m_errors += other.m_errors; @@ -2609,22 +2554,22 @@ std::vector PresetBundle::merge_presets(PresetBundle&& other) void PresetBundle::update_system_maps() { - this->prints.update_map_system_profile_renamed(); - this->sla_prints.update_map_system_profile_renamed(); - this->filaments.update_map_system_profile_renamed(); + this->prints .update_map_system_profile_renamed(); + this->sla_prints .update_map_system_profile_renamed(); + this->filaments .update_map_system_profile_renamed(); this->sla_materials.update_map_system_profile_renamed(); - this->printers.update_map_system_profile_renamed(); + this->printers .update_map_system_profile_renamed(); - this->prints.update_map_alias_to_profile_name(); - this->sla_prints.update_map_alias_to_profile_name(); - this->filaments.update_map_alias_to_profile_name(); + this->prints .update_map_alias_to_profile_name(); + this->sla_prints .update_map_alias_to_profile_name(); + this->filaments .update_map_alias_to_profile_name(); this->sla_materials.update_map_alias_to_profile_name(); - this->printers.update_map_alias_to_profile_name(); + this->printers .update_map_alias_to_profile_name(); this->filaments.update_library_profile_excluded_from(); } -static inline std::string remove_ini_suffix(const std::string& name) +static inline std::string remove_ini_suffix(const std::string &name) { std::string out = name; if (boost::iends_with(out, ".ini")) @@ -2635,14 +2580,14 @@ static inline std::string remove_ini_suffix(const std::string& name) // Set the "enabled" flag for printer vendors, printer models and printer variants // based on the user configuration. // If the "vendor" section is missing, enable all models and variants of the particular vendor. -void PresetBundle::load_installed_printers(const AppConfig& config) +void PresetBundle::load_installed_printers(const AppConfig &config) { - this->update_system_maps(); - for (auto& preset : printers) + this->update_system_maps(); + for (auto &preset : printers) preset.set_visible_from_appconfig(config); } -const std::string& PresetBundle::get_preset_name_by_alias(const Preset::Type& preset_type, const std::string& alias) const +const std::string& PresetBundle::get_preset_name_by_alias( const Preset::Type& preset_type, const std::string& alias) const { if (preset_type == Preset::TYPE_INVALID) return alias; @@ -2651,21 +2596,21 @@ const std::string& PresetBundle::get_preset_name_by_alias(const Preset::Type& pr preset_type == Preset::TYPE_SLA_PRINT ? sla_prints : preset_type == Preset::TYPE_FILAMENT ? filaments : preset_type == Preset::TYPE_PRINTER ? printers : - sla_materials; + sla_materials; return presets.get_preset_name_by_alias(alias); } -// BBS: get filament required hrc by filament type +//BBS: get filament required hrc by filament type const int PresetBundle::get_required_hrc_by_filament_type(const std::string& filament_type) const { - static std::unordered_map filament_type_to_hrc; + static std::unordered_mapfilament_type_to_hrc; if (filament_type_to_hrc.empty()) { for (auto iter = filaments.m_presets.begin(); iter != filaments.m_presets.end(); iter++) { if (iter->vendor && iter->vendor->id == "BBL") { if (iter->config.has("filament_type") && iter->config.has("required_nozzle_HRC")) { - auto type = iter->config.opt_string("filament_type", 0); - auto hrc = iter->config.opt_int("required_nozzle_HRC", 0); + auto type = iter->config.opt_string("filament_type", 0); + auto hrc = iter->config.opt_int("required_nozzle_HRC", 0); filament_type_to_hrc[type] = hrc; } } @@ -2678,17 +2623,14 @@ const int PresetBundle::get_required_hrc_by_filament_type(const std::string& fil return 0; } -// BBS: add project embedded preset logic -void PresetBundle::save_changes_for_preset(const std::string& new_name, - Preset::Type type, - const std::vector& unselected_options, - bool save_to_project) +//BBS: add project embedded preset logic +void PresetBundle::save_changes_for_preset(const std::string& new_name, Preset::Type type, + const std::vector& unselected_options, bool save_to_project) { - PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : - type == Preset::TYPE_SLA_PRINT ? sla_prints : - type == Preset::TYPE_FILAMENT ? filaments : - type == Preset::TYPE_SLA_MATERIAL ? sla_materials : - printers; + PresetCollection& presets = type == Preset::TYPE_PRINT ? prints : + type == Preset::TYPE_SLA_PRINT ? sla_prints : + type == Preset::TYPE_FILAMENT ? filaments : + type == Preset::TYPE_SLA_MATERIAL ? sla_materials : printers; // if we want to save just some from selected options if (!unselected_options.empty()) { @@ -2697,12 +2639,11 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, } // Save the preset into Slic3r::data_dir / presets / section_name / preset_name.ini - // BBS: add project embedded preset logic - // presets.save_current_preset(new_name); + //BBS: add project embedded preset logic + //presets.save_current_preset(new_name); presets.save_current_preset(new_name, false, save_to_project); // Mark the print & filament enabled if they are compatible with the currently selected preset. - // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a - // "red flag" icon showing that they are no more compatible. + // If saving the preset changes compatibility with other presets, keep the now incompatible dependent presets selected, however with a "red flag" icon showing that they are no more compatible. update_compatible(PresetSelectCompatibleType::Never); if (type == Preset::TYPE_FILAMENT) { @@ -2711,90 +2652,87 @@ void PresetBundle::save_changes_for_preset(const std::string& new_name, } } -void PresetBundle::load_installed_filaments(AppConfig& config) +void PresetBundle::load_installed_filaments(AppConfig &config) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, printer size %1%") % printers.size(); - // if (! config.has_section(AppConfig::SECTION_FILAMENTS) - // || config.get_section(AppConfig::SECTION_FILAMENTS).empty()) { - // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. - // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles - // were installed. - std::unordered_set compatible_filaments; - for (const Preset& printer : printers) - if (printer.is_visible && printer.printer_technology() == ptFFF && printer.vendor && (!printer.vendor->models.empty())) { - bool add_default_materials = true; - if (config.has_section(AppConfig::SECTION_FILAMENTS)) { - const std::map& installed_filament = config.get_section(AppConfig::SECTION_FILAMENTS); - for (auto filament_iter : installed_filament) { - Preset* filament = filaments.find_preset(filament_iter.first, false, true); - if (filament && is_compatible_with_printer(PresetWithVendorProfile(*filament, filament->vendor), - PresetWithVendorProfile(printer, printer.vendor))) { - // already has compatible filament - add_default_materials = false; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": printer %1% vendor %2% already has default filament %3%") % - printer.name % printer.vendor % filament_iter.first; - break; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, printer size %1%")%printers.size(); + //if (! config.has_section(AppConfig::SECTION_FILAMENTS) + // || config.get_section(AppConfig::SECTION_FILAMENTS).empty()) { + // Compatibility with the PrusaSlicer 2.1.1 and older, where the filament profiles were not installable yet. + // Find all filament profiles, which are compatible with installed printers, and act as if these filament profiles + // were installed. + std::unordered_set compatible_filaments; + for (const Preset &printer : printers) + if (printer.is_visible && printer.printer_technology() == ptFFF && printer.vendor && (!printer.vendor->models.empty())) { + bool add_default_materials = true; + if (config.has_section(AppConfig::SECTION_FILAMENTS)) + { + const std::map& installed_filament = config.get_section(AppConfig::SECTION_FILAMENTS); + for (auto filament_iter : installed_filament) + { + Preset* filament = filaments.find_preset(filament_iter.first, false, true); + if (filament && is_compatible_with_printer(PresetWithVendorProfile(*filament, filament->vendor), PresetWithVendorProfile(printer, printer.vendor))) + { + + //already has compatible filament + add_default_materials = false; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% already has default filament %3%")%printer.name %printer.vendor %filament_iter.first; + break; + } } } - } - if (!add_default_materials) - continue; + if (!add_default_materials) + continue; - const VendorProfile::PrinterModel* printer_model = PresetUtils::system_printer_model(printer); - if (!printer_model) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find printer_model for printer %1%") % printer.name; - continue; + const VendorProfile::PrinterModel *printer_model = PresetUtils::system_printer_model(printer); + if (!printer_model) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not find printer_model for printer %1%")%printer.name; + continue; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1% vendor %2% don't have filament visible, will add %3% default filaments")%printer.name %printer.vendor %printer_model->default_materials.size(); + for (auto default_filament: printer_model->default_materials) + { + Preset* filament = filaments.find_preset(default_filament, false, true); + if (filament && filament->is_system) + compatible_filaments.insert(filament); + } + //const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + //for (const Preset &filament : filaments) + // if (filament.is_system && is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), printer_with_vendor_profile)) + // compatible_filaments.insert(&filament); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format( - ": printer %1% vendor %2% don't have filament visible, will add %3% default filaments") % - printer.name % printer.vendor % printer_model->default_materials.size(); - for (auto default_filament : printer_model->default_materials) { - Preset* filament = filaments.find_preset(default_filament, false, true); - if (filament && filament->is_system) - compatible_filaments.insert(filament); - } - // const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); - // for (const Preset &filament : filaments) - // if (filament.is_system && is_compatible_with_printer(filaments.get_preset_with_vendor_profile(filament), - // printer_with_vendor_profile)) - // compatible_filaments.insert(&filament); + // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. + for (const auto &filament: compatible_filaments) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set filament %1% to visible by default")%filament->name; + config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); } - // and mark these filaments as installed, therefore this code will not be executed at the next start of the application. - for (const auto& filament : compatible_filaments) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": set filament %1% to visible by default") % filament->name; - config.set(AppConfig::SECTION_FILAMENTS, filament->name, "true"); - } //} - for (auto& preset : filaments) + for (auto &preset : filaments) preset.set_visible_from_appconfig(config); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": exit."); } -void PresetBundle::load_installed_sla_materials(AppConfig& config) +void PresetBundle::load_installed_sla_materials(AppConfig &config) { - if (!config.has_section(AppConfig::SECTION_MATERIALS)) { + if (! config.has_section(AppConfig::SECTION_MATERIALS)) { std::unordered_set comp_sla_materials; - // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. - // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles - // were installed. - for (const Preset& printer : printers) + // Compatibility with the PrusaSlicer 2.1.1 and older, where the SLA material profiles were not installable yet. + // Find all SLA material profiles, which are compatible with installed printers, and act as if these SLA material profiles + // were installed. + for (const Preset &printer : printers) if (printer.is_visible && printer.printer_technology() == ptSLA) { - const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); - for (const Preset& material : sla_materials) - if (material.is_system && - is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) - comp_sla_materials.insert(&material); - } - // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. - for (const auto& material : comp_sla_materials) + const PresetWithVendorProfile printer_with_vendor_profile = printers.get_preset_with_vendor_profile(printer); + for (const Preset &material : sla_materials) + if (material.is_system && is_compatible_with_printer(sla_materials.get_preset_with_vendor_profile(material), printer_with_vendor_profile)) + comp_sla_materials.insert(&material); + } + // and mark these SLA materials as installed, therefore this code will not be executed at the next start of the application. + for (const auto &material: comp_sla_materials) config.set(AppConfig::SECTION_MATERIALS, material->name, "true"); } - for (auto& preset : sla_materials) + for (auto &preset : sla_materials) preset.set_visible_from_appconfig(config); } @@ -2805,10 +2743,11 @@ void PresetBundle::load_installed_sla_materials(AppConfig& config) // indices into exactly that list. Missing keys clear the arrays, so one printer never inherits // another's mixes; fallback_to_global also reads the shared "presets" keys an older config // layout used, which export_selections drops on the next save. -static void load_mixed_filament_settings( - DynamicPrintConfig& project_config, AppConfig& config, const std::string& printer_name, size_t n_filaments, bool fallback_to_global) +static void load_mixed_filament_settings(DynamicPrintConfig &project_config, AppConfig &config, + const std::string &printer_name, size_t n_filaments, + bool fallback_to_global) { - auto raw_value = [&](const char* key, bool& found) -> std::string { + auto raw_value = [&](const char *key, bool &found) -> std::string { if (config.has_printer_setting(printer_name, key)) { found = true; return config.get_printer_setting(printer_name, key); @@ -2821,22 +2760,21 @@ static void load_mixed_filament_settings( return std::string{}; }; std::vector parts; - auto load_bools = [&](const char* key) { - auto& vals = project_config.option(key)->values; + auto load_bools = [&](const char *key) { + auto &vals = project_config.option(key)->values; vals.clear(); - bool found = false; + bool found = false; const std::string s = raw_value(key, found); if (found && !s.empty()) { boost::algorithm::split(parts, s, boost::algorithm::is_any_of(",")); - for (const auto& p : parts) - vals.push_back(p == "1"); + for (const auto &p : parts) vals.push_back(p == "1"); } vals.resize(n_filaments, false); }; - auto load_strings = [&](const char* key) { - auto& vals = project_config.option(key)->values; + auto load_strings = [&](const char *key) { + auto &vals = project_config.option(key)->values; vals.clear(); - bool found = false; + bool found = false; const std::string s = raw_value(key, found); if (found && !s.empty()) { boost::algorithm::split(parts, s, boost::algorithm::is_any_of("|")); @@ -2855,9 +2793,9 @@ static void load_mixed_filament_settings( // The gradient curve is the one array whose values contain '|' themselves (it separates the // control points), so it is stored C-style escaped rather than '|'-joined. { - auto& vals = project_config.option("filament_mixed_gradient_curve")->values; + auto &vals = project_config.option("filament_mixed_gradient_curve")->values; vals.clear(); - bool found = false; + bool found = false; const std::string s = raw_value("filament_mixed_gradient_curve", found); if (found && !s.empty()) { std::vector curves; @@ -2871,12 +2809,12 @@ static void load_mixed_filament_settings( } } -void PresetBundle::update_selections(AppConfig& config) +void PresetBundle::update_selections(AppConfig &config) { - std::string initial_printer_profile_name = printers.get_selected_preset_name(); + std::string initial_printer_profile_name = printers.get_selected_preset_name(); // Orca: load from orca_presets - std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); - std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); // Selects the profiles, which were selected at the last application close. prints.select_preset_by_name_strict(initial_print_profile_name); @@ -2886,8 +2824,8 @@ void PresetBundle::update_selections(AppConfig& config) // Load it even if the current printer technology is SLA. // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() // once the FFF technology gets selected. - this->filament_presets = {filaments.get_selected_preset_name()}; - for (unsigned int i = 1; i < 1000; ++i) { + this->filament_presets = { filaments.get_selected_preset_name() }; + for (unsigned int i = 1; i < 1000; ++ i) { char name[64]; sprintf(name, "filament_%02u", i); auto f_name = config.get_printer_setting(initial_printer_profile_name, name); @@ -2908,18 +2846,14 @@ void PresetBundle::update_selections(AppConfig& config) std::vector multi_filament_colors; if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { - boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); } - if (multi_filament_colors.size() == 0) - project_config.option("filament_multi_colour")->values = filament_colors; - else - project_config.option("filament_multi_colour")->values = multi_filament_colors; + if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; + else project_config.option("filament_multi_colour")->values = multi_filament_colors; std::vector filament_color_types; if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { - boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); } filament_color_types.resize(filament_presets.size(), "1"); project_config.option("filament_colour_type")->values = filament_color_types; @@ -2935,32 +2869,25 @@ void PresetBundle::update_selections(AppConfig& config) std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { - boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); } this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), - flush_volumes_matrix.end()); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), - flush_volumes_vector.end()); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), - flush_multipliers.end()); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); } // No global fallback here: on a printer change the legacy shared keys describe another // printer's filament list, so absent per-printer keys must clear the mixes, not revive them. @@ -2974,27 +2901,26 @@ void PresetBundle::update_selections(AppConfig& config) this->update_multi_material_filament_presets(); std::string first_visible_filament_name; - for (auto& fp : filament_presets) { + for (auto & fp : filament_presets) { // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder. update_compatible_internal // iterates from m_num_default_presets, so the placeholder's is_compatible flag // stays true and the not-found/visible/compatible predicate alone would miss it. - if (auto it = filaments.find_preset_internal(fp); - fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { if (first_visible_filament_name.empty()) first_visible_filament_name = filaments.first_compatible().name; fp = first_visible_filament_name; } } + } // Load selections (current print, current filaments, current printer) from config.ini // This is done on application start up or after updates are applied. -void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& preferred_selection /* = PresetPreferences()*/) +void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& preferred_selection/* = PresetPreferences()*/) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": enter, preferred printer_model_id %1%") % preferred_selection.printer_model_id; - // Update visibility of presets based on application vendor / model / variant configuration. - this->load_installed_printers(config); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, preferred printer_model_id %1%")%preferred_selection.printer_model_id; + // Update visibility of presets based on application vendor / model / variant configuration. + this->load_installed_printers(config); // Update visibility of filament and sla material presets this->load_installed_filaments(config); @@ -3003,7 +2929,7 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p // Parse the initial print / filament / printer profile names. // std::string initial_sla_print_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_PRINT_NAME)); // std::string initial_sla_material_profile_name = remove_ini_suffix(config.get("presets", PRESET_SLA_MATERIALS_NAME)); - std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINTER_NAME)); + std::string initial_printer_profile_name = remove_ini_suffix(config.get("presets", PRESET_PRINTER_NAME)); // Activate print / filament / printer profiles from either the config, // or from the preferred_model_id suggestion passed in by ConfigWizard. @@ -3011,26 +2937,25 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p // Do not select alternate profiles for the print / filament profiles as those presets // will be selected by the following call of this->update_compatible(PresetSelectCompatibleType::Always). - const Preset* initial_printer = printers.find_preset(initial_printer_profile_name); + const Preset *initial_printer = printers.find_preset(initial_printer_profile_name); // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. - const Preset* preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, - preferred_selection.printer_variant); + const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); CNumericLocalesSetter locales_setter; // Orca: load from orca_presets // const auto os_presets = config.get_machine_settings(initial_printer_profile_name); - std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); - std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); + std::string initial_print_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_PRINT_NAME); + std::string initial_filament_profile_name = config.get_printer_setting(initial_printer_profile_name, PRESET_FILAMENT_NAME); - // BBS: set default print/filament profiles to BBL's default setting - if (preferred_printer) { + //BBS: set default print/filament profiles to BBL's default setting + if (preferred_printer) + { const std::string& prefered_print_profile = preferred_printer->config.opt_string("default_print_profile"); if ((!initial_print_profile_name.compare("Default Setting")) && (prefered_print_profile.size() > 0)) initial_print_profile_name = prefered_print_profile; - const std::vector& prefered_filament_profiles = - preferred_printer->config.option("default_filament_profile")->values; + const std::vector& prefered_filament_profiles = preferred_printer->config.option("default_filament_profile")->values; if ((!initial_filament_profile_name.compare(ORCA_DEFAULT_FILAMENT_PLACEHOLDER)) && (prefered_filament_profiles.size() > 0)) { // Check if preferred filament is visible const Preset* preferred_preset = this->filaments.find_preset(prefered_filament_profiles[0], false); @@ -3044,15 +2969,15 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p // Selects the profile, leaves it to -1 if the initial profile name is empty or if it was not found. prints.select_preset_by_name_strict(initial_print_profile_name); filaments.select_preset_by_name_strict(initial_filament_profile_name); - // sla_prints.select_preset_by_name_strict(initial_sla_print_profile_name); + // sla_prints.select_preset_by_name_strict(initial_sla_print_profile_name); // sla_materials.select_preset_by_name_strict(initial_sla_material_profile_name); // Load the names of the other filament profiles selected for a multi-material printer. // Load it even if the current printer technology is SLA. // The possibly excessive filament names will be later removed with this->update_multi_material_filament_presets() // once the FFF technology gets selected. - this->filament_presets = {filaments.get_selected_preset_name()}; - for (unsigned int i = 1; i < 1000; ++i) { + this->filament_presets = { filaments.get_selected_preset_name() }; + for (unsigned int i = 1; i < 1000; ++ i) { char name[64]; sprintf(name, "filament_%02u", i); auto f_name = config.get_printer_setting(initial_printer_profile_name, name); @@ -3074,18 +2999,14 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p std::vector multi_filament_colors; if (config.has_printer_setting(initial_printer_profile_name, "filament_multi_colors")) { - boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(multi_filament_colors, config.get_printer_setting(initial_printer_profile_name, "filament_multi_colors"), boost::algorithm::is_any_of(",")); } - if (multi_filament_colors.size() == 0) - project_config.option("filament_multi_colour")->values = filament_colors; - else - project_config.option("filament_multi_colour")->values = multi_filament_colors; + if (multi_filament_colors.size() == 0) project_config.option("filament_multi_colour")->values = filament_colors; + else project_config.option("filament_multi_colour")->values = multi_filament_colors; std::vector filament_color_types; if (config.has_printer_setting(initial_printer_profile_name, "filament_color_types")) { - boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(filament_color_types, config.get_printer_setting(initial_printer_profile_name, "filament_color_types"), boost::algorithm::is_any_of(",")); } filament_color_types.resize(filament_presets.size(), "1"); project_config.option("filament_colour_type")->values = filament_color_types; @@ -3101,32 +3022,25 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p std::vector extruder_ams_count_str; if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) { - boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), - boost::algorithm::is_any_of(",")); + boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(",")); } this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); auto flush_volumes_matrix = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), - flush_volumes_matrix.end()); + project_config.option("flush_volumes_matrix")->values = std::vector(flush_volumes_matrix.begin(), flush_volumes_matrix.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_vector")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_vector"), boost::algorithm::is_any_of("|")); auto flush_volumes_vector = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), - flush_volumes_vector.end()); + project_config.option("flush_volumes_vector")->values = std::vector(flush_volumes_vector.begin(), flush_volumes_vector.end()); } if (config.has_printer_setting(initial_printer_profile_name, "flush_multiplier")) { - boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), - boost::algorithm::is_any_of("|")); + boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_multiplier"), boost::algorithm::is_any_of("|")); auto flush_multipliers = matrix | boost::adaptors::transformed(boost::lexical_cast); - project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), - flush_multipliers.end()); + project_config.option("flush_multiplier")->values = std::vector(flush_multipliers.begin(), flush_multipliers.end()); } load_mixed_filament_settings(project_config, config, initial_printer_profile_name, filament_presets.size(), true); @@ -3138,17 +3052,17 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p this->update_multi_material_filament_presets(); if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) { - // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new - // printer. Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. + // Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new printer. + // Only run this code if just a filament / SLA material was installed by Config Wizard for an active Printer. auto printer_technology = printers.get_selected_preset().printer_technology(); - if (printer_technology == ptFFF && !preferred_selection.filament.empty()) { + if (printer_technology == ptFFF && ! preferred_selection.filament.empty()) { std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_FILAMENT, preferred_selection.filament); if (auto it = filaments.find_preset_internal(preferred_preset_name); - it != filaments.end() && (it->name == preferred_preset_name) && it->is_visible && it->is_compatible) { + it != filaments.end() && (it->name == preferred_preset_name ) && it->is_visible && it->is_compatible) { filaments.select_preset_by_name_strict(preferred_preset_name); this->filament_presets.front() = filaments.get_selected_preset_name(); } - } else if (printer_technology == ptSLA && !preferred_selection.sla_material.empty()) { + } else if (printer_technology == ptSLA && ! preferred_selection.sla_material.empty()) { std::string preferred_preset_name = get_preset_name_by_alias(Preset::Type::TYPE_SLA_MATERIAL, preferred_selection.sla_material); if (auto it = sla_materials.find_preset_internal(preferred_preset_name); it != sla_materials.end() && it->is_visible && it->is_compatible) @@ -3157,18 +3071,17 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p } std::string first_visible_filament_name; - for (auto& fp : filament_presets) { + for (auto & fp : filament_presets) { // Orca: also match the ORCA_DEFAULT_FILAMENT_PLACEHOLDER placeholder — see update_selections. - if (auto it = filaments.find_preset_internal(fp); - fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { + if (auto it = filaments.find_preset_internal(fp); fp == ORCA_DEFAULT_FILAMENT_PLACEHOLDER || it == filaments.end() || !it->is_visible || !it->is_compatible) { if (first_visible_filament_name.empty()) first_visible_filament_name = filaments.first_compatible().name; fp = first_visible_filament_name; } } - const Preset& current_printer = printers.get_selected_preset(); - const Preset* base_printer = printers.get_preset_base(current_printer); + const Preset& current_printer = printers.get_selected_preset(); + const Preset* base_printer = printers.get_preset_base(current_printer); bool use_default_nozzle_volume_type = true; if (base_printer) { std::string prev_nozzle_volume_type = config.get_nozzle_volume_types_from_config(base_printer->name); @@ -3181,12 +3094,11 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p } if (use_default_nozzle_volume_type) { - project_config.option("nozzle_volume_type")->values = - current_printer.config.option("default_nozzle_volume_type")->values; + project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; } else { // Orca: make sure `nozzle_volume_type` not shorter than `default_nozzle_volume_type`, otherwise we got array out of bound access // later in `Tab::switch_excluder` - auto& opt = project_config.option("nozzle_volume_type")->values; + auto& opt = project_config.option("nozzle_volume_type")->values; const auto& opt_default = current_printer.config.option("default_nozzle_volume_type")->values; while (opt.size() < opt_default.size()) { opt.emplace_back(opt_default[opt.size()]); @@ -3200,17 +3112,15 @@ void PresetBundle::load_selections(AppConfig& config, const PresetPreferences& p if (!initial_physical_printer_name.empty()) physical_printers.select_printer(initial_physical_printer_name); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": finished, preferred printer_model_id %1%") % preferred_selection.printer_model_id; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, preferred printer_model_id %1%")%preferred_selection.printer_model_id; } // Export selections (current print, current filaments, current printer) into config.ini // BBS: change directories by design -void PresetBundle::export_selections(AppConfig& config) +void PresetBundle::export_selections(AppConfig &config) { - assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); - // assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || - // filaments.get_selected_preset_name() == filament_presets.front()); + assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1); + //assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front()); config.clear_section("presets"); auto printer_name = printers.get_selected_preset_name(); config.set("presets", PRESET_PRINTER_NAME, printer_name); @@ -3227,7 +3137,7 @@ void PresetBundle::export_selections(AppConfig& config) config.clear_printer_settings(printer_name); config.set_printer_setting(printer_name, PRESET_PRINTER_NAME, printer_name); config.set_printer_setting(printer_name, PRESET_PRINT_NAME, prints.get_selected_preset_name()); - config.set_printer_setting(printer_name, PRESET_FILAMENT_NAME, filament_presets.front()); + config.set_printer_setting(printer_name, PRESET_FILAMENT_NAME, filament_presets.front()); config.set_printer_setting(printer_name, "curr_bed_type", config.get("curr_bed_type")); for (unsigned i = 1; i < filament_presets.size(); ++i) { char name[64]; @@ -3237,75 +3147,68 @@ void PresetBundle::export_selections(AppConfig& config) } // Load project config data into app config CNumericLocalesSetter locales_setter; - std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); + std::string filament_colors = boost::algorithm::join(project_config.option("filament_colour")->values, ","); config.set_printer_setting(printer_name, "filament_colors", filament_colors); // Load filament multi color data into app config - std::string filament_multi_colors = boost::algorithm::join(project_config.option("filament_multi_colour")->values, - ","); + std::string filament_multi_colors = boost::algorithm::join(project_config.option("filament_multi_colour")->values, ","); config.set_printer_setting(printer_name, "filament_multi_colors", filament_multi_colors); // Load filament color type data into app config - std::string filament_color_types = boost::algorithm::join(project_config.option("filament_colour_type")->values, - ","); + std::string filament_color_types = boost::algorithm::join(project_config.option("filament_colour_type")->values, ","); config.set_printer_setting(printer_name, "filament_color_types", filament_color_types); // Load ams counts data into app config - std::string extruder_ams_count_str = boost::algorithm::join(save_extruder_ams_count_to_string(this->extruder_ams_counts), ","); + std::string extruder_ams_count_str = boost::algorithm::join(save_extruder_ams_count_to_string(this->extruder_ams_counts), ","); config.set_printer_setting(printer_name, "extruder_ams_count", extruder_ams_count_str); std::string flush_volumes_matrix = boost::algorithm::join(project_config.option("flush_volumes_matrix")->values | - boost::adaptors::transformed( - static_cast(std::to_string)), - "|"); + boost::adaptors::transformed(static_cast(std::to_string)), + "|"); config.set_printer_setting(printer_name, "flush_volumes_matrix", flush_volumes_matrix); std::string flush_volumes_vector = boost::algorithm::join(project_config.option("flush_volumes_vector")->values | - boost::adaptors::transformed( - static_cast(std::to_string)), - "|"); + boost::adaptors::transformed(static_cast(std::to_string)), + "|"); config.set_printer_setting(printer_name, "flush_volumes_vector", flush_volumes_vector); + std::string flush_multiplier_str = boost::algorithm::join(project_config.option("flush_multiplier")->values | - boost::adaptors::transformed( - static_cast(std::to_string)), + boost::adaptors::transformed(static_cast(std::to_string)), "|"); config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str); // Mixed-color filament metadata goes into the per-printer snapshot next to the filament list // it indexes (see load_mixed_filament_settings). Bools are ','-joined and the component, ratio // and range strings '|'-joined; the gradient curve is escaped instead, as it contains '|'. - auto join_bools = [](const std::vector& vals) { + auto join_bools = [](const std::vector &vals) { std::string s; for (size_t i = 0; i < vals.size(); ++i) { - if (i > 0) - s += ","; + if (i > 0) s += ","; s += (vals[i] ? "1" : "0"); } return s; }; - if (auto* opt = project_config.option("filament_is_mixed")) + if (auto *opt = project_config.option("filament_is_mixed")) config.set_printer_setting(printer_name, "filament_is_mixed", join_bools(opt->values)); - if (auto* opt = project_config.option("filament_mixed_components")) + if (auto *opt = project_config.option("filament_mixed_components")) config.set_printer_setting(printer_name, "filament_mixed_components", boost::algorithm::join(opt->values, "|")); - if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + if (auto *opt = project_config.option("filament_mixed_sublayer_ratios")) config.set_printer_setting(printer_name, "filament_mixed_sublayer_ratios", boost::algorithm::join(opt->values, "|")); - if (auto* opt = project_config.option("filament_mixed_gradient")) + if (auto *opt = project_config.option("filament_mixed_gradient")) config.set_printer_setting(printer_name, "filament_mixed_gradient", join_bools(opt->values)); - if (auto* opt = project_config.option("filament_mixed_gradient_range")) + if (auto *opt = project_config.option("filament_mixed_gradient_range")) config.set_printer_setting(printer_name, "filament_mixed_gradient_range", boost::algorithm::join(opt->values, "|")); - if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + if (auto *opt = project_config.option("filament_mixed_gradient_curve")) config.set_printer_setting(printer_name, "filament_mixed_gradient_curve", escape_strings_cstyle(opt->values)); - if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + if (auto *opt = project_config.option("filament_mixed_gradient_per_part")) config.set_printer_setting(printer_name, "filament_mixed_gradient_per_part", join_bools(opt->values)); // BBS - // config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); - // config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); - // config.set("presets", "physical_printer", physical_printers.get_selected_full_printer_name()); - // BBS: add config related log - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": printer %1%, print %2%, filaments[0] %3% ") % printers.get_selected_preset_name() % - prints.get_selected_preset_name() % filament_presets[0]; + //config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); + //config.set("presets", "sla_material", sla_materials.get_selected_preset_name()); + //config.set("presets", "physical_printer", physical_printers.get_selected_full_printer_name()); + //BBS: add config related log + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": printer %1%, print %2%, filaments[0] %3% ")%printers.get_selected_preset_name() % prints.get_selected_preset_name() %filament_presets[0]; } void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) @@ -3316,12 +3219,12 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) else { filament_presets.resize(n); } - ConfigOptionStrings* filament_color = project_config.option("filament_colour"); - ConfigOptionStrings* filament_multi_color = project_config.option("filament_multi_colour"); - ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings* filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); // Which slots are new is a fact about the arrays below, not about filament_presets: // update_multi_material_filament_presets() tops that list up to the nozzle count on its own, @@ -3357,12 +3260,12 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) opt->values.resize(n, false); - // BBS set new filament color to new_color + //BBS set new filament color to new_color if (!new_color.empty()) { for (size_t i = old_slot_count; i < n; i++) { - filament_color->values[i] = new_color; + filament_color->values[i] = new_color; filament_multi_color->values[i] = new_color; - filament_color_type->values[i] = "1"; // default color type + filament_color_type->values[i] = "1"; // default color type } } @@ -3377,8 +3280,8 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) // update edited_preset { - Preset& edited_preset = filaments.get_edited_preset(); - bool edited_preset_deleted = true; + Preset& edited_preset = filaments.get_edited_preset(); + bool edited_preset_deleted = true; for (std::string filament_preset_name : filament_presets) { if (filament_preset_name == edited_preset.name) { edited_preset_deleted = false; @@ -3389,12 +3292,12 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) } } - ConfigOptionStrings* filament_color = project_config.option("filament_colour"); - ConfigOptionStrings* filament_multi_color = project_config.option("filament_multi_colour"); - ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings *filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour"); + ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts* filament_map = project_config.option("filament_map"); + ConfigOptionInts* filament_nozzle_map = project_config.option("filament_nozzle_map"); + ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); if (filament_color->values.size() > to_del_flament_id) { filament_color->values.erase(filament_color->values.begin() + to_del_flament_id); if (filament_map->values.size() > to_del_flament_id) { @@ -3406,7 +3309,8 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) if (filament_volume_map->values.size() > to_del_flament_id) { filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id); } - } else { + } + else { filament_color->values.resize(to_del_flament_id); filament_map->values.resize(to_del_flament_id, 1); filament_nozzle_map->values.resize(to_del_flament_id, 0); @@ -3430,27 +3334,29 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) // *physical* filament must be remapped out of every mix before the arrays themselves shrink. // Deleting a mixed slot needs no remap (nothing references a mixed slot as a component). { - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - auto* comp_opt = project_config.option("filament_mixed_components"); + auto *is_mixed_opt = project_config.option("filament_is_mixed"); + auto *comp_opt = project_config.option("filament_mixed_components"); if (is_mixed_opt && comp_opt) { - bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size() || !is_mixed_opt->values[to_del_flament_id]); + bool del_is_physical = (to_del_flament_id >= is_mixed_opt->values.size() + || !is_mixed_opt->values[to_del_flament_id]); if (del_is_physical) - remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values, to_del_flament_id + 1); + remap_mixed_components_on_delete(is_mixed_opt->values, comp_opt->values, + to_del_flament_id + 1); } if (is_mixed_opt) erase_or_resize(is_mixed_opt->values); if (comp_opt) erase_or_resize(comp_opt->values); } - if (auto* opt = project_config.option("filament_mixed_sublayer_ratios")) + if (auto *opt = project_config.option("filament_mixed_sublayer_ratios")) erase_or_resize(opt->values); - if (auto* opt = project_config.option("filament_mixed_gradient")) + if (auto *opt = project_config.option("filament_mixed_gradient")) erase_or_resize(opt->values); - if (auto* opt = project_config.option("filament_mixed_gradient_range")) + if (auto *opt = project_config.option("filament_mixed_gradient_range")) erase_or_resize(opt->values); - if (auto* opt = project_config.option("filament_mixed_gradient_curve")) + if (auto *opt = project_config.option("filament_mixed_gradient_curve")) erase_or_resize(opt->values); - if (auto* opt = project_config.option("filament_mixed_gradient_per_part")) + if (auto *opt = project_config.option("filament_mixed_gradient_per_part")) erase_or_resize(opt->values); update_multi_material_filament_presets(to_del_flament_id); @@ -3458,13 +3364,13 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id) bool PresetBundle::is_mixed_filament(size_t idx) const { - auto* opt = project_config.option("filament_is_mixed"); + auto *opt = project_config.option("filament_is_mixed"); return opt && idx < opt->values.size() && opt->values[idx]; } size_t PresetBundle::num_mixed_filaments() const { - auto* opt = project_config.option("filament_is_mixed"); + auto *opt = project_config.option("filament_is_mixed"); return opt == nullptr ? 0 : size_t(std::count(opt->values.begin(), opt->values.end(), true)); } @@ -3487,16 +3393,17 @@ std::vector PresetBundle::physical_filament_config_indices() const return indices; } + void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) { combox_info.clear(); - for (auto& entry : filament_ams_list) { - auto& ams = entry.second; - auto filament_id = ams.opt_string("filament_id", 0u); - auto filament_color = ams.opt_string("filament_colour", 0u); - auto ams_name = ams.opt_string("tray_name", 0u); - auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); - auto filament_multi_color = ams.opt("filament_multi_colour")->values; + for (auto &entry : filament_ams_list) { + auto &ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto ams_name = ams.opt_string("tray_name", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + auto filament_multi_color = ams.opt("filament_multi_colour")->values; if (filament_id.empty()) { continue; } @@ -3507,18 +3414,15 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) combox_info.ams_names.push_back(ams_name); continue; } - auto iter = std::find_if(filaments.begin(), filaments.end(), [this, &filament_id](auto& f) { - return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; - }); + auto iter = std::find_if(filaments.begin(), filaments.end(), + [this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); if (iter == filaments.end()) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(": filament_id %1% not found or system or compatible") % filament_id; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; auto filament_type = ams.opt_string("filament_type", 0u); if (!filament_type.empty()) { filament_type = "Generic " + filament_type; - iter = std::find_if(filaments.begin(), filaments.end(), [&filament_type](auto& f) { - return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); - }); + iter = std::find_if(filaments.begin(), filaments.end(), + [&filament_type](auto &f) { return f.is_compatible && f.is_system && boost::algorithm::starts_with(f.name, filament_type); }); } if (iter == filaments.end()) { // Prefer old selection @@ -3542,18 +3446,13 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info) } } -unsigned int PresetBundle::sync_ams_list(std::vector>& unknowns, - bool use_map, - std::map& maps, - bool enable_append, - MergeFilamentInfo& merge_info, - bool color_only) +unsigned int PresetBundle::sync_ams_list(std::vector> &unknowns, bool use_map, std::map &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append; std::vector ams_filament_presets; std::vector ams_filament_colors; std::vector ams_filament_color_types; - std::vector ams_array_maps; + std::vector ams_array_maps; ams_multi_color_filment.clear(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_ams_list size: %1%") % filament_ams_list.size(); struct AmsInfo @@ -3561,24 +3460,24 @@ unsigned int PresetBundle::sync_ams_list(std::vector mutli_filament_color; }; auto is_double_extruder = get_printer_extruder_count() == 2; std::vector ams_infos; - int index = 0; - for (auto& entry : filament_ams_list) { - auto& ams = entry.second; - auto filament_id = ams.opt_string("filament_id", 0u); - auto filament_color = ams.opt_string("filament_colour", 0u); - auto filament_color_type = ams.opt_string("filament_colour_type", 0u); - auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); + int index = 0; + for (auto &entry : filament_ams_list) { + auto & ams = entry.second; + auto filament_id = ams.opt_string("filament_id", 0u); + auto filament_color = ams.opt_string("filament_colour", 0u); + auto filament_color_type = ams.opt_string("filament_colour_type", 0u); + auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); auto filament_multi_color = ams.opt("filament_multi_colour")->values; - auto ams_id = ams.opt_string("ams_id", 0u); - auto slot_id = ams.opt_string("slot_id", 0u); - auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); + auto ams_id = ams.opt_string("ams_id", 0u); + auto slot_id = ams.opt_string("slot_id", 0u); + auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u); ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color}); AMSMapInfo temp = {ams_id, slot_id}; ams_array_maps.push_back(temp); @@ -3590,7 +3489,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorconfig.opt_string("filament_type", 0u)); if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) { - iter = it; - best_len = preset_type.size(); + iter = it; + best_len = preset_type.size(); filament_type = "Generic " + it->config.opt_string("filament_type", 0u); } } @@ -3665,28 +3564,26 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname, filament_type) ? - (has_type ? - L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : - L("The filament model is unknown. Generic filament presets will be used.")) : - (has_type ? - L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") : - L("The filament model is unknown. A random filament preset will be used."))); + unknowns.emplace_back(&ams, boost::algorithm::starts_with(iter->name, filament_type) ? + (has_type ? L("The filament may not be compatible with the current machine settings. Generic filament presets will be used.") : + L("The filament model is unknown. Generic filament presets will be used.")) : + (has_type ? L("The filament may not be compatible with the current machine settings. A random filament preset will be used.") : + L("The filament model is unknown. A random filament preset will be used."))); filament_id = iter->filament_id; } ams_filament_presets.push_back(iter->name); @@ -3697,26 +3594,25 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_colour"); - ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type"); - ConfigOptionInts* filament_map = project_config.option("filament_map"); - ConfigOptionInts* filament_volume_map = project_config.option("filament_volume_map"); + ConfigOptionStrings *filament_color = project_config.option("filament_colour"); + ConfigOptionStrings *filament_color_type = project_config.option("filament_colour_type"); + ConfigOptionInts * filament_map = project_config.option("filament_map"); + ConfigOptionInts * filament_volume_map = project_config.option("filament_volume_map"); // Snapshot and temporarily strip mixed filament slots so AMS sync operates on physical // filaments only. A mixed slot is virtual and has no tray to sync against; leaving it in // would let AMS mapping overwrite it and would break the physical-first slot ordering the // rest of the feature relies on. The slots are re-appended verbatim after the sync. - struct MixedSlotSnapshot - { + struct MixedSlotSnapshot { std::string preset; std::string color; std::string color_type; std::string mixed_components; std::string mixed_sublayer_ratios; - bool mixed_gradient = false; + bool mixed_gradient = false; std::string mixed_gradient_range; std::string mixed_gradient_curve; - bool mixed_gradient_per_part = false; + bool mixed_gradient_per_part = false; }; std::vector mixed_snapshots; auto* is_mixed_opt = project_config.option("filament_is_mixed"); @@ -3732,48 +3628,35 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfilament_presets[i]; - snap.color = (i < filament_color->values.size()) ? filament_color->values[i] : ""; + snap.color = (i < filament_color->values.size()) ? filament_color->values[i] : ""; snap.color_type = (i < filament_color_type->values.size()) ? filament_color_type->values[i] : ""; - if (mixed_comp_opt && i < mixed_comp_opt->values.size()) - snap.mixed_components = mixed_comp_opt->values[i]; - if (mixed_ratios_opt && i < mixed_ratios_opt->values.size()) - snap.mixed_sublayer_ratios = mixed_ratios_opt->values[i]; - if (mixed_gradient_opt && i < mixed_gradient_opt->values.size()) - snap.mixed_gradient = mixed_gradient_opt->values[i]; - if (mixed_grad_range_opt && i < mixed_grad_range_opt->values.size()) - snap.mixed_gradient_range = mixed_grad_range_opt->values[i]; - if (mixed_grad_curve_opt && i < mixed_grad_curve_opt->values.size()) - snap.mixed_gradient_curve = mixed_grad_curve_opt->values[i]; - if (mixed_per_part_opt && i < mixed_per_part_opt->values.size()) - snap.mixed_gradient_per_part = mixed_per_part_opt->values[i]; + if (mixed_comp_opt && i < mixed_comp_opt->values.size()) snap.mixed_components = mixed_comp_opt->values[i]; + if (mixed_ratios_opt && i < mixed_ratios_opt->values.size()) snap.mixed_sublayer_ratios = mixed_ratios_opt->values[i]; + if (mixed_gradient_opt && i < mixed_gradient_opt->values.size()) snap.mixed_gradient = mixed_gradient_opt->values[i]; + if (mixed_grad_range_opt && i < mixed_grad_range_opt->values.size()) snap.mixed_gradient_range = mixed_grad_range_opt->values[i]; + if (mixed_grad_curve_opt && i < mixed_grad_curve_opt->values.size()) snap.mixed_gradient_curve = mixed_grad_curve_opt->values[i]; + if (mixed_per_part_opt && i < mixed_per_part_opt->values.size()) snap.mixed_gradient_per_part = mixed_per_part_opt->values[i]; mixed_snapshots.push_back(snap); } if (!mixed_snapshots.empty()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": stripping " << mixed_snapshots.size() - << " mixed filament slot(s) before AMS sync"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": stripping " << mixed_snapshots.size() << " mixed filament slot(s) before AMS sync"; size_t phys_count = this->filament_presets.size() - mixed_snapshots.size(); this->filament_presets.resize(phys_count); filament_color->values.resize(phys_count); filament_color_type->values.resize(phys_count); filament_map->values.resize(phys_count, 1); is_mixed_opt->values.resize(phys_count); - if (mixed_comp_opt) - mixed_comp_opt->values.resize(phys_count); - if (mixed_ratios_opt) - mixed_ratios_opt->values.resize(phys_count); - if (mixed_gradient_opt) - mixed_gradient_opt->values.resize(phys_count); - if (mixed_grad_range_opt) - mixed_grad_range_opt->values.resize(phys_count); - if (mixed_grad_curve_opt) - mixed_grad_curve_opt->values.resize(phys_count); - if (mixed_per_part_opt) - mixed_per_part_opt->values.resize(phys_count); + if (mixed_comp_opt) mixed_comp_opt->values.resize(phys_count); + if (mixed_ratios_opt) mixed_ratios_opt->values.resize(phys_count); + if (mixed_gradient_opt) mixed_gradient_opt->values.resize(phys_count); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(phys_count); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(phys_count); + if (mixed_per_part_opt) mixed_per_part_opt->values.resize(phys_count); } } if (color_only) { - auto get_map_index = [&ams_infos](const std::vector& infos, const AMSMapInfo& temp) { + auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { for (int i = 0; i < infos.size(); i++) { if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { ams_infos[i].is_map = true; @@ -3789,7 +3672,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_multi_colour"); + ConfigOptionStrings *project_multi_color = project_config.option("filament_multi_colour"); if (project_multi_color) { for (size_t i = 0; i < std::min(exist_multi_color_filment.size(), project_multi_color->values.size()); i++) { std::vector colors = split_string(project_multi_color->values[i], ' '); @@ -3808,7 +3691,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector= 0 && valid_index < int(ams_filament_colors.size()) && !ams_filament_colors[valid_index].empty()) { exist_colors[i] = ams_filament_colors[valid_index]; - mapped_any = true; + mapped_any = true; if (valid_index < int(ams_multi_color_filment.size()) && !ams_multi_color_filment[valid_index].empty()) { exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; } else { @@ -3833,11 +3716,11 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = exist_colors; + filament_color->values = exist_colors; ams_multi_color_filment = exist_multi_color_filment; merge_info.merges.clear(); } else if (use_map) { - auto check_has_merge_info = [](std::map& maps, MergeFilamentInfo& merge_info, int exist_colors_size) { + auto check_has_merge_info = [](std::map &maps, MergeFilamentInfo &merge_info, int exist_colors_size) { std::set done; for (auto it_i = maps.begin(); it_i != maps.end(); ++it_i) { std::vector same_ams; @@ -3846,7 +3729,7 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfirst) != done.end()) { continue; } - if (it_i->second.slot_id == "" || it_i->second.ams_id == "") { + if (it_i->second.slot_id == "" || it_i->second.ams_id == ""){ continue; } if (it_i->second.slot_id == it_j->second.slot_id && it_i->second.ams_id == it_j->second.ams_id) { @@ -3859,8 +3742,8 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.size()); - auto get_map_index = [&ams_infos](const std::vector& infos, const AMSMapInfo& temp) { + check_has_merge_info(maps, merge_info,filament_color->values.size()); + auto get_map_index = [&ams_infos](const std::vector &infos, const AMSMapInfo &temp) { for (int i = 0; i < infos.size(); i++) { if (infos[i].slot_id == temp.slot_id && infos[i].ams_id == temp.ams_id) { ams_infos[i].is_map = true; @@ -3870,8 +3753,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector need_append_colors; - auto exist_colors = filament_color->values; - auto exist_color_types = filament_color_type->values; + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; auto exist_filament_presets = this->filament_presets; std::vector> exist_multi_color_filment; exist_multi_color_filment.resize(exist_colors.size()); @@ -3879,57 +3762,54 @@ unsigned int PresetBundle::sync_ams_list(std::vector= 0 && valid_index < ams_filament_presets.size()) { - exist_colors[i] = ams_filament_colors[valid_index]; - exist_color_types[i] = ams_filament_color_types[valid_index]; - exist_filament_presets[i] = ams_filament_presets[valid_index]; + exist_colors[i] = ams_filament_colors[valid_index]; + exist_color_types[i] = ams_filament_color_types[valid_index]; + exist_filament_presets[i] = ams_filament_presets[valid_index]; exist_multi_color_filment[i] = ams_multi_color_filment[valid_index]; } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (mapping exist)"; } } } - for (size_t i = 0; i < ams_infos.size(); i++) { // check append + for (size_t i = 0; i < ams_infos.size(); i++) {// check append if (ams_infos[i].valid) { if (i >= ams_filament_presets.size()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "check error: array bound (check append)"; continue; } - ams_infos[i].filament_preset = ams_filament_presets[i]; + ams_infos[i].filament_preset = ams_filament_presets[i]; ams_infos[i].mutli_filament_color = ams_multi_color_filment[i]; if (!ams_infos[i].is_map) { need_append_colors.emplace_back(ams_infos[i]); - ams_filament_colors[i] = ""; + ams_filament_colors[i] = ""; ams_filament_color_types[i] = ""; - ams_filament_presets[i] = ""; - ams_multi_color_filment[i] = std::vector(); + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); } - } else { - ams_filament_colors[i] = ""; + } + else { + ams_filament_colors[i] = ""; ams_filament_color_types[i] = ""; - ams_filament_presets[i] = ""; - ams_multi_color_filment[i] = std::vector(); + ams_filament_presets[i] = ""; + ams_multi_color_filment[i] = std::vector(); } } - // delete redundant color - ams_filament_colors.erase(std::remove_if(ams_filament_colors.begin(), ams_filament_colors.end(), - [](std::string& value) { return value.empty(); }), + //delete redundant color + ams_filament_colors.erase(std::remove_if(ams_filament_colors.begin(), ams_filament_colors.end(), [](std::string &value) { return value.empty(); }), ams_filament_colors.end()); - ams_filament_color_types.erase(std::remove_if(ams_filament_color_types.begin(), ams_filament_color_types.end(), - [](std::string& value) { return value.empty(); }), + ams_filament_color_types.erase(std::remove_if(ams_filament_color_types.begin(), ams_filament_color_types.end(), [](std::string &value) { return value.empty(); }), ams_filament_color_types.end()); - ams_filament_presets.erase(std::remove_if(ams_filament_presets.begin(), ams_filament_presets.end(), - [](std::string& value) { return value.empty(); }), + ams_filament_presets.erase(std::remove_if(ams_filament_presets.begin(), ams_filament_presets.end(), [](std::string &value) { return value.empty(); }), ams_filament_presets.end()); ams_multi_color_filment.erase(std::remove_if(ams_multi_color_filment.begin(), ams_multi_color_filment.end(), - [](std::vector& value) { return value.empty(); }), + [](std::vector &value) { return value.empty(); }), ams_multi_color_filment.end()); if (need_append_colors.size() > 0 && enable_append) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "need_append_colors.size() > 0 && enable_append"; - auto get_idx_in_array = [](std::vector& presets, std::vector& colors, const std::string& preset, - const std::string& color) -> int { + auto get_idx_in_array = [](std::vector &presets, std::vector &colors, const std::string &preset, const std::string &color) -> int { for (size_t i = 0; i < presets.size(); i++) { if (presets[i] == preset && colors[i] == color) { return i; @@ -3937,12 +3817,11 @@ unsigned int PresetBundle::sync_ams_list(std::vector= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER) { + for (size_t i = 0; i < need_append_colors.size(); i++){ + if (exist_filament_presets.size() >= MAXIMUM_AMS_SYNC_FILAMENT_NUMBER){ break; } - auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, - need_append_colors[i].filament_color); + auto idx = get_idx_in_array(exist_filament_presets, exist_colors, need_append_colors[i].filament_preset, need_append_colors[i].filament_color); if (idx >= 0) { continue; } @@ -3952,19 +3831,21 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues = exist_colors; + filament_color->values = exist_colors; filament_color_type->values = exist_color_types; - ams_multi_color_filment = exist_multi_color_filment; - this->filament_presets = exist_filament_presets; + ams_multi_color_filment = exist_multi_color_filment; + this->filament_presets = exist_filament_presets; filament_map->values.resize(exist_filament_presets.size(), 1); filament_volume_map->values.resize(exist_filament_presets.size(), static_cast(NozzleVolumeType::nvtStandard)); - } else { // overwrite; - bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), [](const AmsInfo& a) { return a.is_placeholder; }); + } + else {//overwrite; + bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(), + [](const AmsInfo& a) { return a.is_placeholder; }); if (has_placeholders) { // Orca: merge — keep existing filaments for empty slots - auto exist_colors = filament_color->values; - auto exist_color_types = filament_color_type->values; - auto exist_presets = this->filament_presets; + auto exist_colors = filament_color->values; + auto exist_color_types = filament_color_type->values; + auto exist_presets = this->filament_presets; size_t tray_count = ams_filament_presets.size(); size_t total = std::max(tray_count, exist_presets.size()); @@ -3982,8 +3863,9 @@ unsigned int PresetBundle::sync_ams_list(std::vector{ams_filament_colors[i]}); + result_multi_colors.push_back( + i < ams_multi_color_filment.size() ? ams_multi_color_filment[i] + : std::vector{ams_filament_colors[i]}); } else if (i < exist_presets.size()) { // Empty tray or beyond tray count: keep existing filament result_colors.push_back(exist_colors[i]); @@ -3992,8 +3874,9 @@ unsigned int PresetBundle::sync_ams_list(std::vectorname : filaments.first_visible().name; result_colors.push_back("#CECECE"); @@ -4011,15 +3894,15 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.resize(total, static_cast(NozzleVolumeType::nvtStandard)); } else { // BBL: existing wholesale replace - filament_color->values = ams_filament_colors; + filament_color->values = ams_filament_colors; filament_color_type->values = ams_filament_color_types; - this->filament_presets = ams_filament_presets; + this->filament_presets = ams_filament_presets; filament_map->values.resize(ams_filament_colors.size(), 1); filament_volume_map->values.resize(ams_filament_colors.size(), static_cast(NozzleVolumeType::nvtStandard)); } - auto& print_config = this->prints.get_edited_preset().config; - auto support_filament_opt = print_config.option("support_filament"); + auto& print_config = this->prints.get_edited_preset().config; + auto support_filament_opt = print_config.option("support_filament"); auto support_interface_filament_opt = print_config.option("support_interface_filament"); if (support_filament_opt->value > filament_color_type->values.size()) support_filament_opt->value = 0; @@ -4031,20 +3914,13 @@ unsigned int PresetBundle::sync_ams_list(std::vectorfilament_presets.size(); - if (is_mixed_opt) - is_mixed_opt->values.resize(new_phys_count, (unsigned char) false); - if (mixed_comp_opt) - mixed_comp_opt->values.resize(new_phys_count); - if (mixed_ratios_opt) - mixed_ratios_opt->values.resize(new_phys_count); - if (mixed_gradient_opt) - mixed_gradient_opt->values.resize(new_phys_count, (unsigned char) false); - if (mixed_grad_range_opt) - mixed_grad_range_opt->values.resize(new_phys_count); - if (mixed_grad_curve_opt) - mixed_grad_curve_opt->values.resize(new_phys_count); - if (mixed_per_part_opt) - mixed_per_part_opt->values.resize(new_phys_count, (unsigned char) false); + if (is_mixed_opt) is_mixed_opt->values.resize(new_phys_count, (unsigned char)false); + if (mixed_comp_opt) mixed_comp_opt->values.resize(new_phys_count); + if (mixed_ratios_opt) mixed_ratios_opt->values.resize(new_phys_count); + if (mixed_gradient_opt) mixed_gradient_opt->values.resize(new_phys_count, (unsigned char)false); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.resize(new_phys_count); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.resize(new_phys_count); + if (mixed_per_part_opt) mixed_per_part_opt->values.resize(new_phys_count, (unsigned char)false); for (auto& snap : mixed_snapshots) { this->filament_presets.push_back(snap.preset); @@ -4052,20 +3928,13 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.push_back(snap.color_type); ams_multi_color_filment.push_back({snap.color}); filament_map->values.push_back(1); - if (is_mixed_opt) - is_mixed_opt->values.push_back((unsigned char) true); - if (mixed_comp_opt) - mixed_comp_opt->values.push_back(snap.mixed_components); - if (mixed_ratios_opt) - mixed_ratios_opt->values.push_back(snap.mixed_sublayer_ratios); - if (mixed_gradient_opt) - mixed_gradient_opt->values.push_back((unsigned char) snap.mixed_gradient); - if (mixed_grad_range_opt) - mixed_grad_range_opt->values.push_back(snap.mixed_gradient_range); - if (mixed_grad_curve_opt) - mixed_grad_curve_opt->values.push_back(snap.mixed_gradient_curve); - if (mixed_per_part_opt) - mixed_per_part_opt->values.push_back((unsigned char) snap.mixed_gradient_per_part); + if (is_mixed_opt) is_mixed_opt->values.push_back((unsigned char)true); + if (mixed_comp_opt) mixed_comp_opt->values.push_back(snap.mixed_components); + if (mixed_ratios_opt) mixed_ratios_opt->values.push_back(snap.mixed_sublayer_ratios); + if (mixed_gradient_opt) mixed_gradient_opt->values.push_back((unsigned char)snap.mixed_gradient); + if (mixed_grad_range_opt) mixed_grad_range_opt->values.push_back(snap.mixed_gradient_range); + if (mixed_grad_curve_opt) mixed_grad_curve_opt->values.push_back(snap.mixed_gradient_curve); + if (mixed_per_part_opt) mixed_per_part_opt->values.push_back((unsigned char)snap.mixed_gradient_per_part); } } @@ -4079,38 +3948,36 @@ unsigned int PresetBundle::sync_ams_list(std::vector exsit_multi_colors; - for (auto& fil_item : ams_multi_color_filment) { - if (fil_item.empty()) - break; + for (auto &fil_item : ams_multi_color_filment){ + if (fil_item.empty()) break; if (fil_item.size() == 1) exsit_multi_colors.push_back(fil_item[0]); else { std::string colors = ""; - for (auto& color : fil_item) { - colors += color + " "; + for (auto &color : fil_item){ + colors += color + " "; } colors.erase(colors.size() - 1); // remove last space exsit_multi_colors.push_back(colors); } } - ConfigOptionStrings* filament_multi_colour = project_config.option("filament_multi_colour"); + ConfigOptionStrings *filament_multi_colour = project_config.option("filament_multi_colour"); filament_multi_colour->resize(exsit_multi_colors.size()); filament_multi_colour->values = exsit_multi_colors; } -std::vector PresetBundle::get_used_tpu_filaments(const std::vector& used_filaments) +std::vector PresetBundle::get_used_tpu_filaments(const std::vector &used_filaments) { std::vector tpu_filaments; for (size_t i = 0; i < this->filament_presets.size(); ++i) { auto iter = std::find(used_filaments.begin(), used_filaments.end(), i + 1); - if (iter == used_filaments.end()) - continue; + if (iter == used_filaments.end()) continue; std::string filament_name = this->filament_presets[i]; for (int f_index = 0; f_index < this->filaments.size(); f_index++) { - PresetCollection* filament_presets = &this->filaments; - Preset* preset = &filament_presets->preset(f_index); - int size = this->filaments.size(); + PresetCollection *filament_presets = &this->filaments; + Preset *preset = &filament_presets->preset(f_index); + int size = this->filaments.size(); if (preset && filament_name.compare(preset->name) == 0) { std::string display_filament_type; std::string filament_type = preset->config.get_filament_type(display_filament_type); @@ -4131,20 +3998,18 @@ void PresetBundle::set_calibrate_printer(std::string name) } if (!name.empty()) calibrate_printer = printers.find_preset(name); - const Preset& printer_preset = calibrate_printer ? *calibrate_printer : printers.get_edited_preset(); + const Preset & printer_preset = calibrate_printer ? *calibrate_printer : printers.get_edited_preset(); const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer_preset); - DynamicPrintConfig config; + DynamicPrintConfig config; config.set_key_value("printer_preset", new ConfigOptionString(active_printer.preset.name)); - const ConfigOption* opt = active_printer.preset.config.option("nozzle_diameter"); - if (opt) - config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast(opt)->values.size())); + const ConfigOption *opt = active_printer.preset.config.option("nozzle_diameter"); + if (opt) config.set_key_value("num_extruders", new ConfigOptionInt((int) static_cast(opt)->values.size())); calibrate_filaments.clear(); for (size_t i = filaments.num_default_presets(); i < filaments.size(); ++i) { - const Preset& preset = filaments.m_presets[i]; + const Preset & preset = filaments.m_presets[i]; const PresetWithVendorProfile this_preset_with_vendor_profile = filaments.get_preset_with_vendor_profile(preset); - bool is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); - if (is_compatible) - calibrate_filaments.insert(&preset); + bool is_compatible = is_compatible_with_printer(this_preset_with_vendor_profile, active_printer, &config); + if (is_compatible) calibrate_filaments.insert(&preset); } } @@ -4165,31 +4030,25 @@ std::vector> PresetBundle::get_extruder_filament return filament_infos; } -std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string& printer_type, - std::string nozzle_diameter_str, - bool system_only) +std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only) { std::set printer_names; if ("0.0" == nozzle_diameter_str || nozzle_diameter_str.empty()) { nozzle_diameter_str = "0.4"; } - std::ostringstream stream; + std::ostringstream stream; for (auto printer_it = this->printers.begin(); printer_it != this->printers.end(); printer_it++) { - if (system_only && !printer_it->is_system) - continue; + if (system_only && !printer_it->is_system) continue; - ConfigOption* printer_model_opt = printer_it->config.option("printer_model"); - ConfigOptionString* printer_model_str = dynamic_cast(printer_model_opt); - if (!printer_model_str) - continue; + ConfigOption * printer_model_opt = printer_it->config.option("printer_model"); + ConfigOptionString *printer_model_str = dynamic_cast(printer_model_opt); + if (!printer_model_str) continue; // use printer_model as printer type - if (printer_model_str->value != printer_type) - continue; + if (printer_model_str->value != printer_type) continue; - if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) - printer_names.insert(printer_it->name); + if (printer_it->name.find(nozzle_diameter_str) != std::string::npos) printer_names.insert(printer_it->name); } assert(printer_names.size() == 1); @@ -4201,40 +4060,32 @@ std::set PresetBundle::get_printer_names_by_printer_type_and_nozzle return printer_names; } -bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string& printer_type, - std::string& nozzle_diameter_str, - std::string& setting_id, - std::string& tag_uid, - std::string& nozzle_temp_min, - std::string& nozzle_temp_max, - std::string& preset_setting_id) +bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray( + const std::string &printer_type, std::string& nozzle_diameter_str, std::string &setting_id, std::string &tag_uid, std::string &nozzle_temp_min, std::string &nozzle_temp_max, std::string& preset_setting_id) { bool is_equation = true; - std::map> filament_list = filaments.get_filament_presets(); - std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); + std::map> filament_list = filaments.get_filament_presets(); + std::set printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); - for (const Preset* preset : filament_list.find(setting_id)->second) { - if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) - continue; - if (preset && !preset->is_user()) - continue; - ConfigOption* printer_opt = const_cast(preset)->config.option("compatible_printers"); - ConfigOptionStrings* printer_strs = dynamic_cast(printer_opt); - bool compared = false; - for (const std::string& printer_str : printer_strs->values) { + for (const Preset *preset : filament_list.find(setting_id)->second) { + if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue; + if (preset && !preset->is_user()) continue; + ConfigOption * printer_opt = const_cast(preset)->config.option("compatible_printers"); + ConfigOptionStrings *printer_strs = dynamic_cast(printer_opt); + bool compared = false; + for (const std::string &printer_str : printer_strs->values) { if (printer_names.find(printer_str) != printer_names.end()) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "nozzle temp matching: preset name: " << preset->name - << " printer name: " << printer_str; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "nozzle temp matching: preset name: " << preset->name << " printer name: " << printer_str; // Compare only once if (!compared) { - compared = true; - bool min_temp_equation = false, max_temp_equation = false; - int min_nozzle_temp = std::stoi(nozzle_temp_min); - int max_nozzle_temp = std::stoi(nozzle_temp_max); - ConfigOption* opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); + compared = true; + bool min_temp_equation = false, max_temp_equation = false; + int min_nozzle_temp = std::stoi(nozzle_temp_min); + int max_nozzle_temp = std::stoi(nozzle_temp_max); + ConfigOption *opt_min = const_cast(preset)->config.option("nozzle_temperature_range_low"); if (opt_min) { - ConfigOptionInts* opt_min_ints = dynamic_cast(opt_min); + ConfigOptionInts *opt_min_ints = dynamic_cast(opt_min); min_nozzle_temp = opt_min_ints->get_at(0); if (std::to_string(min_nozzle_temp) == nozzle_temp_min) min_temp_equation = true; @@ -4243,9 +4094,9 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m nozzle_temp_min = std::to_string(min_nozzle_temp); } } - ConfigOption* opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); + ConfigOption *opt_max = const_cast(preset)->config.option("nozzle_temperature_range_high"); if (opt_max) { - ConfigOptionInts* opt_max_ints = dynamic_cast(opt_max); + ConfigOptionInts *opt_max_ints = dynamic_cast(opt_max); max_nozzle_temp = opt_max_ints->get_at(0); if (std::to_string(max_nozzle_temp) == nozzle_temp_max) max_temp_equation = true; @@ -4255,13 +4106,11 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m } } if (min_temp_equation && max_temp_equation) { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: no changed"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: no changed"; } else { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: has changed"; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " " << __LINE__ << "Determine if the temperature has changed: has changed"; preset_setting_id = preset->setting_id; - is_equation = false; + is_equation = false; } } else { assert(false); @@ -4272,7 +4121,7 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m return is_equation; } -Preset* PresetBundle::get_similar_printer_preset(std::string printer_model, std::string printer_variant) +Preset *PresetBundle::get_similar_printer_preset(std::string printer_model, std::string printer_variant) { if (printer_model.empty()) printer_model = printers.get_selected_preset().config.opt_string("printer_model"); @@ -4280,7 +4129,7 @@ Preset* PresetBundle::get_similar_printer_preset(std::string printer_model, std: return nullptr; auto printer_variant_old = printers.get_selected_preset().config.opt_string("printer_variant"); std::map printer_presets; - for (auto& preset : printers.m_presets) { + for (auto &preset : printers.m_presets) { if (printer_variant.empty() && !preset.is_system) continue; if (preset.config.opt_string("printer_model") == printer_model) @@ -4288,8 +4137,7 @@ Preset* PresetBundle::get_similar_printer_preset(std::string printer_model, std: } if (printer_presets.empty()) return nullptr; - auto prefer_printer = printers.get_selected_preset().alias; //.name ORCA use alias instead "name" for calling system presets. otherwise - // nozzle combo will not change printer presets if they custom named + auto prefer_printer = printers.get_selected_preset().alias; //.name ORCA use alias instead "name" for calling system presets. otherwise nozzle combo will not change printer presets if they custom named if (!printer_variant.empty()) boost::replace_all(prefer_printer, printer_variant_old, printer_variant); @@ -4307,29 +4155,30 @@ Preset* PresetBundle::get_similar_printer_preset(std::string printer_model, std: return printer_presets.begin()->second; } -// BBS: check whether this is the only edited filament +//BBS: check whether this is the only edited filament bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) { unsigned n = this->filament_presets.size(); if (filament_index >= n) return false; - std::string name = this->filament_presets[filament_index]; + std::string name = this->filament_presets[filament_index]; Preset& edited_preset = this->filaments.get_edited_preset(); if (edited_preset.name != name) return false; unsigned index = 0; - while (index < n) { + while (index < n) + { if (index == filament_index) { - index++; + index ++; continue; } std::string filament_preset = this->filament_presets[index]; if (edited_preset.name == filament_preset) return false; else - index++; + index ++; } return true; } @@ -4337,8 +4186,7 @@ bool PresetBundle::is_the_only_edited_filament(unsigned int filament_index) void PresetBundle::reset_default_nozzle_volume_type() { Preset& current_printer = this->printers.get_edited_preset(); - this->project_config.option("nozzle_volume_type")->values = - current_printer.config.option("default_nozzle_volume_type")->values; + this->project_config.option("nozzle_volume_type")->values = current_printer.config.option("default_nozzle_volume_type")->values; } int PresetBundle::get_printer_extruder_count() const @@ -4367,7 +4215,9 @@ void PresetBundle::update_filament_count() const size_t num_extruders = static_cast(get_printer_extruder_count()); if (filament_presets.size() >= num_extruders) return; - filament_presets.resize(num_extruders, filament_presets.empty() ? filaments.first_visible().name : filament_presets.back()); + filament_presets.resize(num_extruders, filament_presets.empty() + ? filaments.first_visible().name + : filament_presets.back()); } bool PresetBundle::support_different_extruders() const @@ -4386,7 +4236,8 @@ std::vector PresetBundle::get_default_nozzle_volume_types_for_filaments(std result.resize(filament_count, static_cast(NozzleVolumeType::nvtStandard)); auto opt_nozzle_volume_type = dynamic_cast(this->project_config.option("nozzle_volume_type")); - for (int index = 0; index < filament_count; index++) { + for (int index = 0; index < filament_count; index++) + { if (opt_nozzle_volume_type && opt_nozzle_volume_type->values.size() > (f_maps[index] - 1)) result[index] = opt_nozzle_volume_type->values[f_maps[index] - 1]; } @@ -4394,43 +4245,40 @@ std::vector PresetBundle::get_default_nozzle_volume_types_for_filaments(std return result; } -DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, - std::optional> filament_maps, - std::optional> filament_volume_maps) const +DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional>filament_maps, std::optional> filament_volume_maps) const { return (this->printers.get_edited_preset().printer_technology() == ptFFF) ? - this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) : - this->full_sla_config(); + this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) : + this->full_sla_config(); } -DynamicPrintConfig PresetBundle::full_config_secure(std::optional> filament_maps) const +DynamicPrintConfig PresetBundle::full_config_secure(std::optional>filament_maps) const { DynamicPrintConfig config = this->full_fff_config(false, filament_maps); - // FIXME legacy, the keys should not be there after conversion to a Physical Printer profile. + //FIXME legacy, the keys should not be there after conversion to a Physical Printer profile. config.erase("print_host"); config.erase("print_host_webui"); config.erase("printhost_apikey"); - config.erase("printhost_cafile"); - config.erase("printhost_user"); - config.erase("printhost_password"); + config.erase("printhost_cafile"); + config.erase("printhost_user"); + config.erase("printhost_password"); config.erase("printhost_port"); return config; } std::vector>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const { - auto full_config = this->full_config(); - int extruder_nums = full_config.option("nozzle_diameter")->values.size(); + auto full_config = this->full_config(); + int extruder_nums = full_config.option("nozzle_diameter")->values.size(); std::vector flush_volume_value = full_config.option("flush_volumes_matrix")->values; - int filament_nums = full_config.option("filament_type")->values.size(); + int filament_nums = full_config.option("filament_type")->values.size(); std::vector>> matrix; for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { - std::vector flush_matrix(cast(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums))); + std::vector flush_matrix(cast(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums))); std::vector> wipe_volumes; for (unsigned int i = 0; i < filament_nums; ++i) - wipe_volumes.push_back( - std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums)); matrix.emplace_back(wipe_volumes); } @@ -4438,9 +4286,9 @@ std::vector>> PresetBundle::get_full_flush_matrix if (with_multiplier) { // Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default // (or the key absent) reads flush_multiplier, so this is inert. - auto* mode_opt = project_config.option>("prime_volume_mode"); - const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast; - auto* mult_opt = project_config.option(use_fast ? "flush_multiplier_fast" : "flush_multiplier"); + auto* mode_opt = project_config.option>("prime_volume_mode"); + const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast; + auto* mult_opt = project_config.option(use_fast ? "flush_multiplier_fast" : "flush_multiplier"); auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option("flush_multiplier")->values; flush_multiplies.resize(extruder_nums, 1); for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) { @@ -4454,38 +4302,41 @@ std::vector>> PresetBundle::get_full_flush_matrix return matrix; } -const std::set ignore_settings_list = {"inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"}; +const std::set ignore_settings_list ={ + "inherits", + "print_settings_id", "filament_settings_id", "printer_settings_id" +}; -DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, - std::optional> filament_maps_new, - std::optional> filament_volume_maps_new) const +DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional> filament_maps_new, std::optional> filament_volume_maps_new) const { DynamicPrintConfig out; out.apply(FullPrintConfig::defaults()); out.apply(this->prints.get_edited_preset().config); // Add the default filament preset to have the "filament_preset_id" defined. - out.apply(this->filaments.default_preset().config); - out.apply(this->printers.get_edited_preset().config); + out.apply(this->filaments.default_preset().config); + out.apply(this->printers.get_edited_preset().config); out.apply(this->project_config); // BBS - size_t num_filaments = this->filament_presets.size(); + size_t num_filaments = this->filament_presets.size(); std::vector filament_maps = out.option("filament_map")->values; - std::vector filament_volume_maps(num_filaments, (int) nvtStandard); + std::vector filament_volume_maps(num_filaments, (int)nvtStandard); ConfigOptionInts* filament_volume_map_opt = out.option("filament_volume_map"); if (filament_maps_new.has_value()) filament_maps = *filament_maps_new; if (filament_volume_maps_new.has_value()) { - filament_volume_maps = *filament_volume_maps_new; + filament_volume_maps = *filament_volume_maps_new; out.option("filament_volume_map", true)->values = filament_volume_maps; - } else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) + } + else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments) filament_volume_maps = filament_volume_map_opt->values; - // in some middle state, they may be different + //in some middle state, they may be different if (filament_maps.size() != num_filaments) { filament_maps.resize(num_filaments, 1); - } else { + } + else { assert(filament_maps.size() == num_filaments); } if (filament_volume_maps.size() != num_filaments) { @@ -4499,35 +4350,32 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::vector inherits; std::vector filament_ids; std::vector print_compatible_printers; - // BBS: add logic for settings check between different system presets + //BBS: add logic for settings check between different system presets std::vector different_settings; std::string different_print_settings, different_printer_settings; compatible_printers_condition.emplace_back(this->prints.get_edited_preset().compatible_printers_condition()); - const ConfigOptionStrings* compatible_printers = - (const_cast(this))->prints.get_edited_preset().config.option("compatible_printers", false); + const ConfigOptionStrings* compatible_printers = (const_cast(this))->prints.get_edited_preset().config.option("compatible_printers", false); if (compatible_printers) print_compatible_printers = compatible_printers->values; - // BBS: add logic for settings check between different system presets + //BBS: add logic for settings check between different system presets std::string print_inherits = this->prints.get_edited_preset().inherits(); - inherits.emplace_back(print_inherits); - const Preset* print_parent_preset = this->prints.get_selected_preset_parent(); + inherits .emplace_back(print_inherits); + const Preset* print_parent_preset = this->prints.get_selected_preset_parent(); if (print_parent_preset) { - std::vector dirty_options = this->prints.dirty_options_without_option_list(&(this->prints.get_edited_preset()), - print_parent_preset, ignore_settings_list, - false); + std::vector dirty_options = this->prints.dirty_options_without_option_list(&(this->prints.get_edited_preset()), print_parent_preset, ignore_settings_list, false); if (!dirty_options.empty()) { different_print_settings = Slic3r::escape_strings_cstyle(dirty_options); } } different_settings.emplace_back(different_print_settings); - // BBS: update printer config related with variants + //BBS: update printer config related with variants std::vector> nozzle_volume_types; int extruder_count = 1, extruder_volume_type_count = 1; bool different_extruder = false; if (apply_extruder) { - different_extruder = out.support_different_extruders(extruder_count); + different_extruder = out.support_different_extruders(extruder_count); extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types); if ((extruder_count > 1) || different_extruder) { @@ -4539,40 +4387,33 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, // per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing // path composes variant_2 first and is unaffected; changing the order here would alter // long-standing composed values, so any fix must re-baseline them. - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); - // update print config related with variants - out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - print_options_with_variant, "print_extruder_id", "print_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant"); + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2); + //update print config related with variants + out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant"); } } if (num_filaments <= 1) { - // BBS: update filament config related with variants + //BBS: update filament config related with variants DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config; if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, - filament_options_with_variant, "", "filament_extruder_variant", 1, - filament_maps[0], (NozzleVolumeType) filament_volume_maps[0]); + filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]); out.apply(filament_config); compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition()); - compatible_prints_condition.emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); - // BBS: add logic for settings check between different system presets - // std::string filament_inherits = this->filaments.get_edited_preset().inherits(); + compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition()); + //BBS: add logic for settings check between different system presets + //std::string filament_inherits = this->filaments.get_edited_preset().inherits(); std::string current_preset_name = this->filament_presets[0]; - const Preset* preset = this->filaments.find_preset(current_preset_name, true); - std::string filament_inherits = preset->inherits(); - inherits.emplace_back(filament_inherits); + const Preset* preset = this->filaments.find_preset(current_preset_name, true); + std::string filament_inherits = preset->inherits(); + inherits .emplace_back(filament_inherits); filament_ids.emplace_back(this->filaments.get_edited_preset().filament_id); std::string different_filament_settings; - const Preset* filament_parent_preset = this->filaments.get_selected_preset_parent(); + const Preset* filament_parent_preset = this->filaments.get_selected_preset_parent(); if (filament_parent_preset) { - std::vector dirty_options = - this->filaments.dirty_options_without_option_list(&(this->filaments.get_edited_preset()), filament_parent_preset, - ignore_settings_list, false); + std::vector dirty_options = this->filaments.dirty_options_without_option_list(&(this->filaments.get_edited_preset()), filament_parent_preset, ignore_settings_list, false); if (!dirty_options.empty()) { different_filament_settings = Slic3r::escape_strings_cstyle(dirty_options); } @@ -4581,7 +4422,7 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, different_settings.emplace_back(different_filament_settings); std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); } else { // Retrieve filament presets and build a single config object for them. @@ -4600,16 +4441,16 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, filament_configs.emplace_back(&(preset->config)); } for (int index = 0; index < num_filaments; index++) { - const DynamicPrintConfig* cfg = filament_configs[index]; - const Preset* preset = filament_presets[index]; + const DynamicPrintConfig *cfg = filament_configs[index]; + const Preset *preset = filament_presets[index]; // The compatible_prints/printers_condition() returns a reference to configuration key, which may not yet exist. - DynamicPrintConfig& cfg_rw = *const_cast(cfg); + DynamicPrintConfig &cfg_rw = *const_cast(cfg); compatible_printers_condition.emplace_back(Preset::compatible_printers_condition(cfg_rw)); - compatible_prints_condition.emplace_back(Preset::compatible_prints_condition(cfg_rw)); + compatible_prints_condition .emplace_back(Preset::compatible_prints_condition(cfg_rw)); - // BBS: add logic for settings check between different system presets + //BBS: add logic for settings check between different system presets std::string filament_inherits = Preset::inherits(cfg_rw); - inherits.emplace_back(filament_inherits); + inherits .emplace_back(filament_inherits); filament_ids.emplace_back(preset->filament_id); std::string different_filament_settings; @@ -4617,13 +4458,15 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, if (preset->is_system || preset->is_default) { bool is_selected = this->filaments.get_selected_preset_name() == preset->name; if (is_selected) { - // use the real preset + //use the real preset filament_parent_preset = const_cast(this)->filaments.find_preset(preset->name, false, true); - } else { + } + else { filament_parent_preset = preset; } - } else if (!filament_inherits.empty()) - filament_parent_preset = const_cast(this)->filaments.find_preset(filament_inherits, false, true); + } + else if (!filament_inherits.empty()) + filament_parent_preset = const_cast(this)->filaments.find_preset(filament_inherits, false, true); if (filament_parent_preset) { std::vector dirty_options = cfg_rw.diff(filament_parent_preset->config); @@ -4632,7 +4475,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, while (iter != dirty_options.end()) { if (ignore_settings_list.find(*iter) != ignore_settings_list.end()) { iter = dirty_options.erase(iter); - } else { + } + else { ++iter; } } @@ -4648,22 +4492,19 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, for (size_t i = 0; i < num_filaments; ++i) { filament_temp_configs[i] = *(filament_configs[i]); if (apply_extruder && ((extruder_count > 1) || different_extruder)) - filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, - nozzle_volume_types, filament_options_with_variant, "", - "filament_extruder_variant", 1, filament_maps[i], - (NozzleVolumeType) filament_volume_maps[i]); + filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]); } // loop through options and apply them to the resulting config. std::vector filament_variant_count(num_filaments, 1); - for (const t_config_option_key& key : this->filaments.default_preset().config.keys()) { - if (key == "compatible_prints" || key == "compatible_printers") - continue; + for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; // Get a destination option. - ConfigOption* opt_dst = out.option(key, false); + ConfigOption *opt_dst = out.option(key, false); if (opt_dst->is_scalar()) { // Get an option, do not create if it does not exist. - const ConfigOption* opt_src = filament_temp_configs.front().option(key); + const ConfigOption *opt_src = filament_temp_configs.front().option(key); if (opt_src != nullptr) opt_dst->set(opt_src); } else { @@ -4676,10 +4517,10 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, for (size_t i = 0; i < filament_opts.size(); ++i) filament_opts[i] = filament_temp_configs[i].option(key); opt_vec_dst->set(filament_opts); - } else { + } + else { for (size_t i = 0; i < num_filaments; ++i) { - const ConfigOptionVectorBase* filament_option = static_cast( - filament_temp_configs[i].option(key)); + const ConfigOptionVectorBase* filament_option = static_cast(filament_temp_configs[i].option(key)); if (i == 0) opt_vec_dst->set(filament_option); else @@ -4694,9 +4535,9 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, } if (!apply_extruder) { - // append filament_self_index + //append filament_self_index std::vector& filament_self_indice = out.option("filament_self_index", true)->values; - int index_size = out.option("filament_extruder_variant")->size(); + int index_size = out.option("filament_extruder_variant")->size(); filament_self_indice.resize(index_size, 1); int k = 0; for (size_t i = 0; i < num_filaments; i++) { @@ -4707,15 +4548,13 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, } } - // BBS: add logic for settings check between different system presets + //BBS: add logic for settings check between different system presets std::string printer_inherits = this->printers.get_edited_preset().inherits(); // Don't store the "compatible_printers_condition" for the printer profile, there is none. - inherits.emplace_back(printer_inherits); - const Preset* printer_parent_preset = this->printers.get_selected_preset_parent(); + inherits .emplace_back(printer_inherits); + const Preset* printer_parent_preset = this->printers.get_selected_preset_parent(); if (printer_parent_preset) { - std::vector dirty_options = this->printers.dirty_options_without_option_list(&(this->printers.get_edited_preset()), - printer_parent_preset, - ignore_settings_list, false); + std::vector dirty_options = this->printers.dirty_options_without_option_list(&(this->printers.get_edited_preset()), printer_parent_preset, ignore_settings_list, false); if (!dirty_options.empty()) { different_printer_settings = Slic3r::escape_strings_cstyle(dirty_options); } @@ -4728,38 +4567,40 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, out.erase("compatible_printers"); out.erase("compatible_printers_condition"); out.erase("inherits"); - // BBS: add logic for settings check between different system presets + //BBS: add logic for settings check between different system presets out.erase("different_settings_to_system"); static const char* keys[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; - for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) { std::string key = std::string(keys[i]); - auto* opt = dynamic_cast(out.option(key, false)); + auto *opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); opt->value = boost::algorithm::clamp(opt->value, 0, int(num_filaments)); } - static const char* keys_with_default[] = {"outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", - "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id"}; - for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++i) { + static const char* keys_with_default[] = { + "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_filament_id", + "internal_solid_filament_id", "top_surface_filament_id", "bottom_surface_filament_id" + }; + for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) { std::string key = std::string(keys_with_default[i]); - auto* opt = dynamic_cast(out.option(key, false)); + auto *opt = dynamic_cast(out.option(key, false)); assert(opt != nullptr); - if (opt->value < 0 || opt->value > int(num_filaments)) + if(opt->value < 0 || opt->value > int(num_filaments)) opt->value = 0; } - out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); + out.option("print_settings_id", true)->value = this->prints.get_selected_preset_name(); out.option("filament_settings_id", true)->values = this->filament_presets; - out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); - out.option("filament_ids", true)->values = filament_ids; - out.option("filament_map", true)->values = filament_maps; + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + out.option("filament_ids", true)->values = filament_ids; + out.option("filament_map", true)->values = filament_maps; // Serialize the collected "compatible_printers_condition" and "inherits" fields. // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. // The vector will not be stored if all fields are empty strings. - auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { bool nonempty = false; - for (const std::string& v : values) - if (!v.empty()) { + for (const std::string &v : values) + if (! v.empty()) { nonempty = true; break; } @@ -4767,14 +4608,14 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); - add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); - add_if_some_non_empty(std::move(inherits), "inherits_group"); - // BBS: add logic for settings check between different system presets - add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); - add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); - out.option("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); + //BBS: add logic for settings check between different system presets + add_if_some_non_empty(std::move(different_settings), "different_settings_to_system"); + add_if_some_non_empty(std::move(print_compatible_printers), "print_compatible_printers"); + out.option("extruder_ams_count", true)->values = save_extruder_ams_count_to_string(this->extruder_ams_counts); - out.option("printer_technology", true)->value = ptFFF; + out.option("printer_technology", true)->value = ptFFF; return out; } @@ -4786,36 +4627,35 @@ DynamicPrintConfig PresetBundle::full_sla_config() const out.apply(this->sla_materials.get_edited_preset().config); out.apply(this->printers.get_edited_preset().config); // There are no project configuration values as of now, the project_config is reserved for FFF printers. - // out.apply(this->project_config); +// out.apply(this->project_config); - // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_prints, sla_materials, printers) into a - // single vector. + // Collect the "compatible_printers_condition" and "inherits" values over all presets (sla_prints, sla_materials, printers) into a single vector. std::vector compatible_printers_condition; - std::vector compatible_prints_condition; + std::vector compatible_prints_condition; std::vector inherits; compatible_printers_condition.emplace_back(this->sla_prints.get_edited_preset().compatible_printers_condition()); - inherits.emplace_back(this->sla_prints.get_edited_preset().inherits()); + inherits .emplace_back(this->sla_prints.get_edited_preset().inherits()); compatible_printers_condition.emplace_back(this->sla_materials.get_edited_preset().compatible_printers_condition()); - compatible_prints_condition.emplace_back(this->sla_materials.get_edited_preset().compatible_prints_condition()); - inherits.emplace_back(this->sla_materials.get_edited_preset().inherits()); - inherits.emplace_back(this->printers.get_edited_preset().inherits()); + compatible_prints_condition .emplace_back(this->sla_materials.get_edited_preset().compatible_prints_condition()); + inherits .emplace_back(this->sla_materials.get_edited_preset().inherits()); + inherits .emplace_back(this->printers.get_edited_preset().inherits()); // These two value types clash between the print and filament profiles. They should be renamed. out.erase("compatible_printers"); out.erase("compatible_printers_condition"); out.erase("inherits"); - out.option("sla_print_settings_id", true)->value = this->sla_prints.get_selected_preset_name(); - out.option("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset_name(); - out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); + out.option("sla_print_settings_id", true)->value = this->sla_prints.get_selected_preset_name(); + out.option("sla_material_settings_id", true)->value = this->sla_materials.get_selected_preset_name(); + out.option("printer_settings_id", true)->value = this->printers.get_selected_preset_name(); // Serialize the collected "compatible_printers_condition" and "inherits" fields. // There will be 1 + num_exturders fields for "inherits" and 2 + num_extruders for "compatible_printers_condition" stored. // The vector will not be stored if all fields are empty strings. - auto add_if_some_non_empty = [&out](std::vector&& values, const std::string& key) { + auto add_if_some_non_empty = [&out](std::vector &&values, const std::string &key) { bool nonempty = false; - for (const std::string& v : values) - if (!v.empty()) { + for (const std::string &v : values) + if (! v.empty()) { nonempty = true; break; } @@ -4823,49 +4663,48 @@ DynamicPrintConfig PresetBundle::full_sla_config() const out.set_key_value(key, new ConfigOptionStrings(std::move(values))); }; add_if_some_non_empty(std::move(compatible_printers_condition), "compatible_machine_expression_group"); - add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); - add_if_some_non_empty(std::move(inherits), "inherits_group"); + add_if_some_non_empty(std::move(compatible_prints_condition), "compatible_process_expression_group"); + add_if_some_non_empty(std::move(inherits), "inherits_group"); - out.option("printer_technology", true)->value = ptSLA; - return out; + out.option("printer_technology", true)->value = ptSLA; + return out; } // Load an external config file containing the print, filament and printer presets. // Instead of a config file, a G-code may be loaded containing the full set of parameters. // In the future the configuration will likely be read from an AMF file as well. // If the file is loaded successfully, its print / filament / printer profiles will be activated. -ConfigSubstitutions PresetBundle::load_config_file(const std::string& path, ForwardCompatibilitySubstitutionRule compatibility_rule) +ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule) { - if (is_gcode_file(path)) { - DynamicPrintConfig config; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(" enter, gcodefile %1%, compatibility_rule %2%") % path % compatibility_rule; - config.apply(FullPrintConfig::defaults()); + if (is_gcode_file(path)) { + DynamicPrintConfig config; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule; + config.apply(FullPrintConfig::defaults()); ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule); Preset::normalize(config); - load_config_file_config(path, true, std::move(config)); - return config_substitutions; - } + load_config_file_config(path, true, std::move(config)); + return config_substitutions; + } - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode") % path; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ; throw Slic3r::RuntimeError(std::string("Unknown configuration file: ") + path); - + return ConfigSubstitutions{}; } -// some filament presets split from one to sperate ones -// following map recording these filament presets -// for example: previously ''Bambu PLA Basic @BBL H2D 0.6 nozzle' was saved in ''Bambu PLA Basic @BBL H2D' with 0.4 -static std::map> filament_preset_convert = - {{"Bambu Lab H2D 0.6 nozzle", - {{"Bambu PLA Basic @BBL H2D", "Bambu PLA Basic @BBL H2D 0.6 nozzle"}, - {"Bambu PLA Matte @BBL H2D", "Bambu PLA Matte @BBL H2D 0.6 nozzle"}, - {"Bambu ABS @BBL H2D", "Bambu ABS @BBL H2D 0.6 nozzle"}}}, - {"Bambu Lab H2D 0.8 nozzle", - {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, - {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}}}; + +//some filament presets split from one to sperate ones +//following map recording these filament presets +//for example: previously ''Bambu PLA Basic @BBL H2D 0.6 nozzle' was saved in ''Bambu PLA Basic @BBL H2D' with 0.4 +static std::map> filament_preset_convert = { +{"Bambu Lab H2D 0.6 nozzle", {{"Bambu PLA Basic @BBL H2D", "Bambu PLA Basic @BBL H2D 0.6 nozzle"}, + {"Bambu PLA Matte @BBL H2D", "Bambu PLA Matte @BBL H2D 0.6 nozzle"}, + {"Bambu ABS @BBL H2D", "Bambu ABS @BBL H2D 0.6 nozzle"}}}, +{"Bambu Lab H2D 0.8 nozzle", {{"Bambu PETG HF @BBL H2D 0.6 nozzle", "Bambu PETG HF @BBL H2D 0.8 nozzle"}, + {"Bambu ASA @BBL H2D 0.6 nozzle", "Bambu ASA @BBL H2D 0.8 nozzle"}}} +}; // Relocate the per-slot cells of one mixed-filament project vector inside the imported // config, applying every authored-slot -> destination move at once. Each move reads its @@ -4910,26 +4749,24 @@ static void apply_mixed_config_relocations(DynamicPrintConfig& } } -// convert the old filament preset to new one after split + +//convert the old filament preset to new one after split static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name) { auto machine_iter = filament_preset_convert.find(machine_name); - if (machine_iter != filament_preset_convert.end()) { + if (machine_iter != filament_preset_convert.end()) + { std::map& filament_maps = machine_iter->second; - auto filament_iter = filament_maps.find(filament_name); - if (filament_iter != filament_maps.end()) { + auto filament_iter = filament_maps.find(filament_name); + if (filament_iter != filament_maps.end()) + { filament_name = filament_iter->second; } } } // Load a config file from a boost property_tree. This is a private method called from load_config_file. // is_external == false on if called from ConfigWizard -void PresetBundle::load_config_file_config(const std::string& name_or_path, - bool is_external, - DynamicPrintConfig&& config, - Semver file_version, - bool selected, - PublishedConfig* published_config) +void PresetBundle::load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version, bool selected, PublishedConfig *published_config) { PrinterTechnology printer_technology = Preset::printer_technology(config); @@ -4937,8 +4774,8 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // the author-selected published keys onto the edited presets. const bool is_published = published_config != nullptr && published_config->published; - auto clear_compatible_printers = [](DynamicPrintConfig& config) { - ConfigOption* opt_compatible = config.optptr("compatible_printers"); + auto clear_compatible_printers = [](DynamicPrintConfig& config){ + ConfigOption *opt_compatible = config.optptr("compatible_printers"); if (opt_compatible != nullptr) { assert(opt_compatible->type() == coStrings); if (opt_compatible->type() == coStrings) @@ -4960,49 +4797,44 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, #else // BBS: use filament_colour insteadof filament_settings_id, filament_settings_id sometimes is not generated ConfigOptionStrings* filament_colour_option = config.option("filament_colour"); - size_t num_filaments = filament_colour_option ? filament_colour_option->size() : 0; + size_t num_filaments = filament_colour_option?filament_colour_option->size():0; if (num_filaments == 0) throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); #endif - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(": , name_or_path %1%, is_external %2%, num_filaments %3%") % name_or_path % is_external % - num_filaments; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name_or_path %1%, is_external %2%, num_filaments %3%") % name_or_path % is_external % num_filaments; // Make a copy of the "compatible_machine_expression_group" and "inherits_group" vectors, which // accumulate values over all presets (print, filaments, printers). // These values will be distributed into their particular presets when loading. - std::vector compatible_printers_condition_values = std::move( - config.option("compatible_machine_expression_group", true)->values); - std::vector compatible_prints_condition_values = std::move( - config.option("compatible_process_expression_group", true)->values); - std::vector inherits_values = std::move(config.option("inherits_group", true)->values); - std::vector filament_ids = std::move(config.option("filament_ids", true)->values); - std::vector print_compatible_printers = std::move( - config.option("print_compatible_printers", true)->values); - // BBS: add different settings check logic - bool has_different_settings_to_system = config.option("different_settings_to_system") ? true : false; - std::vector different_values = std::move(config.option("different_settings_to_system", true)->values); - std::string& compatible_printers_condition = Preset::compatible_printers_condition(config); - std::string& compatible_prints_condition = Preset::compatible_prints_condition(config); - std::string& inherits = Preset::inherits(config); + std::vector compatible_printers_condition_values = std::move(config.option("compatible_machine_expression_group", true)->values); + std::vector compatible_prints_condition_values = std::move(config.option("compatible_process_expression_group", true)->values); + std::vector inherits_values = std::move(config.option("inherits_group", true)->values); + std::vector filament_ids = std::move(config.option("filament_ids", true)->values); + std::vector print_compatible_printers = std::move(config.option("print_compatible_printers", true)->values); + //BBS: add different settings check logic + bool has_different_settings_to_system = config.option("different_settings_to_system")?true:false; + std::vector different_values = std::move(config.option("different_settings_to_system", true)->values); + std::string &compatible_printers_condition = Preset::compatible_printers_condition(config); + std::string &compatible_prints_condition = Preset::compatible_prints_condition(config); + std::string &inherits = Preset::inherits(config); compatible_printers_condition_values.resize(num_filaments + 2, std::string()); compatible_prints_condition_values.resize(num_filaments, std::string()); inherits_values.resize(num_filaments + 2, std::string()); different_values.resize(num_filaments + 2, std::string()); filament_ids.resize(num_filaments, std::string()); // The "default_filament_profile" will be later extracted into the printer profile. - switch (printer_technology) { - case ptFFF: - config.option("default_print_profile", true); + switch (printer_technology) { + case ptFFF: + config.option("default_print_profile", true); config.option("default_filament_profile", true); - break; - case ptSLA: - config.option("default_sla_print_profile", true); - config.option("default_sla_material_profile", true); - break; + break; + case ptSLA: + config.option("default_sla_print_profile", true); + config.option("default_sla_material_profile", true); + break; default: break; - } + } bool process_multi_extruder = false; std::vector filament_variant_index; @@ -5022,15 +4854,13 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, filament_extruder_variant.resize(num_filaments, "Direct Drive Standard"); } if (config.option("extruder_variant_list")) { - // 3mf support multiple extruder logic - size_t extruder_count = config.option("nozzle_diameter")->values.size(); + //3mf support multiple extruder logic + size_t extruder_count = config.option("nozzle_diameter")->values.size(); extruder_variant_count = config.option("filament_extruder_variant", true)->size(); - if ((extruder_variant_count != filament_self_indice.size()) || (extruder_variant_count < num_filaments)) { + if ((extruder_variant_count != filament_self_indice.size()) + || (extruder_variant_count < num_filaments)) { assert(false); - BOOST_LOG_TRIVIAL(error) - << __FUNCTION__ - << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % - name_or_path; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path; throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path); } if (num_filaments != extruder_variant_count) { @@ -5048,12 +4878,13 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, } } } - // no need to parse extruder_ams_count + //no need to parse extruder_ams_count std::vector extruder_ams_count = std::move(config.option("extruder_ams_count", true)->values); config.erase("extruder_ams_count"); if (this->extruder_ams_counts.empty()) this->extruder_ams_counts = get_extruder_ams_count(extruder_ams_count); + // 1) Create a name from the file name. // Keep the suffix (.ini, .gcode, .amf, .3mf etc) to differentiate it from the normal profiles. std::string name = is_external ? boost::filesystem::path(name_or_path).filename().string() : name_or_path; @@ -5061,62 +4892,63 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // 2) If the loading succeeded, split and load the config into print / filament / printer settings. // First load the print and printer presets. - auto load_preset = [&config, &inherits, &inherits_values, &compatible_printers_condition, &compatible_printers_condition_values, - &compatible_prints_condition, &compatible_prints_condition_values, is_external, &name, &name_or_path, file_version, - selected](PresetCollection& presets, size_t idx, const std::string& key, - const std::set& different_keys, std::string filament_id) { - // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. - inherits = inherits_values[idx]; - compatible_printers_condition = compatible_printers_condition_values[idx]; + auto load_preset = + [&config, &inherits, &inherits_values, + &compatible_printers_condition, &compatible_printers_condition_values, + &compatible_prints_condition, &compatible_prints_condition_values, + is_external, &name, &name_or_path, file_version, selected] + (PresetCollection &presets, size_t idx, const std::string &key, const std::set &different_keys, std::string filament_id) { + // Split the "compatible_printers_condition" and "inherits" values one by one from a single vector to the print & printer profiles. + inherits = inherits_values[idx]; + compatible_printers_condition = compatible_printers_condition_values[idx]; if (idx > 0 && idx - 1 < compatible_prints_condition_values.size()) compatible_prints_condition = compatible_prints_condition_values[idx - 1]; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(": , name %1%, is_external %2%, inherits %3%") % name % is_external % inherits; - if (is_external) - presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config, different_keys, - PresetCollection::LoadAndSelect::Always, file_version, filament_id); - else + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": , name %1%, is_external %2%, inherits %3%")%name %is_external %inherits; + if (is_external) + presets.load_external_preset(name_or_path, name, config.opt_string(key, true), config, different_keys, PresetCollection::LoadAndSelect::Always, file_version, filament_id); + else presets.load_preset(presets.path_from_name(name, inherits.empty()), name, config, selected, file_version).save(nullptr); - }; + }; switch (Preset::printer_technology(config)) { - case ptFFF: { + case ptFFF: + { // A "published" 3MF project keeps the user's currently-selected presets, so the // print / printer / filament presets are NOT loaded from the file. Only the // project config values and the published keys are applied below. if (!is_published) { - // BBS: add different settings logic + //BBS: add different settings logic BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id"); std::vector print_different_keys_vector; std::string print_different_settings = different_values[0]; Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector); std::set print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end()); - // if (!has_different_settings_to_system) { - // print_different_keys_set.clear(); - // } - // else - print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + //if (!has_different_settings_to_system) { + // print_different_keys_set.clear(); + //} + //else + print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); if (!print_compatible_printers.empty()) { ConfigOptionStrings* compatible_printers = config.option("compatible_printers", true); - compatible_printers->values = print_compatible_printers; + compatible_printers->values = print_compatible_printers; } load_preset(this->prints, 0, "print_settings_id", print_different_keys_set, std::string()); - // clear compatible printers + //clear compatible printers clear_compatible_printers(config); std::vector printer_different_keys_vector; std::string printer_different_settings = different_values[num_filaments + 1]; Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector); std::set printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end()); - // if (!has_different_settings_to_system) { - // printer_different_keys_set.clear(); - // } - // else - printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); - // BBS: add config related logs + //if (!has_different_settings_to_system) { + // printer_different_keys_set.clear(); + //} + //else + printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id"); load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string()); @@ -5130,55 +4962,51 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // Split the "compatible_printers_condition" and "inherits" values from the cummulative vectors to separate filament presets. inherits = inherits_values[1]; compatible_printers_condition = compatible_printers_condition_values[1]; - compatible_prints_condition = compatible_prints_condition_values.front(); - Preset* loaded = nullptr; + compatible_prints_condition = compatible_prints_condition_values.front(); + Preset *loaded = nullptr; - // BBS: add different settings logic + //BBS: add different settings logic std::vector filament_different_keys_vector; std::string filament_different_settings = different_values[1]; Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), - filament_different_keys_vector.end()); - // if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - // } - // else - filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + //} + //else + filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); std::string filament_id = filament_ids[0]; - // BBS: add config related logs + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load single filament preset from filament_settings_id"); if (is_external) { if (inherits.empty()) convert_filament_preset_name(old_machine_profile_name->value, old_filament_profile_names->values.front()); else convert_filament_preset_name(old_machine_profile_name->value, inherits); - loaded = this->filaments - .load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config, - filament_different_keys_set, PresetCollection::LoadAndSelect::Always, file_version, - filament_id) - .first; - } else { - // called from Config Wizard. - loaded = &this->filaments.load_preset(this->filaments.path_from_name(name, inherits.empty()), name, config, true, - file_version); - loaded->save(nullptr); + loaded = this->filaments.load_external_preset(name_or_path, name, old_filament_profile_names->values.front(), config, filament_different_keys_set, PresetCollection::LoadAndSelect::Always, file_version, filament_id).first; } + else { + // called from Config Wizard. + loaded= &this->filaments.load_preset(this->filaments.path_from_name(name, inherits.empty()), name, config, true, file_version); + loaded->save(nullptr); + } this->filament_presets.clear(); - this->filament_presets.emplace_back(loaded->name); + this->filament_presets.emplace_back(loaded->name); } else { assert(is_external); // Split the filament presets, load each of them separately. std::vector configs(num_filaments, this->filaments.default_preset().config); // loop through options and scatter them into configs. - for (const t_config_option_key& key : this->filaments.default_preset().config.keys()) { - ConfigOption* other_opt = config.option(key); + for (const t_config_option_key &key : this->filaments.default_preset().config.keys()) { + ConfigOption *other_opt = config.option(key); if (other_opt == nullptr) continue; if (other_opt->is_scalar()) { - for (size_t i = 0; i < configs.size(); ++i) + for (size_t i = 0; i < configs.size(); ++ i) configs[i].option(key, false)->set(other_opt); - } else if (key != "compatible_printers" && key != "compatible_prints") { + } + else if (key != "compatible_printers" && key != "compatible_prints") { for (size_t i = 0; i < configs.size(); ++i) { if (process_multi_extruder && (filament_options_with_variant.find(key) != filament_options_with_variant.end())) { ConfigOptionVectorBase* other_opt_vec = static_cast(other_opt); @@ -5186,9 +5014,9 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, other_opt_vec->resize(extruder_variant_count); } size_t next_index = (i < (configs.size() - 1)) ? filament_variant_index[i + 1] : extruder_variant_count; - static_cast(configs[i].option(key, false)) - ->set(other_opt, filament_variant_index[i], next_index - filament_variant_index[i]); - } else + static_cast(configs[i].option(key, false))->set(other_opt, filament_variant_index[i], next_index - filament_variant_index[i]); + } + else static_cast(configs[i].option(key, false))->set_at(other_opt, 0, i); } } @@ -5199,26 +5027,25 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // in a case when next added preset take a place of previosly selected preset, // we should add presets from last to first bool any_modified = false; - // BBS: add config related logs + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load multiple filament preset from filament_settings_id"); - for (int i = (int) configs.size() - 1; i >= 0; i--) { - DynamicPrintConfig& cfg = configs[i]; + for (int i = (int)configs.size()-1; i >= 0; i--) { + DynamicPrintConfig &cfg = configs[i]; // Split the "compatible_printers_condition" and "inherits" from the cummulative vectors to separate filament presets. cfg.opt_string("compatible_printers_condition", true) = compatible_printers_condition_values[i + 1]; - cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; + cfg.opt_string("compatible_prints_condition", true) = compatible_prints_condition_values[i]; cfg.opt_string("inherits", true) = inherits_values[i + 1]; - // BBS: add different settings logic + //BBS: add different settings logic std::vector filament_different_keys_vector; - std::string filament_different_settings = different_values[i + 1]; + std::string filament_different_settings = different_values[i+1]; Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector); - std::set filament_different_keys_set(filament_different_keys_vector.begin(), - filament_different_keys_vector.end()); - // if (!has_different_settings_to_system) { - // filament_different_keys_set.clear(); - // } - // else - filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); + std::set filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end()); + //if (!has_different_settings_to_system) { + // filament_different_keys_set.clear(); + //} + //else + filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end()); std::string filament_id = filament_ids[i]; @@ -5229,15 +5056,16 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, else convert_filament_preset_name(old_machine_profile_name->value, filament_inherit); auto [loaded, modified] = this->filaments.load_external_preset(name_or_path, name, - (i < int(old_filament_profile_names->values.size())) ? - old_filament_profile_names->values[i] : - "", - std::move(cfg), filament_different_keys_set, - i == 0 ? PresetCollection::LoadAndSelect::Always : - any_modified ? - PresetCollection::LoadAndSelect::Never : - PresetCollection::LoadAndSelect::OnlyIfModified, - file_version, filament_id); + (i < int(old_filament_profile_names->values.size())) ? old_filament_profile_names->values[i] : "", + std::move(cfg), + filament_different_keys_set, + i == 0 ? + PresetCollection::LoadAndSelect::Always : + any_modified ? + PresetCollection::LoadAndSelect::Never : + PresetCollection::LoadAndSelect::OnlyIfModified, + file_version, + filament_id); any_modified |= modified; this->filament_presets[i] = loaded->name; } @@ -5250,17 +5078,19 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, break; } - case ptSLA: { + case ptSLA: + { /*std::set different_keys_set; load_preset(this->sla_prints, 0, "sla_print_settings_id", different_keys_set); load_preset(this->sla_materials, 1, "sla_material_settings_id", different_keys_set); load_preset(this->printers, 2, "printer_settings_id", different_keys_set);*/ break; } - default: break; + default: + break; } - this->update_compatible(PresetSelectCompatibleType::Never); + this->update_compatible(PresetSelectCompatibleType::Never); this->update_multi_material_filament_presets(); // A "published" 3MF project overlays the author-selected published keys onto the user's @@ -6369,23 +6199,23 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, this->filaments.select_preset_by_name(this->filament_presets.front(), false); } - // BBS - // const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; + + //BBS + //const std::string &physical_printer = config.option("physical_printer_settings_id", true)->value; const std::string physical_printer; if (!is_published) { if (this->printers.get_edited_preset().is_external || physical_printer.empty()) { this->physical_printers.unselect_printer(); } else { // Activate the physical printer profile if possible. - PhysicalPrinter* pp = this->physical_printers.find_printer(physical_printer, true); - if (pp != nullptr && std::find(pp->preset_names.begin(), pp->preset_names.end(), this->printers.get_edited_preset().name) != - pp->preset_names.end()) + PhysicalPrinter *pp = this->physical_printers.find_printer(physical_printer, true); + if (pp != nullptr && std::find(pp->preset_names.begin(), pp->preset_names.end(), this->printers.get_edited_preset().name) != pp->preset_names.end()) this->physical_printers.select_printer(pp->name, this->printers.get_edited_preset().name); else this->physical_printers.unselect_printer(); } } - // BBS: add config related logs + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": finished"); } @@ -6396,31 +6226,27 @@ void PresetBundle::load_config_file_config(const std::string& name_or_path, // (config_maps) or against base_bundle's filament library, flattens, validates // and registers the preset. Returns the reason loading failed, empty on // success. -std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, - const std::string& path, - const std::string& vendor_name, - const PresetBundle* base_bundle, - LoadConfigBundleAttributes flags, - ConfigSubstitutionContext& substitution_context, - PresetsConfigSubstitutions& substitutions, - std::map& config_maps, - std::map& filament_id_maps, - PresetCollection* presets_collection, - size_t& count, - bool is_from_lib, - const std::set* retain_configs) +std::string PresetBundle::load_vendor_preset( + const CachedPreset& entry, + const std::string& path, const std::string& vendor_name, + const PresetBundle* base_bundle, + LoadConfigBundleAttributes flags, + ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions, + std::map& config_maps, std::map& filament_id_maps, + PresetCollection* presets_collection, size_t& count, bool is_from_lib, + const std::set* retain_configs) { - const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); - const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; - const std::string& preset_name = entry.name; - std::string alias_name, filament_id = entry.filament_id; - std::vector renamed_from = entry.renamed_from; - DynamicPrintConfig config; + const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name); + const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path; + const std::string& preset_name = entry.name; + std::string alias_name, filament_id = entry.filament_id; + std::vector renamed_from = entry.renamed_from; + DynamicPrintConfig config; const DynamicPrintConfig* default_config = nullptr; - std::string reason; + std::string reason; - // check whether it inherits other preset or not - if (!entry.inherits.empty()) { + //check whether it inherits other preset or not + if (! entry.inherits.empty()) { auto it2 = config_maps.find(entry.inherits); if (it2 != config_maps.end()) default_config = &(it2->second); @@ -6442,14 +6268,16 @@ std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, } } } - } else { + } + else { ++m_errors; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find inherits " << entry.inherits << " for " << preset_name; // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); reason = "Can not find inherits: " + entry.inherits; return reason; } - } else { + } + else { if (presets_collection->type() == Preset::TYPE_PRINTER) default_config = &presets_collection->default_preset_for(entry.config_src).config; else @@ -6474,7 +6302,7 @@ std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, return reason; } if (config.has("alias")) - alias_name = (dynamic_cast(config.option("alias")))->value; + alias_name = (dynamic_cast(config.option("alias")))->value; Preset::normalize(config); // Report configuration fields, which are misplaced into a wrong group. @@ -6488,36 +6316,37 @@ std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, if (presets_collection->type() == Preset::TYPE_PRINTER) { // Filter out printer presets, which are not mentioned in the vendor profile. // These presets are considered not installed. - auto printer_model = config.opt_string("printer_model"); + auto printer_model = config.opt_string("printer_model"); if (printer_model.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" defines no printer model, it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer model, it will be ignored."; reason = std::string("can not find printer_model"); return reason; } auto printer_variant = config.opt_string("printer_variant"); if (printer_variant.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" defines no printer variant, it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines no printer variant, it will be ignored."; reason = std::string("can not find printer_variant"); return reason; } auto it_model = std::find_if(current_vendor_profile->models.cbegin(), current_vendor_profile->models.cend(), - [&](const VendorProfile::PrinterModel& m) { return m.id == printer_model; }); + [&](const VendorProfile::PrinterModel &m) { return m.id == printer_model; } + ); if (it_model == current_vendor_profile->models.end()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer model \"" << printer_model << "\", it will be ignored."; reason = std::string("can not find printer model in vendor profile"); return reason; } auto it_variant = it_model->variant(printer_variant); if (it_variant == nullptr) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" defines invalid printer variant \"" << printer_variant << "\", it will be ignored."; reason = std::string("can not find printer_variant in vendor profile"); return reason; } @@ -6530,74 +6359,71 @@ std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, // validated, not variant uniqueness. Validation-only so the app keeps loading existing // profiles unchanged. if (validation_mode && entry.instantiation == "true") { - const auto* nd = config.option("nozzle_diameter"); + const auto *nd = config.option("nozzle_diameter"); std::set nozzles, variant_nozzles; if (nd != nullptr) nozzles.insert(nd->values.begin(), nd->values.end()); std::vector variant_tokens; boost::algorithm::split(variant_tokens, printer_variant, boost::algorithm::is_any_of("+")); bool variant_ok = true; // printer_variant is already guaranteed non-empty above - for (const std::string& tok : variant_tokens) { + for (const std::string &tok : variant_tokens) { size_t consumed = 0; - double d = string_to_double_decimal_point(tok, &consumed); + double d = string_to_double_decimal_point(tok, &consumed); // Require a leading numeric diameter; a trailing suffix (e.g. "HF") is allowed. - if (consumed == 0) { - variant_ok = false; - break; - } + if (consumed == 0) { variant_ok = false; break; } variant_nozzles.insert(d); } if (!variant_ok || variant_nozzles != nozzles) { ++m_errors; - BOOST_LOG_TRIVIAL(error) - << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" has printer_variant \"" << printer_variant << "\" that does not match its nozzle_diameter \"" - << (nd ? nd->serialize() : std::string()) - << "\". " - "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " - "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " - "nozzle order (e.g. \"0.4+0.6\")."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has printer_variant \"" << printer_variant << + "\" that does not match its nozzle_diameter \"" << (nd ? nd->serialize() : std::string()) << "\". " + "printer_variant must begin with the nozzle diameter, optionally followed by a non-numeric suffix " + "(e.g. \"0.4\", \"0.8HF\"); for multi-nozzle printers, join the per-nozzle diameters with \"+\" in " + "nozzle order (e.g. \"0.4+0.6\")."; } } } - const Preset* preset_existing = presets_collection->find_preset(preset_name, false); + const Preset *preset_existing = presets_collection->find_preset(preset_name, false); if (preset_existing != nullptr) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << preset_name - << "\" has already been loaded from another Config Bundle."; + BOOST_LOG_TRIVIAL(error) << "Error in a Vendor Config Bundle \"" << path << "\": The printer preset \"" << + preset_name << "\" has already been loaded from another Config Bundle."; reason = std::string("duplicated defines"); return reason; } - auto file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred(); - if (validation_mode) + auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred(); + if(validation_mode) file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred(); // Load the preset into the list of presets, save it to disk. - Preset& loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); + Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false); if (flags.has(LoadConfigBundleAttribute::LoadSystem)) { - loaded.is_system = true; - loaded.vendor = current_vendor_profile; - loaded.version = current_vendor_profile->config_version; + loaded.is_system = true; + loaded.vendor = current_vendor_profile; + loaded.version = current_vendor_profile->config_version; loaded.description = entry.description; - loaded.setting_id = entry.setting_id; + loaded.setting_id = entry.setting_id; // Derive the preset setting_id on the fly when a profile ships without one, // matching scripts/assign_vendor_setting_ids.py. Only instantiated presets // carry an id; non-instantiated base profiles return earlier above. This never // touches the per-user cloud-sync setting_id written into user .info files. if (loaded.setting_id.empty() && entry.instantiation == "true") - loaded.setting_id = generate_preset_setting_id(vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); - loaded.filament_id = filament_id; + loaded.setting_id = generate_preset_setting_id( + vendor_name, Preset::get_type_string(presets_collection->type()), preset_name); + loaded.filament_id = filament_id; loaded.m_from_orca_filament_lib = is_from_lib; BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << " " << __LINE__ << ", " << loaded.name << " load filament_id: " << filament_id; if (presets_collection->type() == Preset::TYPE_FILAMENT) { if (filament_id.empty() && "Template" != vendor_name) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find filament_id for " << preset_name; - // throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": can not find filament_id for " << preset_name; + //throw ConfigurationError(format("can not find inherits %1% for %2%", inherits, preset_name)); reason = "Can not find filament_id for " + preset_name; return reason; - } else { + } + else { filament_id_maps.emplace(preset_name, filament_id); } } @@ -6621,34 +6447,31 @@ std::string PresetBundle::load_vendor_preset(const CachedPreset& entry, filaments.set_printer_hold_alias(loaded.alias, loaded); } loaded.renamed_from = std::move(renamed_from); - if (!substitution_context.empty()) - substitutions.push_back({preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, std::string(), - std::move(substitution_context.substitutions)}); + if (! substitution_context.empty()) + substitutions.push_back({ + preset_name, presets_collection->type(), PresetConfigSubstitutions::Source::ConfigBundle, + std::string(), std::move(substitution_context.substitutions) }); if (retain_configs == nullptr || retain_configs->count(preset_name) != 0) config_maps.emplace(preset_name, loaded.config); ++count; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%") % loaded.name % subfile; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", got preset %1%, from %2%")%loaded.name %subfile; return reason; } -// BBS: Load a config bundle file from json +//BBS: Load a config bundle file from json std::pair PresetBundle::load_vendor_configs_from_json( - const std::string& dir, - const std::string& vendor_name, - LoadConfigBundleAttributes flags, - ForwardCompatibilitySubstitutionRule compatibility_rule, - const PresetBundle* base_bundle) + const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) { // Enable substitutions for user config bundle, throw an exception when loading a system profile. - ConfigSubstitutionContext substitution_context{compatibility_rule}; + ConfigSubstitutionContext substitution_context { compatibility_rule }; PresetsConfigSubstitutions substitutions; // Errors already on this bundle when the load began; the cache stamp below // counts only what this parse adds. const int errors_at_entry = m_errors; - // BBS: add config related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%") % dir.c_str() % compatibility_rule; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, path %1%, compatibility_rule %2%")%dir.c_str()%compatibility_rule; if (flags.has(LoadConfigBundleAttribute::ResetUserProfile) || flags.has(LoadConfigBundleAttribute::LoadSystem)) // Reset this bundle, delete user profile files if SaveImported. this->reset(flags.has(LoadConfigBundleAttribute::SaveImported)); @@ -6656,15 +6479,13 @@ std::pair PresetBundle::load_vendor_configs_ // Orca: only a whole-vendor load has a cache — the vendor-only and filament-only // scans want a slice of one. Validation reads the JSONs whatever is cached. const boost::filesystem::path dir_path(dir); - const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && !flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); - if (cacheable && !validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { + const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly); + if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) { size_t presets_loaded = 0; - for (const PresetCollection* coll : - std::initializer_list{&this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, - &this->printers}) + for (const PresetCollection* coll : std::initializer_list{ + &this->prints, &this->sla_prints, &this->filaments, &this->sla_materials, &this->printers }) presets_loaded += coll->m_presets.size() - coll->m_num_default_presets; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(", %1% served from its preset cache, %2% presets") % vendor_name % presets_loaded; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", %1% served from its preset cache, %2% presets")%vendor_name%presets_loaded; return std::make_pair(std::move(substitutions), presets_loaded); } @@ -6688,9 +6509,10 @@ std::pair PresetBundle::load_vendor_configs_ } else if (boost::iequals(iter2.key(), BBL_JSON_KEY_SUB_PATH)) { subpath = iter2.value(); } - } else { + } + else { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid value type for " << iter2.key(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": invalid value type for " << iter2.key(); } } if (!name.empty() && !subpath.empty()) @@ -6712,51 +6534,55 @@ std::pair PresetBundle::load_vendor_configs_ boost::nowide::ifstream ifs(root_file); json j; ifs >> j; - // parse the json elements + //parse the json elements for (auto it = j.begin(); it != j.end(); it++) { if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { - // get version + //get version std::string version_str = it.value(); - auto config_version = Semver::parse(version_str); - if (!config_version) { + auto config_version = Semver::parse(version_str); + if (! config_version) { ++m_errors; - throw ConfigurationError( - (boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") % - vendor_name % version_str % dir) - .str()); + throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly") + % vendor_name % version_str % dir).str()); } else { vendor_profile.config_version = std::move(*config_version); } - } else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { - // get url + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + //get url vendor_profile.config_update_url = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { - // get description - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": parse " << root_file << ", got description: " << it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { - // get name + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) { + //get description + BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< ": parse "< PresetBundle::load_vendor_configs_ process_subfiles.clear(); } - // 2) paste the machine model - for (auto& machine_model : machine_model_subfiles) { + //2) paste the machine model + for (auto& machine_model : machine_model_subfiles) + { std::string subfile = dir + "/" + vendor_name + "/" + machine_model.second; VendorProfile::PrinterModel model; model.id = machine_model.first; @@ -6774,44 +6601,49 @@ std::pair PresetBundle::load_vendor_configs_ boost::nowide::ifstream ifs(subfile); json j; ifs >> j; - // parse the json elements + //parse the json elements for (auto it = j.begin(); it != j.end(); it++) { if (boost::iequals(it.key(), BBL_JSON_KEY_VERSION)) { - // get version - } else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { - // get url - } else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { - // get name + //get version + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) { + //get url + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) { + //get name model.name = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_MODEL_ID)) { - // get model_id + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_MODEL_ID)) { + //get model_id model.model_id = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_NOZZLE_DIAMETER)) { - // get nozzle diameter + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_NOZZLE_DIAMETER)) { + //get nozzle diameter std::string nozzle_diameters = it.value(); std::vector variants; if (Slic3r::unescape_strings_cstyle(nozzle_diameters, variants)) { - for (const std::string& variant_name : variants) { + for (const std::string &variant_name : variants) { if (model.variant(variant_name) == nullptr) model.variants.emplace_back(VendorProfile::PrinterVariant(variant_name)); } } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) - << __FUNCTION__ - << boost::format(": invalid nozzle_diameters %1% for Vendor %1%") % nozzle_diameters % vendor_name; + BOOST_LOG_TRIVIAL(error)<< __FUNCTION__ << boost::format(": invalid nozzle_diameters %1% for Vendor %1%") % nozzle_diameters % vendor_name; } - } else if (boost::iequals(it.key(), BBL_JSON_KEY_PRINTER_TECH)) { - // get printer tech + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_PRINTER_TECH)) { + //get printer tech if (boost::algorithm::starts_with(it.value(), "SL")) model.technology = ptSLA; else model.technology = ptFFF; - } else if (boost::iequals(it.key(), BBL_JSON_KEY_FAMILY)) { - // get family + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_FAMILY)) { + //get family model.family = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_MODEL)) { - // get bed model + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_MODEL)) { + //get bed model model.bed_model = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME)) { model.bottom_texture_end_name = it.value(); @@ -6823,26 +6655,28 @@ std::pair PresetBundle::load_vendor_configs_ model.bottom_texture_rect_longer = it.value(); } else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) { model.middle_texture_rect = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_IMAGE_BED_TYPE)) { + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_IMAGE_BED_TYPE)) { model.image_bed_type = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_TEXTURE)) { - // get bed texture + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_BED_TEXTURE)) { + //get bed texture model.bed_texture = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_HOTEND_MODEL)) { + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_HOTEND_MODEL)) { model.hotend_model = it.value(); - } else if (boost::iequals(it.key(), BBL_JSON_KEY_DEFAULT_MATERIALS)) { - // get machine list + } + else if (boost::iequals(it.key(), BBL_JSON_KEY_DEFAULT_MATERIALS)) { + //get machine list std::string default_materials_field = it.value(); if (Slic3r::unescape_strings_cstyle(default_materials_field, model.default_materials)) { - Slic3r::sort_remove_duplicates(model.default_materials); - if (!model.default_materials.empty() && model.default_materials.front().empty()) + Slic3r::sort_remove_duplicates(model.default_materials); + if (! model.default_materials.empty() && model.default_materials.front().empty()) // An empty material was inserted into the list of default materials. Remove it. model.default_materials.erase(model.default_materials.begin()); } else { ++m_errors; - BOOST_LOG_TRIVIAL(error) - << __FUNCTION__ - << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_field % vendor_name; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid default_materials %1% for Vendor %1%") % default_materials_field % vendor_name; } } else if (boost::iequals(it.key(), BBL_JSON_KEY_NOT_SUPPORT_BED_TYPE)) { // get machine list @@ -6854,67 +6688,65 @@ std::pair PresetBundle::load_vendor_configs_ model.not_support_bed_types.erase(model.not_support_bed_types.begin()); } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ - << boost::format(": invalid not_support_bed_types %1% for Vendor %1%") % - not_support_bed_type_field % vendor_name; + << boost::format(": invalid not_support_bed_types %1% for Vendor %1%") % not_support_bed_type_field % vendor_name; } } } - } catch (nlohmann::detail::parse_error& err) { - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << subfile - << " got a nlohmann::detail::parse_error, reason = " << err.what(); - throw ConfigurationError( - (boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") % subfile % - err.what() % dir) - .str()); + } + catch(nlohmann::detail::parse_error &err) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); + throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly") + %subfile %err.what() % dir).str()); } - if (!model.id.empty() && !model.variants.empty()) + if (! model.id.empty() && ! model.variants.empty()) vendor_profile.models.push_back(std::move(model)); } - // insert the vendor profile + //insert the vendor profile this->vendors.emplace(vendor_name, vendor_profile); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%") % vendor_profile.name % - vendor_profile.id % vendor_profile.config_version.to_string(); + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", loaded vendor profile, name %1%, id %2%, version %3%")%vendor_profile.name%vendor_profile.id%vendor_profile.config_version.to_string(); if (flags.has(LoadConfigBundleAttribute::LoadVendorOnly)) return std::make_pair(PresetsConfigSubstitutions{}, 0); // 3) paste the process/filament/print configs - PresetCollection* presets = nullptr; - size_t presets_loaded = 0; + PresetCollection *presets = nullptr; + size_t presets_loaded = 0; // Parse one subfile into a source-form entry — everything the JSON states, // nothing resolved. Loading the entry (load_vendor_preset) is the // same code whether the entry was parsed just now or deserialized from the // vendor's cache. - auto parse_subfile = [this, dir, vendor_name](ConfigSubstitutionContext& substitution_context, - const std::pair& subfile_iter, - CachedPreset& entry) -> std::string { + auto parse_subfile = [this, dir, vendor_name]( + ConfigSubstitutionContext& substitution_context, + const std::pair& subfile_iter, + CachedPreset& entry) -> std::string { + std::string subfile = dir + "/" + vendor_name + "/" + subfile_iter.second; std::string reason; try { std::map key_values; substitution_context.substitutions.clear(); - // parse the json elements + //parse the json elements entry.sub_path = subfile_iter.second; entry.config_src.load_from_json(subfile, substitution_context, false, key_values, reason); if (!reason.empty()) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": load config file " << subfile << " Failed!"; + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": load config file "< PresetBundle::load_vendor_configs_ << "\" contains invalid \"renamed_from\" key, which is being ignored."; } } - } catch (nlohmann::detail::parse_error& err) { + } + catch(nlohmann::detail::parse_error &err) { ++m_errors; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": parse " << subfile - << " got a nlohmann::detail::parse_error, reason = " << err.what(); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what(); reason = std::string("json parse error") + err.what(); return reason; } @@ -6966,8 +6798,8 @@ std::pair PresetBundle::load_vendor_configs_ // so the parse_errors stamped into the cache must hold only what a cache load // will not recount. int install_errors = 0; - auto load_subfiles = [&](std::vector>& subfiles, std::vector& entries, - const char* kind, bool is_from_lib = false) { + auto load_subfiles = [&](std::vector>& subfiles, + std::vector& entries, const char* kind, bool is_from_lib = false) { configs.clear(); filament_id_maps.clear(); for (auto& subfile : subfiles) { @@ -6975,19 +6807,17 @@ std::pair PresetBundle::load_vendor_configs_ std::string reason = parse_subfile(substitution_context, subfile, entry); if (reason.empty()) { const int errors_before_install = m_errors; - reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, substitution_context, substitutions, configs, - filament_id_maps, presets, presets_loaded, is_from_lib); + reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags, + substitution_context, substitutions, configs, filament_id_maps, presets, + presets_loaded, is_from_lib); install_errors += m_errors - errors_before_install; } if (!reason.empty()) { ++m_errors; - // parse error + //parse error std::string subfile_path = dir + "/" + vendor_name + "/" + subfile.second; - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ - << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; - throw ConfigurationError( - (boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir) - .str()); + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", got error when parse %1% setting from %2%") % kind % subfile_path; + throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % dir).str()); } if (will_cache) entries.emplace_back(std::move(entry)); @@ -6997,12 +6827,12 @@ std::pair PresetBundle::load_vendor_configs_ // The section order below — process, filaments (with the ORCA-lib map copy), // printers — is mirrored by load_vendor_cache's install loops; keep the two // in lockstep. - // 3.1) paste the process + //3.1) paste the process presets = &this->prints; load_subfiles(process_subfiles, cache_data.process_entries, "process"); - // 3.2) paste the filaments - presets = &this->filaments; + //3.2) paste the filaments + presets = &this->filaments; const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; load_subfiles(filament_subfiles, cache_data.filament_entries, "filament", is_orca_lib); if (is_orca_lib) { @@ -7010,7 +6840,7 @@ std::pair PresetBundle::load_vendor_configs_ m_filament_id_maps = filament_id_maps; } - // 3.3) paste the printers + //3.3) paste the printers presets = &this->printers; load_subfiles(machine_subfiles, cache_data.machine_entries, "printer"); @@ -7019,13 +6849,13 @@ std::pair PresetBundle::load_vendor_configs_ // wrapped would be added to every future load of this vendor. cache_data.parse_errors = uint64_t(std::max(0, m_errors - errors_at_entry - install_errors)); cache_data.vendors = this->vendors; - if (!VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, vendor_profile.config_version.to_string(), - cache_data)) + if (! VendorCacheFile::save((dir_path / (vendor_name + ".opc")).string(), vendor_name, + vendor_profile.config_version.to_string(), cache_data)) BOOST_LOG_TRIVIAL(warning) << "PresetBundle: failed to save vendor cache for " << vendor_name; } - // BBS: add config related logs - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%") % presets_loaded; + //BBS: add config related logs + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%")%presets_loaded; return std::make_pair(std::move(substitutions), presets_loaded); } @@ -7063,9 +6893,9 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // Now verify if flush_volumes_matrix has proper size (it is used to deduce number of extruders in wipe tower generator): std::vector old_matrix = this->project_config.option("flush_volumes_matrix")->values; - size_t old_nozzle_nums = this->project_config.option("flush_multiplier")->values.size(); + size_t old_nozzle_nums = this->project_config.option("flush_multiplier")->values.size(); size_t old_number_of_filaments = size_t(sqrt(old_matrix.size() / old_nozzle_nums) + EPSILON); - size_t nozzle_nums = get_printer_extruder_count(); + size_t nozzle_nums = get_printer_extruder_count(); if (old_nozzle_nums != nozzle_nums) { std::vector& f_multiplier = this->project_config.option("flush_multiplier")->values; f_multiplier.resize(nozzle_nums, 1.f); @@ -7074,11 +6904,11 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam if (old_matrix.size() != num_filaments * num_filaments * nozzle_nums) { // First verify if purging volumes presets for each extruder matches number of extruders std::vector& filaments = this->project_config.option("flush_volumes_vector")->values; - while (filaments.size() < 2 * num_filaments) { - filaments.push_back(filaments.size() > 1 ? filaments[0] : 140.); // copy the values from the first extruder - filaments.push_back(filaments.size() > 1 ? filaments[1] : 140.); + while (filaments.size() < 2* num_filaments) { + filaments.push_back(filaments.size()>1 ? filaments[0] : 140.); // copy the values from the first extruder + filaments.push_back(filaments.size()>1 ? filaments[1] : 140.); } - while (filaments.size() > 2 * num_filaments) { + while (filaments.size() > 2* num_filaments) { filaments.pop_back(); filaments.pop_back(); } @@ -7096,19 +6926,14 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // for this nozzle slot; otherwise initialize from the per-filament // flush volumes the same way the (i,j) out-of-range branch does. if (nozzle_id < old_nozzle_nums) { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = - old_matrix[old_i * old_number_of_filaments + old_j + old_matrix_size * nozzle_id]; + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = old_matrix[old_i * old_number_of_filaments + old_j + old_matrix_size * nozzle_id]; } else { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? - 0. : - filaments[2 * i] + filaments[2 * j + 1]); + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); } } } else { for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) { - new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? - 0. : - filaments[2 * i] + filaments[2 * j + 1]); + new_matrix[i * num_filaments + j + new_matrix_size * nozzle_id] = (i == j ? 0. : filaments[2 * i] + filaments[2 * j + 1]); } } } @@ -7121,15 +6946,15 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam // Preset::normalize_inherits: target.find_preset(name, false) resolves "renamed_from" recursively // and returns nullptr for unknown names, so we rewrite only on a positive, changed match and leave // user/deleted names untouched. -static void normalize_compatible_field(Preset& preset, const char* field_key, PresetCollection& target) +static void normalize_compatible_field(Preset &preset, const char *field_key, PresetCollection &target) { - auto* opt = preset.config.option(field_key); + auto *opt = preset.config.option(field_key); if (opt == nullptr) return; - for (std::string& name : opt->values) { + for (std::string &name : opt->values) { if (name.empty()) continue; - if (const Preset* resolved = target.find_preset(name, false); resolved != nullptr && resolved->name != name) + if (const Preset *resolved = target.find_preset(name, false); resolved != nullptr && resolved->name != name) name = resolved->name; } } @@ -7146,59 +6971,56 @@ void PresetBundle::normalize_compatible_presets() // references a process preset. System presets are normalized too: a vendor profile can itself // reference a sibling preset by a name that was later renamed, and the rewrite is in-memory only // (system presets are never persisted back to vendor JSON). (begin()/end() skip defaults.) - auto normalize = [this](PresetCollection& holders, PresetCollection* processes) { - for (Preset& p : holders) { + auto normalize = [this](PresetCollection &holders, PresetCollection *processes) { + for (Preset &p : holders) { normalize_compatible_field(p, "compatible_printers", this->printers); if (processes != nullptr) normalize_compatible_field(p, "compatible_prints", *processes); } }; - normalize(this->prints, nullptr); - normalize(this->filaments, &this->prints); - normalize(this->sla_prints, nullptr); + normalize(this->prints, nullptr); + normalize(this->filaments, &this->prints); + normalize(this->sla_prints, nullptr); normalize(this->sla_materials, &this->sla_prints); } -void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, - PresetSelectCompatibleType select_other_filament_if_incompatible) +void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) { - const Preset& printer_preset = this->printers.get_edited_preset(); - const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); + const Preset &printer_preset = this->printers.get_edited_preset(); + const PresetWithVendorProfile printer_preset_with_vendor_profile = this->printers.get_preset_with_vendor_profile(printer_preset); class PreferedProfileMatch { public: - PreferedProfileMatch(const std::string& prefered_alias, const std::string& prefered_name) - : m_prefered_alias(prefered_alias), m_prefered_name(prefered_name) - {} + PreferedProfileMatch(const std::string &prefered_alias, const std::string &prefered_name) : + m_prefered_alias(prefered_alias), m_prefered_name(prefered_name) {} - int operator()(const Preset& preset) const + int operator()(const Preset &preset) const { - return preset.is_default || preset.is_external ? - // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. - 0 : - !m_prefered_alias.empty() && m_prefered_alias == preset.alias ? - // Matching an alias, always take this preset with priority. - std::numeric_limits::max() : - // Otherwise take the prefered profile, or the first compatible. - preset.name == m_prefered_name; + return + preset.is_default || preset.is_external ? + // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. + 0 : + ! m_prefered_alias.empty() && m_prefered_alias == preset.alias ? + // Matching an alias, always take this preset with priority. + std::numeric_limits::max() : + // Otherwise take the prefered profile, or the first compatible. + preset.name == m_prefered_name; } private: - const std::string m_prefered_alias; - const std::string& m_prefered_name; + const std::string m_prefered_alias; + const std::string &m_prefered_name; }; // Matching by the layer height in addition. class PreferedPrintProfileMatch : public PreferedProfileMatch { public: - PreferedPrintProfileMatch(const Preset* preset, const std::string& prefered_name) - : PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name) - , m_prefered_layer_height(preset ? preset->config.opt_float("layer_height") : 0) - {} + PreferedPrintProfileMatch(const Preset *preset, const std::string &prefered_name) : + PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), m_prefered_layer_height(preset ? preset->config.opt_float("layer_height") : 0) {} - int operator()(const Preset& preset) const + int operator()(const Preset &preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external) @@ -7222,12 +7044,11 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri class PreferedFilamentProfileMatch : public PreferedProfileMatch { public: - PreferedFilamentProfileMatch(const Preset* preset, const std::string& prefered_name) - : PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name) - , m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string()) - {} + PreferedFilamentProfileMatch(const Preset *preset, const std::string &prefered_name) : + PreferedProfileMatch(preset ? preset->alias : std::string(), prefered_name), + m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string()) {} - int operator()(const Preset& preset) const + int operator()(const Preset &preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external) @@ -7235,9 +7056,9 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri int match_quality = PreferedProfileMatch::operator()(preset); if (match_quality < std::numeric_limits::max()) { match_quality += 1; - if (preset.is_visible) + if(preset.is_visible) match_quality += 1; - if (!m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) match_quality *= 10; } return match_quality; @@ -7251,141 +7072,118 @@ void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_pri class PreferedFilamentsProfileMatch { public: - PreferedFilamentsProfileMatch(const Preset* preset, const std::vector& prefered_names) - : m_prefered_alias(preset ? preset->alias : std::string()) - , m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string("PLA")) - , // BBS: default choose PLA + PreferedFilamentsProfileMatch(const Preset *preset, const std::vector &prefered_names) : + m_prefered_alias(preset ? preset->alias : std::string()), + m_prefered_filament_type(preset ? preset->config.opt_string("filament_type", 0) : std::string("PLA")), // BBS: default choose PLA m_prefered_names(prefered_names) - {} + {} - int operator()(const Preset& preset) const + int operator()(const Preset &preset) const { // Don't match any properties of the "-- default --" profile or the external profiles when switching printer profile. if (preset.is_default || preset.is_external || !preset.is_visible) return 0; - if (!m_prefered_alias.empty() && m_prefered_alias == preset.alias) + if (! m_prefered_alias.empty() && m_prefered_alias == preset.alias) // Matching an alias, always take this preset with priority. return std::numeric_limits::max(); int match_quality = (std::find(m_prefered_names.begin(), m_prefered_names.end(), preset.name) != m_prefered_names.end()) + 1; - if (!m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) + if (! m_prefered_filament_type.empty() && m_prefered_filament_type == preset.config.opt_string("filament_type", 0)) match_quality *= 10; return match_quality; } private: - const std::string m_prefered_alias; - const std::string m_prefered_filament_type; - const std::vector& m_prefered_names; + const std::string m_prefered_alias; + const std::string m_prefered_filament_type; + const std::vector &m_prefered_names; }; - BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets enter, select_other_print_if_incompatible %1%, " - "select_other_filament_if_incompatible %2%") % - (int) select_other_print_if_incompatible % (int) select_other_filament_if_incompatible; - switch (printer_preset.printer_technology()) { - case ptFFF: { - assert(printer_preset.config.has("default_print_profile")); - assert(printer_preset.config.has("default_filament_profile")); - const std::vector& prefered_filament_profiles = - printer_preset.config.option("default_filament_profile")->values; + BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets enter, select_other_print_if_incompatible %1%, select_other_filament_if_incompatible %2%")%(int)select_other_print_if_incompatible %(int)select_other_filament_if_incompatible; + switch (printer_preset.printer_technology()) { + case ptFFF: + { + assert(printer_preset.config.has("default_print_profile")); + assert(printer_preset.config.has("default_filament_profile")); + const std::vector &prefered_filament_profiles = printer_preset.config.option("default_filament_profile")->values; this->prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, - PreferedPrintProfileMatch(this->prints.get_selected_idx() == size_t(-1) ? - nullptr : - &this->prints.get_edited_preset(), - printer_preset.config.opt_string("default_print_profile"))); - const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); + PreferedPrintProfileMatch(this->prints.get_selected_idx() == size_t(-1) ? nullptr : &this->prints.get_edited_preset(), printer_preset.config.opt_string("default_print_profile"))); + const PresetWithVendorProfile print_preset_with_vendor_profile = this->prints.get_edited_preset_with_vendor_profile(); // Remember whether the filament profiles were compatible before updating the filament compatibility. - std::vector filament_preset_was_compatible(this->filament_presets.size(), false); - for (size_t idx = 0; idx < this->filament_presets.size(); ++idx) { - Preset* preset = this->filaments.find_preset(this->filament_presets[idx], false); + std::vector filament_preset_was_compatible(this->filament_presets.size(), false); + for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { + Preset *preset = this->filaments.find_preset(this->filament_presets[idx], false); filament_preset_was_compatible[idx] = preset != nullptr && preset->is_compatible; } // First select a first compatible profile for the preset editor. - BOOST_LOG_TRIVIAL(info) << boost::format("prefered filaments: size %1%, previous selected %2%") % - prefered_filament_profiles.size() % this->filaments.get_selected_idx(); - if (this->filaments.get_selected_idx() != size_t(-1)) { + BOOST_LOG_TRIVIAL(info) << boost::format("prefered filaments: size %1%, previous selected %2%") %prefered_filament_profiles.size() % this->filaments.get_selected_idx(); + if (this->filaments.get_selected_idx() != size_t(-1)) + { BOOST_LOG_TRIVIAL(info) << boost::format("previous selected filament: %1%") % this->filaments.get_edited_preset().name; } for (size_t idx = 0; idx < prefered_filament_profiles.size(); ++idx) { BOOST_LOG_TRIVIAL(info) << boost::format("prefered filament: %1%") % prefered_filament_profiles[idx]; } - this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, - select_other_filament_if_incompatible, - PreferedFilamentsProfileMatch(this->filaments.get_selected_idx() == size_t(-1) ? - nullptr : - &this->filaments.get_edited_preset(), - prefered_filament_profiles)); + this->filaments.update_compatible(printer_preset_with_vendor_profile, &print_preset_with_vendor_profile, select_other_filament_if_incompatible, + PreferedFilamentsProfileMatch(this->filaments.get_selected_idx() == size_t(-1) ? nullptr : &this->filaments.get_edited_preset(), prefered_filament_profiles)); if (select_other_filament_if_incompatible != PresetSelectCompatibleType::Never) { // Verify validity of the current filament presets. - const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : - prefered_filament_profiles.front(); + const std::string prefered_filament_profile = prefered_filament_profiles.empty() ? std::string() : prefered_filament_profiles.front(); if (this->filament_presets.size() == 1) { // The compatible profile should have been already selected for the preset editor. Just use it. - if (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible.front()) - this->filament_presets.front() = this->filaments.get_edited_preset().name; + if (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible.front()) + this->filament_presets.front() = this->filaments.get_edited_preset().name; } else { - for (size_t idx = 0; idx < this->filament_presets.size(); ++idx) { - std::string& filament_name = this->filament_presets[idx]; - Preset* preset = this->filaments.find_preset(filament_name, false); - if (preset == nullptr || - (!preset->is_compatible && (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || - filament_preset_was_compatible[idx]))) + for (size_t idx = 0; idx < this->filament_presets.size(); ++ idx) { + std::string &filament_name = this->filament_presets[idx]; + Preset *preset = this->filaments.find_preset(filament_name, false); + if (preset == nullptr || (! preset->is_compatible && (select_other_filament_if_incompatible == PresetSelectCompatibleType::Always || filament_preset_was_compatible[idx]))) // Pick a compatible profile. If there are prefered_filament_profiles, use them. - filament_name = this->filaments - .first_compatible( - PreferedFilamentProfileMatch(preset, (idx < prefered_filament_profiles.size()) ? - prefered_filament_profiles[idx] : - prefered_filament_profile)) - .name; + filament_name = this->filaments.first_compatible( + PreferedFilamentProfileMatch(preset, + (idx < prefered_filament_profiles.size()) ? prefered_filament_profiles[idx] : prefered_filament_profile)).name; } } } - break; + break; } - case ptSLA: { - assert(printer_preset.config.has("default_sla_print_profile")); - assert(printer_preset.config.has("default_sla_material_profile")); - this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, - PreferedPrintProfileMatch(this->sla_prints.get_selected_idx() == size_t(-1) ? - nullptr : - &this->sla_prints.get_edited_preset(), - printer_preset.config.opt_string("default_sla_print_profile"))); + case ptSLA: + { + assert(printer_preset.config.has("default_sla_print_profile")); + assert(printer_preset.config.has("default_sla_material_profile")); + this->sla_prints.update_compatible(printer_preset_with_vendor_profile, nullptr, select_other_print_if_incompatible, + PreferedPrintProfileMatch(this->sla_prints.get_selected_idx() == size_t(-1) ? nullptr : &this->sla_prints.get_edited_preset(), printer_preset.config.opt_string("default_sla_print_profile"))); const PresetWithVendorProfile sla_print_preset_with_vendor_profile = this->sla_prints.get_edited_preset_with_vendor_profile(); - this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, - select_other_filament_if_incompatible, - PreferedProfileMatch(this->sla_materials.get_selected_idx() == size_t(-1) ? - std::string() : - this->sla_materials.get_edited_preset().alias, - printer_preset.config.opt_string("default_sla_material_profile"))); - break; - } + this->sla_materials.update_compatible(printer_preset_with_vendor_profile, &sla_print_preset_with_vendor_profile, select_other_filament_if_incompatible, + PreferedProfileMatch(this->sla_materials.get_selected_idx() == size_t(-1) ? std::string() : this->sla_materials.get_edited_preset().alias, printer_preset.config.opt_string("default_sla_material_profile"))); + break; + } default: break; } BOOST_LOG_TRIVIAL(info) << boost::format("update_compatibility for all presets exit"); } -std::vector PresetBundle::export_current_configs(const std::string& path, - std::function override_confirm, - bool include_modify, - bool export_system_settings) + +std::vector PresetBundle::export_current_configs(const std::string & path, + std::function override_confirm, + bool include_modify, + bool export_system_settings) { - const Preset& print_preset = include_modify ? prints.get_edited_preset() : prints.get_selected_preset(); - const Preset& printer_preset = include_modify ? printers.get_edited_preset() : printers.get_selected_preset(); - std::set presets{&print_preset, &printer_preset}; - for (auto& f : filament_presets) { + const Preset &print_preset = include_modify ? prints.get_edited_preset() : prints.get_selected_preset(); + const Preset &printer_preset = include_modify ? printers.get_edited_preset() : printers.get_selected_preset(); + std::set presets { &print_preset, &printer_preset }; + for (auto &f : filament_presets) { auto filament_preset = filaments.find_preset(f, include_modify); - if (filament_preset) - presets.insert(filament_preset); + if (filament_preset) presets.insert(filament_preset); } int overwrite = 0; std::vector result; for (auto preset : presets) { - if ((preset->is_system && !export_system_settings) || preset->is_default) + if ((preset->is_system && !export_system_settings) || preset->is_default) continue; std::string file = path + "/" + preset->name + ".json"; - if (overwrite == 0) - overwrite = 1; + if (overwrite == 0) overwrite = 1; if (boost::filesystem::exists(file) && overwrite < 2) { overwrite = override_confirm(preset->name); if (overwrite == 0 || overwrite == 2) @@ -7399,7 +7197,7 @@ std::vector PresetBundle::export_current_configs(const std::string& // Set the filament preset name. As the name could come from the UI selection box, // an optional "(modified)" suffix will be removed from the filament name. -void PresetBundle::set_filament_preset(size_t idx, const std::string& name) +void PresetBundle::set_filament_preset(size_t idx, const std::string &name) { if (idx >= filament_presets.size()) { BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: set_filament_preset out of range %1% - %2%") % idx % filament_presets.size(); @@ -7428,7 +7226,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const if (!preset.is_system) continue; // It's per design that the Orca Filament Library can have the empty compatible_printers. - if (preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) + if(preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY) continue; auto* compatible_printers = dynamic_cast(preset.config.option("compatible_printers")); if (compatible_printers == nullptr || compatible_printers->values.empty()) { @@ -7451,7 +7249,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const // VS Code integrated terminal (Cmd/Ctrl+click) and macOS Terminal.app // (Cmd+double-click); quotes or literal spaces break link detection in both, so // the characters that would terminate the URI token are percent-encoded. -static std::string preset_file_uri(const std::string& file) +static std::string preset_file_uri(const std::string &file) { std::string path; try { @@ -7467,7 +7265,7 @@ static std::string preset_file_uri(const std::string& file) case ' ': uri += "%20"; break; case '#': uri += "%23"; break; case '%': uri += "%25"; break; - default: uri += c; + default: uri += c; } } return uri; @@ -7489,34 +7287,34 @@ bool PresetBundle::check_preset_references() const // Resolve one reference (an inherits parent or a compatible_* entry) against its target // collection and log if it is dangling (unknown) or uses a renamed preset's old name. - auto report_ref = [&](const Preset& p, const std::string& name, const PresetCollection& target, const char* verb, const char* noun) { - const Preset* resolved = target.find_preset(name, false); + auto report_ref = [&](const Preset &p, const std::string &name, const PresetCollection &target, + const char *verb, const char *noun) { + const Preset *resolved = target.find_preset(name, false); if (resolved == nullptr) { found = true; BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " unknown " << noun << " \"" << name << "\":\n" << preset_file_uri(p.file); } else if (resolved->name != name) { found = true; - BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " renamed " << noun << " \"" << name << "\" (now \"" - << resolved->name << "\"):\n" - << preset_file_uri(p.file); + BOOST_LOG_TRIVIAL(error) << "Preset \"" << p.name << "\" " << verb << " renamed " << noun << " \"" << name + << "\" (now \"" << resolved->name << "\"):\n" << preset_file_uri(p.file); } }; - auto check_list = [&](const Preset& p, const char* key, const PresetCollection& target) { - const auto* opt = p.config.option(key); + auto check_list = [&](const Preset &p, const char *key, const PresetCollection &target) { + const auto *opt = p.config.option(key); if (opt == nullptr) return; - for (const std::string& name : opt->values) + for (const std::string &name : opt->values) if (!name.empty()) report_ref(p, name, target, "references", key); }; - auto check_collection = [&](const PresetCollection& holders, const PresetCollection* processes) { - for (const Preset& p : holders) { + auto check_collection = [&](const PresetCollection &holders, const PresetCollection *processes) { + for (const Preset &p : holders) { if (!p.is_system) continue; - if (const std::string& inh = p.inherits(); !inh.empty()) + if (const std::string &inh = p.inherits(); !inh.empty()) report_ref(p, inh, holders, "inherits", "parent"); check_list(p, "compatible_printers", this->printers); if (processes != nullptr) @@ -7526,10 +7324,10 @@ bool PresetBundle::check_preset_references() const // Printers carry no compatible_printers/compatible_prints (those name a printer, so a printer // holding them makes no sense); check_list is a no-op for them, so only their inherits is checked. - check_collection(this->printers, nullptr); - check_collection(this->prints, nullptr); - check_collection(this->filaments, &this->prints); - check_collection(this->sla_prints, nullptr); + check_collection(this->printers, nullptr); + check_collection(this->prints, nullptr); + check_collection(this->filaments, &this->prints); + check_collection(this->sla_prints, nullptr); check_collection(this->sla_materials, &this->sla_prints); return found; @@ -7547,15 +7345,15 @@ bool PresetBundle::check_duplicate_filament_subtypes() const // printer against its own vendor's filaments. A vendor's compatible_printers // only names that vendor's printers, so same-vendor scoping is correctness // preserving and avoids an O(all printers x all filaments) sweep. - std::map> filaments_by_vendor; - for (const auto& preset : filaments) { + std::map> filaments_by_vendor; + for (const auto &preset : filaments) { if (!preset.is_system || preset.filament_id.empty() || preset.vendor == nullptr) continue; filaments_by_vendor[preset.vendor->name].push_back(&preset); } bool found_duplicates = false; - for (const auto& printer : printers) { + for (const auto &printer : printers) { if (!printer.is_system || printer.vendor == nullptr) continue; auto vendor_it = filaments_by_vendor.find(printer.vendor->name); @@ -7564,39 +7362,40 @@ bool PresetBundle::check_duplicate_filament_subtypes() const const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer); // std::map keeps the reported errors in a deterministic (sorted) order. - std::map> by_filament_id; - for (const Preset* fil : vendor_it->second) + std::map> by_filament_id; + for (const Preset *fil : vendor_it->second) if (is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*fil), active_printer)) by_filament_id[fil->filament_id].push_back(fil); - for (const auto& entry : by_filament_id) { + for (const auto &entry : by_filament_id) { if (entry.second.size() < 2) continue; found_duplicates = true; // List each conflicting preset with a clickable file:// URI on its own // line, so the profile author can jump straight to the files to fix. std::string presets; - for (const Preset* p : entry.second) + for (const Preset *p : entry.second) presets += "\n - " + p->name + "\n " + preset_file_uri(p->file); - BOOST_LOG_TRIVIAL(error) << "Ambiguous AMS filament match: " << entry.second.size() << " filament presets share filament_id \"" - << entry.first << "\" and are all compatible with printer \"" << printer.name - << "\". When matching an AMS spool the slicer cannot tell them apart and" - " silently picks whichever loads first." - << presets; + BOOST_LOG_TRIVIAL(error) + << "Ambiguous AMS filament match: " << entry.second.size() + << " filament presets share filament_id \"" << entry.first + << "\" and are all compatible with printer \"" << printer.name + << "\". When matching an AMS spool the slicer cannot tell them apart and" + " silently picks whichever loads first." << presets; } } // Print the troubleshooting guidance once, not per error, to keep the log readable. if (found_duplicates) BOOST_LOG_TRIVIAL(error) << "\n========================================\n" - << "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament" - " preset with a given filament_id is compatible with each printer. Either" - "\n (a) remove the overlapping printer from a preset's \"compatible_printers\"" - " list (e.g. a '@printer' preset over-claiming a nozzle that already has its own" - " '@printer 0.x nozzle' preset), or" - "\n (b) if these are genuinely different materials, give each its own" - " \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another" - " material's @base preset."; + << "How to fix \"Ambiguous AMS filament match\" errors: make sure only ONE filament" + " preset with a given filament_id is compatible with each printer. Either" + "\n (a) remove the overlapping printer from a preset's \"compatible_printers\"" + " list (e.g. a '@printer' preset over-claiming a nozzle that already has its own" + " '@printer 0.x nozzle' preset), or" + "\n (b) if these are genuinely different materials, give each its own" + " \"filament_id\" - a common cause is a wrong \"inherits\" pointing at another" + " material's @base preset."; return found_duplicates; } @@ -7612,39 +7411,30 @@ bool BundleMetadata::load_from_json(const std::string& path) json j; ifs >> j; - if (j.contains("id")) - this->id = j["id"].get(); + if (j.contains("id")) this->id = j["id"].get(); - if (j.contains("name")) - this->name = j["name"].get(); - else if (j.contains("bundle_id")) - this->name = j["bundle_id"].get(); // backwards compat w bundle_structure.json + if (j.contains("name")) this->name = j["name"].get(); + else if (j.contains("bundle_id")) this->name = j["bundle_id"].get(); // backwards compat w bundle_structure.json - if (j.contains("version")) - this->version = j["version"].get(); + if (j.contains("version")) this->version = j["version"].get(); - if (j.contains("description")) - this->description = j["description"].get(); - else if (j.contains("bundle_type")) - this->description = j["bundle_type"].get(); // backwards compat w bundle_structure.json + if (j.contains("description")) this->description = j["description"].get(); + else if (j.contains("bundle_type")) this->description = j["bundle_type"].get(); // backwards compat w bundle_structure.json - if (j.contains("author")) - this->author = j["author"].get(); + if (j.contains("author")) this->author = j["author"].get(); - if (j.contains("imported_time")) - this->imported_time = j["imported_time"].get(); + if (j.contains("imported_time")) this->imported_time = j["imported_time"].get(); - if (j.contains("updated_time")) - this->updated_time = j["updated_time"].get(); + if (j.contains("updated_time")) this->updated_time = j["updated_time"].get(); - if (j.contains("print_presets")) - this->print_presets = j["print_presets"].get>(); - - if (j.contains("filament_presets")) - this->filament_presets = j["filament_presets"].get>(); - - if (j.contains("printer_presets")) - this->printer_presets = j["printer_presets"].get>(); + if (j.contains("print_presets")) + this->print_presets = j["print_presets"].get>(); + + if (j.contains("filament_presets")) + this->filament_presets = j["filament_presets"].get>(); + + if (j.contains("printer_presets")) + this->printer_presets = j["printer_presets"].get>(); return true; } catch (const std::exception& e) { @@ -7656,26 +7446,27 @@ bool BundleMetadata::load_from_json(const std::string& path) bool BundleMetadata::save_to_json(const std::string& path) const { auto strip_prefix = [](const std::vector& names) { - json arr = json::array(); - for (const auto& name : names) { - arr.push_back(boost::filesystem::path(name).filename().string()); - std::string test = boost::filesystem::path(name).filename().string(); - } - return arr; - }; + json arr = json::array(); + for (const auto& name : names) + { + arr.push_back(boost::filesystem::path(name).filename().string()); + std::string test = boost::filesystem::path(name).filename().string(); + } + return arr; + }; try { json j; - j["id"] = this->id; - j["name"] = this->name; - j["version"] = this->version; - j["description"] = this->description; - j["author"] = this->author; + j["id"] = this->id; + j["name"] = this->name; + j["version"] = this->version; + j["description"] = this->description; + j["author"] = this->author; j["imported_time"] = this->imported_time; - j["updated_time"] = this->updated_time; + j["updated_time"] = this->updated_time; - j["print_presets"] = strip_prefix(this->print_presets); - j["filament_presets"] = strip_prefix(this->filament_presets); - j["printer_presets"] = strip_prefix(this->printer_presets); + j["print_presets"] = strip_prefix(this->print_presets); + j["filament_presets"] = strip_prefix(this->filament_presets); + j["printer_presets"] = strip_prefix(this->printer_presets); boost::nowide::ofstream ofs(path); ofs << j.dump(4); @@ -7696,14 +7487,13 @@ bool PresetBundle::load_vendor_cache(const boost::filesystem::path& dir, const s // of the installation, against nothing, since nothing on disk can then be // newer than it. That state is Semver::inf(), which no real profile carries. const boost::filesystem::path profile = dir / (vendor_name + ".json"); - const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) : Semver::inf(); + const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) + : Semver::inf(); return this->load_vendor_cache((dir / (vendor_name + ".opc")).string(), vendor_name, version, base_bundle); } -bool PresetBundle::load_vendor_cache(const std::string& cache_path, - const std::string& expected_vendor_name, - const Semver& expected_vendor_version, - const PresetBundle* base_bundle) +bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name, + const Semver& expected_vendor_version, const PresetBundle* base_bundle) { // What this bundle had counted before the cache was tried. The caller // measures its own parse against this same baseline, so a rejection must @@ -7712,11 +7502,11 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, // Read and validated before this bundle is touched: a rejected file leaves // no state to roll back. VendorCacheData data; - if (!VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) + if (! VendorCacheFile::load(cache_path, expected_vendor_name, expected_vendor_version, data)) return false; try { - const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match - this->vendors = std::move(data.vendors); + const std::string& vendor_name = expected_vendor_name; // VendorCacheFile::load checked they match + this->vendors = std::move(data.vendors); // What the parse counted before install took over; install recounts its // own below, so m_errors comes out as a JSON parse would leave it. @@ -7726,13 +7516,13 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, // them straight after parsing — same code, same order. The substitution // context stays empty (the entries were substituted when they were // parsed), so no substitutions are reported, as before. - ConfigSubstitutionContext substitution_context{ForwardCompatibilitySubstitutionRule::EnableSilent}; + ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent }; PresetsConfigSubstitutions substitutions; std::map configs; std::map filament_id_maps; const std::string path = boost::filesystem::path(cache_path).parent_path().string(); - size_t count = 0; - auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { + size_t count = 0; + auto install_entries = [&](const std::vector& entries, PresetCollection* presets, bool is_from_lib) { configs.clear(); filament_id_maps.clear(); // Only configs of presets that other entries inherit are ever looked @@ -7741,14 +7531,14 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, // become the m_config_maps other vendors resolve against. std::set inherited; for (const CachedPreset& entry : entries) - if (!entry.inherits.empty()) + if (! entry.inherits.empty()) inherited.insert(entry.inherits); const std::set* retain_configs = is_from_lib ? nullptr : &inherited; for (const CachedPreset& entry : entries) { - const std::string reason = load_vendor_preset(entry, path, vendor_name, base_bundle, LoadConfigBundleAttribute::LoadSystem, - substitution_context, substitutions, configs, filament_id_maps, presets, - count, is_from_lib, retain_configs); - if (!reason.empty()) + const std::string reason = load_vendor_preset(entry, path, vendor_name, + base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions, + configs, filament_id_maps, presets, count, is_from_lib, retain_configs); + if (! reason.empty()) throw std::runtime_error("entry " + entry.name + " failed to install: " + reason); } }; diff --git a/src/libslic3r/PublishSettings.cpp b/src/libslic3r/PublishSettings.cpp index c4c71f9436..6d28cc0af3 100644 --- a/src/libslic3r/PublishSettings.cpp +++ b/src/libslic3r/PublishSettings.cpp @@ -19,6 +19,27 @@ std::string publish_base_key(const std::string &key) return pos == std::string::npos ? key : key.substr(0, pos); } +// Parse the trailing "#N" variant index ("retraction_length#2" -> 2). Returns -1 when the key +// carries no '#' separator or its suffix is malformed; mirrors the importer's strict parse +// (PresetBundle.cpp) so the export side rejects the same variants the receiver would skip. +static int publish_variant_index(const std::string &key, const std::string &base_key) +{ + if (key.size() <= base_key.size() || key.compare(0, base_key.size(), base_key) != 0 || key[base_key.size()] != '#') + return -1; + const std::string suffix = key.substr(base_key.size() + 1); + if (suffix.empty()) + return -1; + int idx = 0; + for (const char c : suffix) { + if (c < '0' || c > '9') + return -1; + idx = idx * 10 + (c - '0'); + if (idx > 1000000) // overflow guard; real vector sizes are tiny + return -1; + } + return idx; +} + std::string normalize_filament_type(const std::string& type) { if (type.empty()) @@ -174,8 +195,8 @@ DynamicPrintConfig filter_published_config( DynamicPrintConfig filtered; std::set base_keys_to_include; - // Never masked (whole-vector serialization): identity, plate geometry and process/printer - // keys. + // Never masked (whole-vector serialization): identity, plate geometry, process keys and + // printer keys without a "#N" variant. std::set mask_exempt_keys; // Material entries: base key -> author slots whose values must survive; other slots are // masked to their defaults so a publish (partial or full) does not leak unrelated slot @@ -209,12 +230,23 @@ DynamicPrintConfig filter_published_config( mask_exempt_keys.insert(key); } - // 3. Process and printer published keys + // 3. Process and printer published keys. Printer per-extruder keys carry a "#N" variant + // (e.g. retraction_length#2): mask the base to the author's extruder index so a partial + // publish does not serialize every extruder's value (same slot-masking as the material side). + const std::set &printer_keys = publishable_printer_keys(); for (const std::string &key : published_keys) { const std::string base_key = publish_base_key(key); - if (!base_key.empty()) { - base_keys_to_include.insert(base_key); - mask_exempt_keys.insert(base_key); + if (base_key.empty()) + continue; + base_keys_to_include.insert(base_key); + if (printer_keys.count(base_key) != 0) { + const int variant_idx = publish_variant_index(key, base_key); + if (variant_idx >= 0) + slot_mask_map[base_key].insert(variant_idx); + else + mask_exempt_keys.insert(base_key); // bare printer key or malformed variant: whole vector + } else { + mask_exempt_keys.insert(base_key); // process key: whole vector } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 044771bf7c..2ee875901c 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -3343,17 +3343,7 @@ size_t NotificationManager::get_notification_count() const void NotificationManager::bbl_show_plateinfo_notification(const std::string &text) { NotificationData data{NotificationType::BBLPlateInfo, NotificationLevel::PrintInfoNotificationLevel, BBL_NOTICE_MAX_INTERVAL, text}; - - for (std::unique_ptr ¬ification : m_pop_notifications) { - if (notification->get_type() == NotificationType::BBLPlateInfo) { - notification->reinit(); - notification->update(data); - return; - } - } - - auto notification = std::make_unique(data, m_id_provider, m_evt_handler); - push_notification_data(std::move(notification), 0); + push_notification_data(data, 0); } void NotificationManager::bbl_close_3mf_warn_notification() @@ -3367,17 +3357,7 @@ void NotificationManager::bbl_close_3mf_warn_notification() void NotificationManager::bbl_show_3mf_warn_notification(const std::string &text, NotificationLevel level) { NotificationData data{NotificationType::BBL3MFInfo, level, BBL_NOTICE_MAX_INTERVAL, text}; - - for (std::unique_ptr ¬ification : m_pop_notifications) { - if (notification->get_type() == NotificationType::BBL3MFInfo) { - notification->reinit(); - notification->update(data); - return; - } - } - - auto notification = std::make_unique(data, m_id_provider, m_evt_handler); - push_notification_data(std::move(notification), 0); + push_notification_data(data, 0); } void NotificationManager::bbl_close_plateinfo_notification() @@ -3392,17 +3372,7 @@ void NotificationManager::bbl_close_plateinfo_notification() void NotificationManager::bbl_show_preview_only_notification(const std::string &text) { NotificationData data{NotificationType::BBLPreviewOnlyMode, NotificationLevel::WarningNotificationLevel, 0, text}; - - for (std::unique_ptr ¬ification : m_pop_notifications) { - if (notification->get_type() == NotificationType::BBLPreviewOnlyMode) { - notification->reinit(); - notification->update(data); - return; - } - } - - auto notification = std::make_unique(data, m_id_provider, m_evt_handler); - push_notification_data(std::move(notification), 0); + push_notification_data(data, 0); } void NotificationManager::bbl_close_preview_only_notification() diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 55d10dd95c..cb65524dac 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -1083,7 +1083,10 @@ private: NotificationType::ProgressBar, NotificationType::PrintHostUpload, NotificationType::SimplifySuggestion, - NotificationType::ValidateWarning + NotificationType::ValidateWarning, + // A published file load can produce several distinct 3MF warnings (invalid values, + // skipped settings, changed slots); let them stack rather than clobber each other. + NotificationType::BBL3MFInfo }; //prepared (basic) notifications // non-static so its not loaded too early. If static, the translations wont load correctly. diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 86f3626eaa..0d2c5ac4c9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -61,7 +61,7 @@ #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" #include "libslic3r/Format/AMF.hpp" -// #include "libslic3r/Format/3mf.hpp" +//#include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Model.hpp" @@ -128,11 +128,11 @@ #include "NotificationManager.hpp" #include "PresetComboBoxes.hpp" #include "MsgDialog.hpp" -#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount -#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles +#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount +#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles #include "ProjectDirtyStateManager.hpp" #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification -#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file +#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GizmoObjectManipulation.hpp" // BBS @@ -155,7 +155,7 @@ #endif // __APPLE__ #include -#include // Needs to be last because reasons :-/ +#include // Needs to be last because reasons :-/ #include #include "WipeTowerDialog.hpp" #include "MixedFilamentDialog.hpp" @@ -195,9 +195,10 @@ using Slic3r::Preset; using Slic3r::GUI::format_wxstr; using namespace nlohmann; -static const std::pair THUMBNAIL_SIZE_3MF = {512, 512}; +static const std::pair THUMBNAIL_SIZE_3MF = { 512, 512 }; -namespace Slic3r { namespace GUI { +namespace Slic3r { +namespace GUI { // A textured mesh is only worth routing through the import dialog when it actually carries // decoded image data; UV-only meshes have nothing to sample. @@ -210,37 +211,37 @@ static bool has_importable_texture(const Slic3r::TexturedMesh& textured_mesh) return true; return std::any_of(textured_mesh.textures.begin(), textured_mesh.textures.end(), - [](const Slic3r::TextureImage& texture) { return !texture.data.empty(); }); + [](const Slic3r::TextureImage& texture) { return !texture.data.empty(); }); } -wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); -wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); -wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); -wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); -wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); -wxDEFINE_EVENT(EVT_EXPORT_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_IMPORT_MODEL_ID, wxCommandEvent); -wxDEFINE_EVENT(EVT_DOWNLOAD_PROJECT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PUBLISH, wxCommandEvent); -wxDEFINE_EVENT(EVT_OPEN_PLATESETTINGSDIALOG, wxCommandEvent); +wxDEFINE_EVENT(EVT_SCHEDULE_BACKGROUND_PROCESS, SimpleEvent); +wxDEFINE_EVENT(EVT_SLICING_UPDATE, SlicingStatusEvent); +wxDEFINE_EVENT(EVT_SLICING_COMPLETED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PROCESS_COMPLETED, SlicingProcessCompletedEvent); +wxDEFINE_EVENT(EVT_EXPORT_BEGAN, wxCommandEvent); +wxDEFINE_EVENT(EVT_EXPORT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_IMPORT_MODEL_ID, wxCommandEvent); +wxDEFINE_EVENT(EVT_DOWNLOAD_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH, wxCommandEvent); +wxDEFINE_EVENT(EVT_OPEN_PLATESETTINGSDIALOG, wxCommandEvent); wxDEFINE_EVENT(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, wxCommandEvent); // BBS: backup & restore -wxDEFINE_EVENT(EVT_RESTORE_PROJECT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PRINT_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_SEND_CALIBRATION_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_SEND_FINISHED, wxCommandEvent); -wxDEFINE_EVENT(EVT_PUBLISH_FINISHED, wxCommandEvent); -// BBS: repair model -wxDEFINE_EVENT(EVT_REPAIR_MODEL, wxCommandEvent); -wxDEFINE_EVENT(EVT_FILAMENT_COLOR_CHANGED, wxCommandEvent); -wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_NETWORKING, wxCommandEvent); -wxDEFINE_EVENT(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, wxCommandEvent); -wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_HINT, wxCommandEvent); -wxDEFINE_EVENT(EVT_PREVIEW_ONLY_MODE_HINT, wxCommandEvent); -// BBS: change light/dark mode -wxDEFINE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); -// BBS: print -wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); +wxDEFINE_EVENT(EVT_RESTORE_PROJECT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PRINT_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_CALIBRATION_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_SEND_FINISHED, wxCommandEvent); +wxDEFINE_EVENT(EVT_PUBLISH_FINISHED, wxCommandEvent); +//BBS: repair model +wxDEFINE_EVENT(EVT_REPAIR_MODEL, wxCommandEvent); +wxDEFINE_EVENT(EVT_FILAMENT_COLOR_CHANGED, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_NETWORKING, wxCommandEvent); +wxDEFINE_EVENT(EVT_UPDATE_PLUGINS_WHEN_LAUNCH, wxCommandEvent); +wxDEFINE_EVENT(EVT_INSTALL_PLUGIN_HINT, wxCommandEvent); +wxDEFINE_EVENT(EVT_PREVIEW_ONLY_MODE_HINT, wxCommandEvent); +//BBS: change light/dark mode +wxDEFINE_EVENT(EVT_GLCANVAS_COLOR_MODE_CHANGED, SimpleEvent); +//BBS: print +wxDEFINE_EVENT(EVT_PRINT_FROM_SDCARD_VIEW, SimpleEvent); wxDEFINE_EVENT(EVT_CREATE_FILAMENT, SimpleEvent); wxDEFINE_EVENT(EVT_MODIFY_FILAMENT, SimpleEvent); @@ -250,31 +251,32 @@ wxDEFINE_EVENT(EVT_ADD_CUSTOM_FILAMENT, ColorEvent); wxDEFINE_EVENT(EVT_NOTICE_CHILDE_SIZE_CHANGED, SimpleEvent); wxDEFINE_EVENT(EVT_NOTICE_FULL_SCREEN_CHANGED, IntEvent); #define PRINTER_THUMBNAIL_SIZE (wxSize(40, 40)) // ORCA -#define PRINTER_PANEL_SIZE (wxSize(70, 60)) // ORCA -#define PRINTER_PANEL_RADIUS (6) // ORCA +#define PRINTER_PANEL_SIZE ( wxSize(70, 60)) // ORCA +#define PRINTER_PANEL_RADIUS (6) // ORCA #define BTN_SYNC_SIZE (wxSize(FromDIP(96), FromDIP(98))) static string get_diameter_string(float diameter) { std::ostringstream stream; // ORCA ensure 0.25 returned as 0.25. previous code returned as 0.2 because of std::setprecision(1) - stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably + stream << std::fixed << std::setprecision(2) << diameter; // Use 2 decimals to capture 0.25 / 0.15 reliably std::string s = stream.str(); - if (s.find('.') != std::string::npos) { // Remove trailing zeros, but keep at least one decimal if needed + if (s.find('.') != std::string::npos) { // Remove trailing zeros, but keep at least one decimal if needed s.erase(s.find_last_not_of('0') + 1); - if (s.back() == '.') - s += '0'; // Ensure "1." → "1.0" + if (s.back() == '.') s += '0'; // Ensure "1." → "1.0" } return s; } -template static void set_config_values(DynamicPrintConfig* config, const std::string& key, T value) +template +static void set_config_values(DynamicPrintConfig *config, const std::string &key, T value) { auto config_opt = config->option(key); if (config_opt) { for (size_t i = 0; i < config_opt->values.size(); ++i) { config_opt->values[i] = value; } - } else { + } + else { BOOST_LOG_TRIVIAL(info) << "set_config_values: the key" << key << "is empty."; } } @@ -296,22 +298,36 @@ bool Plater::has_illegal_filename_characters(const std::string& name) } void Plater::show_illegal_characters_warning(wxWindow* parent) -{ show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\""); } +{ + show_error(parent, _L("Invalid name, the following characters are not allowed:") + " <>:/\\|?*\""); +} -void Plater::mark_plate_toolbar_image_dirty() { m_b_plate_toolbar_image_dirty = true; } +void Plater::mark_plate_toolbar_image_dirty() +{ + m_b_plate_toolbar_image_dirty = true; +} -bool Plater::is_plate_toolbar_image_dirty() const { return m_b_plate_toolbar_image_dirty; } +bool Plater::is_plate_toolbar_image_dirty() const +{ + return m_b_plate_toolbar_image_dirty; +} -void Plater::clear_plate_toolbar_image_dirty() { m_b_plate_toolbar_image_dirty = false; } +void Plater::clear_plate_toolbar_image_dirty() +{ + m_b_plate_toolbar_image_dirty = false; +} -static std::map bed_type_thumbnails = {{BedType::btPC, "bed_cool"}, - {BedType::btEP, "bed_engineering"}, - {BedType::btPEI, "bed_high_templ"}, - {BedType::btPTE, "bed_pei"}, - {BedType::btPCT, "bed_pei_cool"}, - {BedType::btSuperTack, "bed_cool_supertack"}}; +static std::map bed_type_thumbnails = { + {BedType::btPC, "bed_cool" }, + {BedType::btEP, "bed_engineering" }, + {BedType::btPEI, "bed_high_templ" }, + {BedType::btPTE, "bed_pei" }, + {BedType::btPCT, "bed_pei_cool" }, + {BedType::btSuperTack, "bed_cool_supertack" } +}; -enum SlicedInfoIdx { +enum SlicedInfoIdx +{ siFilament_m, siFilament_mm3, siFilament_g, @@ -331,30 +347,37 @@ enum class LoadFilesType { Multiple3MFOther, }; -enum class LoadType : unsigned char { Unknown, OpenProject, LoadGeometry, LoadConfig }; +enum class LoadType : unsigned char +{ + Unknown, + OpenProject, + LoadGeometry, + LoadConfig +}; class SlicedInfo : public wxStaticBoxSizer { public: - SlicedInfo(wxWindow* parent); - void SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label = ""); + SlicedInfo(wxWindow *parent); + void SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label=""); private: std::vector> info_vec; }; -SlicedInfo::SlicedInfo(wxWindow* parent) : wxStaticBoxSizer(new wxStaticBox(parent, wxID_ANY, _L("Sliced Info")), wxVERTICAL) +SlicedInfo::SlicedInfo(wxWindow *parent) : + wxStaticBoxSizer(new wxStaticBox(parent, wxID_ANY, _L("Sliced Info")), wxVERTICAL) { GetStaticBox()->SetFont(wxGetApp().bold_font()); wxGetApp().UpdateDarkUI(GetStaticBox()); - auto* grid_sizer = new wxFlexGridSizer(2, 5, 15); + auto *grid_sizer = new wxFlexGridSizer(2, 5, 15); grid_sizer->SetFlexibleDirection(wxVERTICAL); info_vec.reserve(siCount); auto init_info_label = [this, parent, grid_sizer](wxString text_label) { - auto* text = new wxStaticText(parent, wxID_ANY, text_label); + auto *text = new wxStaticText(parent, wxID_ANY, text_label); text->SetForegroundColour(*wxBLACK); text->SetFont(wxGetApp().small_font()); auto info_label = new wxStaticText(parent, wxID_ANY, "N/A"); @@ -377,7 +400,7 @@ SlicedInfo::SlicedInfo(wxWindow* parent) : wxStaticBoxSizer(new wxStaticBox(pare this->Show(false); } -void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label /*=""*/) +void SlicedInfo::SetTextAndShow(SlicedInfoIdx idx, const wxString& text, const wxString& new_label/*=""*/) { const bool show = text != "N/A"; if (show) @@ -402,8 +425,9 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove } static const std::regex state_pattern(R"(state=(\d+);)"); - constexpr unsigned int disabled_wayland_flags = static_cast(wxAuiPaneInfo::optionFloating) | - static_cast(wxAuiPaneInfo::optionFloatable); + constexpr unsigned int disabled_wayland_flags = + static_cast(wxAuiPaneInfo::optionFloating) | + static_cast(wxAuiPaneInfo::optionFloatable); const std::string input = layout.utf8_string(); std::string output; @@ -417,9 +441,9 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove output.append(search_start, match[0].first); try { - const unsigned long state = std::stoul(match[1].str()); + const unsigned long state = std::stoul(match[1].str()); const unsigned long sanitized_state = state & ~static_cast(disabled_wayland_flags); - modified = modified || sanitized_state != state; + modified = modified || sanitized_state != state; output += "state=" + std::to_string(sanitized_state) + ";"; } catch (const std::exception&) { @@ -442,7 +466,11 @@ wxString sanitize_window_layout_for_wayland(const wxString& layout, bool* remove // Sidebar / private -enum class ActionButtonType : int { abReslice, abExport, abSendGCode }; +enum class ActionButtonType : int { + abReslice, + abExport, + abSendGCode +}; // Background for the extruder-group title chip and its edit buttons, matching the StaticGroup // interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour. @@ -461,7 +489,7 @@ static wxColour extruder_group_chip_bg() class HoverLabel : public wxPanel { public: - HoverLabel(wxWindow* parent, const wxString& label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) + HoverLabel(wxWindow *parent, const wxString &label) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE) { SetBackgroundColour(extruder_group_chip_bg()); auto sizer = new wxBoxSizer(wxHORIZONTAL); @@ -488,7 +516,7 @@ public: m_hover_btn = new ScalableButton(this, wxID_ANY, "dot"); m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1)); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); - m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto& evt) { + m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { if (m_enabled && m_hover_on_click) m_hover_on_click(); }); @@ -529,7 +557,7 @@ public: UpdateSizing(); } - void SetTitle(const wxString& title) + void SetTitle(const wxString &title) { m_label->SetLabel(title); UpdateSizing(); @@ -542,7 +570,7 @@ public: { SetBackgroundColour(extruder_group_chip_bg()); m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B"))); - for (wxStaticText* t : {m_brace_left, m_count, m_brace_right}) + for (wxStaticText *t : {m_brace_left, m_count, m_brace_right}) t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); m_hover_btn->SetBackgroundColour(extruder_group_chip_bg()); Refresh(); @@ -560,36 +588,36 @@ private: GetParent()->Layout(); } - wxStaticText* m_label; - wxStaticText* m_brace_left; - wxStaticText* m_count; - wxStaticText* m_brace_right; - ScalableButton* m_hover_btn; + wxStaticText *m_label; + wxStaticText *m_brace_left; + wxStaticText *m_count; + wxStaticText *m_brace_right; + ScalableButton *m_hover_btn; std::function m_hover_on_click; - bool m_enabled{false}; + bool m_enabled{false}; }; struct ExtruderGroup : StaticGroup { - ExtruderGroup(wxWindow* parent, int index, wxString const& title); - wxStaticBoxSizer* sizer = nullptr; - HoverLabel* hover_label = nullptr; - ScalableButton* btn_edit = nullptr; - ComboBox* combo_diameter = nullptr; - ComboBox* combo_flow = nullptr; - AMSPreview* ams[4] = {nullptr}; - wxStaticText* ams_not_installed_msg{nullptr}; - ScalableButton* btn_up{nullptr}; - ScalableButton* btn_down{nullptr}; - wxBoxSizer* hsizer_ams{nullptr}; - size_t page_cur{0}; - size_t page_num{3}; - size_t ams_n4 = 0; - size_t ams_n1 = 0; + ExtruderGroup(wxWindow * parent, int index, wxString const &title); + wxStaticBoxSizer *sizer = nullptr; + HoverLabel * hover_label = nullptr; + ScalableButton * btn_edit = nullptr; + ComboBox * combo_diameter = nullptr; + ComboBox * combo_flow = nullptr; + AMSPreview * ams[4] = {nullptr}; + wxStaticText *ams_not_installed_msg{nullptr}; + ScalableButton * btn_up{nullptr}; + ScalableButton * btn_down{nullptr}; + wxBoxSizer *hsizer_ams { nullptr }; + size_t page_cur{0}; + size_t page_num{3}; + size_t ams_n4 = 0; + size_t ams_n1 = 0; std::vector ams_4; std::vector ams_1; - wxString diameter; + wxString diameter; void set_ams_count(int n4, int n1) { @@ -604,23 +632,11 @@ struct ExtruderGroup : StaticGroup void update_ams(); void SetTitle(const wxString& title); - void SetCount(int count) - { - if (hover_label) - hover_label->SetCount(count); - } - void SetEditEnabled(bool enable) - { - if (hover_label) - hover_label->EnableEdit(enable); - } - void SetOnHoverClick(std::function on_click) - { - if (hover_label) - hover_label->SetOnHoverClick(std::move(on_click)); - } + void SetCount(int count) { if (hover_label) hover_label->SetCount(count); } + void SetEditEnabled(bool enable) { if (hover_label) hover_label->EnableEdit(enable); } + void SetOnHoverClick(std::function on_click) { if (hover_label) hover_label->SetOnHoverClick(std::move(on_click)); } - void sync_ams(MachineObject const* obj, std::vector const& ams4, std::vector const& ams1); + void sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1); void Rescale() { @@ -648,70 +664,70 @@ struct ExtruderGroup : StaticGroup struct Sidebar::priv { - Plater* plater; + Plater *plater; - wxPanel* scrolled = nullptr; - PlaterPresetComboBox* combo_sla_print = nullptr; - PlaterPresetComboBox* combo_sla_material = nullptr; + wxPanel *scrolled = nullptr; + PlaterPresetComboBox *combo_sla_print = nullptr; + PlaterPresetComboBox *combo_sla_material = nullptr; // Printer - wxSizer* vsizer_printer = nullptr; - wxBoxSizer* extruder_dual_sizer = nullptr; - wxBoxSizer* extruder_single_sizer = nullptr; + wxSizer * vsizer_printer = nullptr; + wxBoxSizer * extruder_dual_sizer = nullptr; + wxBoxSizer * extruder_single_sizer = nullptr; // Printer - preset - StaticBox* panel_printer_preset = nullptr; - wxStaticBitmap* image_printer = nullptr; - PlaterPresetComboBox* combo_printer = nullptr; - ScalableButton* btn_edit_printer = nullptr; - ScalableButton* btn_connect_printer = nullptr; + StaticBox * panel_printer_preset = nullptr; + wxStaticBitmap * image_printer = nullptr; + PlaterPresetComboBox *combo_printer = nullptr; + ScalableButton * btn_edit_printer = nullptr; + ScalableButton * btn_connect_printer = nullptr; // Nozzle diameter - StaticBox* panel_nozzle_dia = nullptr; - Label* label_nozzle_title = nullptr; - ComboBox* combo_nozzle_dia = nullptr; - Label* label_nozzle_type = nullptr; + StaticBox * panel_nozzle_dia = nullptr; + Label * label_nozzle_title= nullptr; + ComboBox * combo_nozzle_dia = nullptr; + Label * label_nozzle_type = nullptr; // Printer - bed - StaticBox* panel_printer_bed = nullptr; - wxStaticBitmap* image_printer_bed = nullptr; - ComboBox* combo_printer_bed = nullptr; + StaticBox * panel_printer_bed = nullptr; + wxStaticBitmap *image_printer_bed = nullptr; + ComboBox * combo_printer_bed = nullptr; - ImageDPIFrame* big_bed_image_popup = nullptr; + ImageDPIFrame *big_bed_image_popup = nullptr; // Printer - sync - // Button *btn_sync_printer; + //Button *btn_sync_printer; std::shared_ptr counter_sync_printer = std::make_shared(); - wxTimer* timer_sync_printer = new wxTimer(); + wxTimer * timer_sync_printer = new wxTimer(); // Printer - ams - ExtruderGroup* left_extruder = nullptr; - ExtruderGroup* right_extruder = nullptr; - ExtruderGroup* single_extruder = nullptr; + ExtruderGroup *left_extruder = nullptr; + ExtruderGroup *right_extruder = nullptr; + ExtruderGroup *single_extruder = nullptr; - int FromDIP(int n) { return plater->FromDIP(n); } + int FromDIP(int n) { return plater->FromDIP(n); } void layout_printer(bool isBBL, bool isDual); void flush_printer_sync(bool restart = false); - PlaterPresetComboBox* combo_print = nullptr; + PlaterPresetComboBox *combo_print = nullptr; std::vector combos_filament; - int editing_filament = -1; - wxBoxSizer* sizer_filaments = nullptr; + int editing_filament = -1; + wxBoxSizer *sizer_filaments = nullptr; - // BBS Sidebar widgets + //BBS Sidebar widgets wxPanel* m_panel_print_title; wxStaticText* m_staticText_print_title; wxPanel* m_panel_print_content; - wxBoxSizer* sizer_params; + wxBoxSizer *sizer_params; - // wxComboBox * m_comboBox_print_preset; - wxStaticLine* m_staticline1; + //wxComboBox * m_comboBox_print_preset; + wxStaticLine * m_staticline1; StaticBox* m_panel_filament_title; - wxPanel* m_panel_filament_separator; + wxPanel* m_panel_filament_separator; wxStaticText* m_staticText_filament_settings; wxStaticText* m_staticText_filament_count; - ScalableButton* m_bpButton_add_filament; - ScalableButton* m_bpButton_del_filament; - ScalableButton* m_bpButton_ams_filament; - ScalableButton* m_bpButton_set_filament; + ScalableButton * m_bpButton_add_filament; + ScalableButton * m_bpButton_del_filament; + ScalableButton * m_bpButton_ams_filament; + ScalableButton * m_bpButton_set_filament; int m_menu_filament_id = -1; wxPanel* m_filament_area_wrapper; @@ -721,63 +737,63 @@ struct Sidebar::priv // Mixed-color filament section. Sits directly under the physical filament list in // scrolled_sizer. BBS hosts the equivalent widgets inside an m_filament_area_wrapper // that Orca's sidebar has no counterpart for, so these are parented to p->scrolled. - wxPanel* m_btn_add_mixed_filament{nullptr}; // "+ Add Mixed Filament" full-width button - wxPanel* m_panel_mixed_title{nullptr}; // title row: "Mixed Filament" + add/del buttons - wxStaticText* m_text_mixed_title{nullptr}; - ScalableButton* m_btn_mixed_add{nullptr}; - ScalableButton* m_btn_mixed_del{nullptr}; - wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows - wxPanel* m_panel_mixed_content{nullptr}; - wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments - wxPanel* m_panel_mixed_warning{nullptr}; // red bar for broken/mismatched mixes - wxStaticText* m_text_mixed_warning{nullptr}; - bool m_mixed_filament_broken{false}; + wxPanel* m_btn_add_mixed_filament{nullptr}; // "+ Add Mixed Filament" full-width button + wxPanel* m_panel_mixed_title{nullptr}; // title row: "Mixed Filament" + add/del buttons + wxStaticText* m_text_mixed_title{nullptr}; + ScalableButton* m_btn_mixed_add{nullptr}; + ScalableButton* m_btn_mixed_del{nullptr}; + wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows + wxPanel* m_panel_mixed_content{nullptr}; + wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments + wxPanel* m_panel_mixed_warning{nullptr}; // red bar for broken/mismatched mixes + wxStaticText* m_text_mixed_warning{nullptr}; + bool m_mixed_filament_broken{false}; wxScrolledWindow* m_scrolledWindow_filament_content; wxStaticLine* m_staticline2; wxPanel* m_panel_project_title; ScalableButton* m_filament_icon = nullptr; - Button* m_purge_mode_btn = nullptr; - Button* m_flushing_volume_btn = nullptr; - TextInput* m_search_item = nullptr; - StaticBox* m_search_bar = nullptr; + Button * m_purge_mode_btn = nullptr; + Button * m_flushing_volume_btn = nullptr; + TextInput* m_search_item = nullptr; + StaticBox* m_search_bar = nullptr; Search::SearchObjectDialog* dia = nullptr; // BBS printer config - StaticBox* m_panel_printer_title = nullptr; - wxPanel* m_panel_printer_separator = nullptr; - ScalableButton* m_printer_icon = nullptr; - ScalableButton* m_printer_connect = nullptr; - ScalableButton* m_printer_bbl_sync = nullptr; - ScalableButton* m_printer_setting = nullptr; - wxStaticText* m_text_printer_settings = nullptr; - wxPanel* m_panel_printer_content = nullptr; + StaticBox* m_panel_printer_title = nullptr; + wxPanel* m_panel_printer_separator = nullptr; + ScalableButton* m_printer_icon = nullptr; + ScalableButton* m_printer_connect = nullptr; + ScalableButton* m_printer_bbl_sync = nullptr; + ScalableButton* m_printer_setting = nullptr; + wxStaticText * m_text_printer_settings = nullptr; + wxPanel* m_panel_printer_content = nullptr; // Filament Track Switch status overlay: an icon floated over the left/single extruder AMS area, // shown only when the switch is installed (green when ready, red when not calibrated). wxStaticBitmap* extruder_separator_icon = nullptr; - ObjectList* m_object_list{nullptr}; - ObjectSettings* object_settings{nullptr}; - ObjectLayers* object_layers{nullptr}; + ObjectList *m_object_list{ nullptr }; + ObjectSettings *object_settings{ nullptr }; + ObjectLayers *object_layers{ nullptr }; - wxButton* btn_export_gcode; - wxButton* btn_reslice; - ScalableButton* btn_send_gcode; - // ScalableButton *btn_eject_device; - ScalableButton* btn_export_gcode_removable; // exports to removable drives (appears only if removable drive is connected) + wxButton *btn_export_gcode; + wxButton *btn_reslice; + ScalableButton *btn_send_gcode; + //ScalableButton *btn_eject_device; + ScalableButton* btn_export_gcode_removable; //exports to removable drives (appears only if removable drive is connected) - bool is_switching_diameter{false}; - Search::OptionsSearcher searcher; + bool is_switching_diameter{false}; + Search::OptionsSearcher searcher; std::string ams_list_device; - priv(Plater* plater) : plater(plater) {} + priv(Plater *plater) : plater(plater) {} ~priv(); void show_preset_comboboxes(); void jump_to_object(ObjectDataViewModelNode* item); void can_search(); - bool sync_extruder_list(bool& only_external_material, bool is_manual = false); + bool sync_extruder_list(bool &only_external_material, bool is_manual = false); // Resolve the nozzle option for a multi-nozzle machine. Returns nullopt (and is a no-op) unless // extruder_count >= 2 && support_multi_nozzle. When is_manual, always pops the MultiNozzleSyncDialog; // otherwise reuses the app_config-cached option when the machine's nozzle config is unchanged. @@ -808,16 +824,17 @@ struct Sidebar::priv void Sidebar::priv::layout_printer(bool isBBL, bool isDual) { // Printer - preset - if (auto sizer = static_cast(panel_printer_preset->GetSizer()); - sizer == nullptr /*|| isBBL != (sizer->GetOrientation() == wxVERTICAL)*/) { - // if (isBBL) { - wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); - hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); - hsizer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(2)); // 1 already triggers wxEXPAND - hsizer->AddSpacer(FromDIP(2)); - hsizer->Add(btn_edit_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); - // hsizer->Add(btn_connect_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); - panel_printer_preset->SetSizer(hsizer); + if (auto sizer = static_cast(panel_printer_preset->GetSizer()); + sizer == nullptr /*|| isBBL != (sizer->GetOrientation() == wxVERTICAL)*/) { + + //if (isBBL) { + wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, FromDIP(10)); + hsizer->Add(combo_printer, 1, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(2)); // 1 already triggers wxEXPAND + hsizer->AddSpacer(FromDIP(2)); + hsizer->Add(btn_edit_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + //hsizer->Add(btn_connect_printer, 0, wxRIGHT | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL, FromDIP(SidebarProps::IconSpacing())); + panel_printer_preset->SetSizer(hsizer); //} else { // wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); // hsizer->Add(image_printer, 0, wxLEFT | wxALIGN_CENTER, FromDIP(4)); @@ -829,11 +846,11 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) } if (vsizer_printer->GetItemCount() == 0) { - wxBoxSizer* hsizer_printer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *hsizer_printer = new wxBoxSizer(wxHORIZONTAL); hsizer_printer->Add(panel_printer_preset, 1, wxEXPAND, 0); - hsizer_printer->Add(panel_nozzle_dia, 0, wxLEFT, FromDIP(4)); + hsizer_printer->Add(panel_nozzle_dia , 0, wxLEFT, FromDIP(4)); hsizer_printer->Add(panel_printer_bed, 0, wxLEFT, FromDIP(4)); - // hsizer_printer->Add(btn_sync_printer , 0, wxLEFT, FromDIP(4)); + //hsizer_printer->Add(btn_sync_printer , 0, wxLEFT, FromDIP(4)); vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); vsizer_printer->Add(hsizer_printer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); @@ -848,9 +865,8 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // Filament Track Switch status icon, floated over the extruder AMS area (positioned in // update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip. if (!extruder_separator_icon) { - auto bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); - extruder_separator_icon = new wxStaticBitmap(m_panel_printer_content, wxID_ANY, bitmap.bmp(), wxDefaultPosition, - bitmap.GetBmpSize()); + auto bitmap = ScalableBitmap(m_panel_printer_content, "fila_switch", 10); + extruder_separator_icon = new wxStaticBitmap(m_panel_printer_content, wxID_ANY, bitmap.bmp(), wxDefaultPosition, bitmap.GetBmpSize()); extruder_separator_icon->Hide(); extruder_separator_icon->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& evt) { show_fila_switch_msg(is_fila_switch_ready()); @@ -859,9 +875,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) } // single - extruder_single_sizer = single_extruder->sizer; - wxBoxSizer* extruder_sizer = new wxBoxSizer(wxVERTICAL); - extruder_sizer->Add(extruder_dual_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); + extruder_single_sizer = single_extruder->sizer; + wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL); + extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin())); vsizer_printer->Add(extruder_sizer, 1, wxEXPAND | wxTOP, FromDIP(2)); @@ -869,14 +885,14 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) vsizer_printer->AddSpacer(FromDIP(SidebarProps::ContentMarginV())); } - // btn_connect_printer->Show(!isBBL); + //btn_connect_printer->Show(!isBBL); m_printer_connect->Show(!isBBL); - // btn_sync_printer->Show(isBBL); + //btn_sync_printer->Show(isBBL); m_printer_bbl_sync->Show(isBBL); // ORCA show plate type combo box only when its supported - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - const auto& cfg = preset_bundle.printers.get_edited_preset().config; + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + const auto& cfg = preset_bundle.printers.get_edited_preset().config; // Orca: we use preset_bundle.is_bbl_vendor() instead of isBBL to determine if the plate type combo box should be shown // ref: https://github.com/OrcaSlicer/OrcaSlicer/pull/11610#discussion_r2607411847 panel_printer_bed->Show(preset_bundle.is_bbl_vendor() || cfg.opt_bool("support_multi_bed_types")); @@ -887,7 +903,7 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // Single nozzle & non ams if (!isDual) { // Orca: for printer without flow variant, we do not show flow combo - int extruder_count = 0; + int extruder_count = 0; const bool has_flow_variant = cfg.support_different_extruders(extruder_count); panel_nozzle_dia->Show(!has_flow_variant); @@ -899,9 +915,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual) // ORCA ensure printer section is visible after changing printer from printer selection dialog // this will inform user on printer change when printer section is collapsed - if (m_panel_printer_content) { + if (m_panel_printer_content){ bool isShown = m_panel_printer_content->IsShown(); - if (!isShown && m_text_printer_settings) { + if(!isShown && m_text_printer_settings){ m_text_printer_settings->SetLabel(_L("Printer")); // ensure title returns to default state m_panel_printer_content->Show(); } @@ -914,7 +930,7 @@ void Sidebar::priv::flush_printer_sync(bool restart) *counter_sync_printer = 6; timer_sync_printer->Start(500); } - // btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688"); + //btn_sync_printer->SetBackgroundColorNormal((*counter_sync_printer & 1) ? "#F8F8F8" :"#009688"); m_printer_bbl_sync->SetBitmap_((*counter_sync_printer & 1) ? "printer_sync_not" : "printer_sync_ok"); if (--*counter_sync_printer <= 0) timer_sync_printer->Stop(); @@ -923,7 +939,7 @@ void Sidebar::priv::flush_printer_sync(bool restart) Sidebar::priv::~priv() { // BBS - // delete object_manipulation; + //delete object_manipulation; delete object_settings; // BBS #if 0 @@ -935,7 +951,7 @@ void Sidebar::priv::show_preset_comboboxes() { const bool showSLA = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptSLA; -// BBS +//BBS #if 0 for (size_t i = 0; i < 4; ++i) sizer_presets->Show(i, !showSLA); @@ -952,7 +968,10 @@ void Sidebar::priv::show_preset_comboboxes() scrolled->Refresh(); } -void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item) { m_object_list->selected_object(item); } +void Sidebar::priv::jump_to_object(ObjectDataViewModelNode* item) +{ + m_object_list->selected_object(item); +} void Sidebar::priv::can_search() { @@ -1003,75 +1022,61 @@ void Sidebar::priv::hide_rich_tip(wxButton* btn) } #endif -std::vector get_min_flush_volumes(const DynamicPrintConfig& full_config, size_t nozzle_id) +std::vector get_min_flush_volumes(const DynamicPrintConfig &full_config, size_t nozzle_id) { - std::vector extra_flush_volumes; - // const auto& full_config = wxGetApp().preset_bundle->full_config(); - // auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + std::vectorextra_flush_volumes; + //const auto& full_config = wxGetApp().preset_bundle->full_config(); + //auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; const ConfigOptionFloatsNullable* nozzle_volume_opt = full_config.option("nozzle_volume"); - int nozzle_volume_val = nozzle_volume_opt ? (int) nozzle_volume_opt->get_at(nozzle_id) : 0; + int nozzle_volume_val = nozzle_volume_opt ? (int)nozzle_volume_opt->get_at(nozzle_id) : 0; const ConfigOptionInt* enable_long_retraction_when_cut_opt = full_config.option("enable_long_retraction_when_cut"); - int machine_enabled_level = 0; + int machine_enabled_level = 0; if (enable_long_retraction_when_cut_opt) { machine_enabled_level = enable_long_retraction_when_cut_opt->value; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": get enable_long_retraction_when_cut from config, value=%1%") % machine_enabled_level; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get enable_long_retraction_when_cut from config, value=%1%")%machine_enabled_level; } const ConfigOptionBools* long_retractions_when_cut_opt = full_config.option("long_retractions_when_cut"); - bool machine_activated = false; + bool machine_activated = false; if (long_retractions_when_cut_opt) { machine_activated = long_retractions_when_cut_opt->values[nozzle_id] == 1; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": get long_retractions_when_cut from config, value=%1%, activated=%2%") % - long_retractions_when_cut_opt->values[0] % machine_activated; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get long_retractions_when_cut from config, value=%1%, activated=%2%")%long_retractions_when_cut_opt->values[0] %machine_activated; } size_t filament_size = full_config.option("filament_diameter")->values.size(); - std::vector filament_retraction_distance_when_cut(filament_size, 18.0f), - printer_retraction_distance_when_cut(filament_size, 18.0f); + std::vector filament_retraction_distance_when_cut(filament_size, 18.0f), printer_retraction_distance_when_cut(filament_size, 18.0f); std::vector filament_long_retractions_when_cut(filament_size, 0); - const ConfigOptionFloats* filament_retraction_distances_when_cut_opt = full_config.option( - "filament_retraction_distances_when_cut"); + const ConfigOptionFloats* filament_retraction_distances_when_cut_opt = full_config.option("filament_retraction_distances_when_cut"); if (filament_retraction_distances_when_cut_opt) { filament_retraction_distance_when_cut = filament_retraction_distances_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": get filament_retraction_distance_when_cut from config, size=%1%, values=%2%") % - filament_retraction_distance_when_cut.size() % - filament_retraction_distances_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_retraction_distance_when_cut from config, size=%1%, values=%2%")%filament_retraction_distance_when_cut.size() %filament_retraction_distances_when_cut_opt->serialize(); } - const ConfigOptionFloats* printer_retraction_distance_when_cut_opt = full_config.option( - "retraction_distances_when_cut"); + const ConfigOptionFloats* printer_retraction_distance_when_cut_opt = full_config.option("retraction_distances_when_cut"); if (printer_retraction_distance_when_cut_opt) { printer_retraction_distance_when_cut = printer_retraction_distance_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": get retraction_distances_when_cut from config, size=%1%, values=%2%") % - printer_retraction_distance_when_cut.size() % printer_retraction_distance_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get retraction_distances_when_cut from config, size=%1%, values=%2%")%printer_retraction_distance_when_cut.size() %printer_retraction_distance_when_cut_opt->serialize(); } - const ConfigOptionBools* filament_long_retractions_when_cut_opt = full_config.option( - "filament_long_retractions_when_cut"); + const ConfigOptionBools* filament_long_retractions_when_cut_opt = full_config.option("filament_long_retractions_when_cut"); if (filament_long_retractions_when_cut_opt) { filament_long_retractions_when_cut = filament_long_retractions_when_cut_opt->values; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": get filament_long_retractions_when_cut from config, size=%1%, values=%2%") % - filament_long_retractions_when_cut.size() % filament_long_retractions_when_cut_opt->serialize(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": get filament_long_retractions_when_cut from config, size=%1%, values=%2%")%filament_long_retractions_when_cut.size() %filament_long_retractions_when_cut_opt->serialize(); } for (size_t idx = 0; idx < filament_size; ++idx) { int extra_flush_volume = nozzle_volume_val; - int retract_length = machine_enabled_level && machine_activated ? printer_retraction_distance_when_cut[nozzle_id] : 0; + int retract_length = machine_enabled_level && machine_activated ? printer_retraction_distance_when_cut[nozzle_id] : 0; unsigned char filament_activated = filament_long_retractions_when_cut[idx]; - double filament_retract_length = filament_retraction_distance_when_cut[idx]; + double filament_retract_length = filament_retraction_distance_when_cut[idx]; if (filament_activated == 0) retract_length = 0; else if (filament_activated == 1 && machine_enabled_level == LongRectrationLevel::EnableFilament) { if (!std::isnan(filament_retract_length)) - retract_length = (int) filament_retraction_distance_when_cut[idx]; + retract_length = (int)filament_retraction_distance_when_cut[idx]; else retract_length = printer_retraction_distance_when_cut[nozzle_id]; } @@ -1092,20 +1097,20 @@ struct DynamicFilamentList : DynamicList // physical-only list for all of its keys. explicit DynamicFilamentList(bool physical_only = false) : physical_only(physical_only) {} bool physical_only; - std::vector> items; + std::vector> items; std::vector slot_map{0}; // combo index -> 1-based filament slot; slot_map[0] = 0 is "Default" - void apply_on(Choice* c) override + void apply_on(Choice *c) override { if (!c) return; if (items.empty()) update(true); - auto cb = dynamic_cast(c->window); + auto cb = dynamic_cast(c->window); if (!cb) return; wxString old_selection = cb->GetStringSelection(); - int old_index = cb->GetSelection(); + int old_index = cb->GetSelection(); // slot_map is already rebuilt here: restoring through it keeps the index of every slot // still listed and sends a vanished slot to the fallback below. int old_slot = old_index >= 0 && old_index < int(slot_map.size()) ? slot_map[old_index] : -1; @@ -1152,7 +1157,7 @@ struct DynamicFilamentList : DynamicList slot_map.assign(1, 0); if (!force && m_choices.empty()) return; - auto icons = get_extruder_color_icons(true); + auto icons = get_extruder_color_icons(true); auto presets = wxGetApp().preset_bundle->filament_presets; for (int i = 0; i < presets.size(); ++i) { if (physical_only && wxGetApp().preset_bundle->is_mixed_filament(i)) @@ -1176,7 +1181,10 @@ static bool has_junction_deviation(const DynamicPrintConfig* printer_config) } const auto gcode_flavor = printer_config->option>("gcode_flavor"); const auto junction_dev = printer_config->option("machine_max_junction_deviation"); - return gcode_flavor && gcode_flavor->value == GCodeFlavor::gcfMarlinFirmware && junction_dev && !junction_dev->values.empty() && + return gcode_flavor && + gcode_flavor->value == GCodeFlavor::gcfMarlinFirmware && + junction_dev && + !junction_dev->values.empty() && junction_dev->values.front() > 0.0; } @@ -1186,22 +1194,21 @@ static DynamicFilamentList dynamic_physical_filament_list(true); // physical slo class AMSCountPopupWindow : public PopupWindow { public: - AMSCountPopupWindow(ExtruderGroup* extruder, int index) : PopupWindow(extruder, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) + AMSCountPopupWindow(ExtruderGroup *extruder, int index) + : PopupWindow(extruder, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS) { SetBackgroundColour(*wxWHITE); - auto msg = new wxStaticText(this, wxID_ANY, _L("Set the number of AMS installed on the nozzle.")); + auto msg = new wxStaticText(this, wxID_ANY, _L("Set the number of AMS installed on the nozzle.")); msg->SetFont(Label::Body_14); msg->SetForegroundColour("#262E30"); msg->Wrap(FromDIP(280)); auto box = new StaticBox(this, wxID_ANY); box->SetBackgroundColor(0xF8F8F8); box->SetBorderWidth(0); - auto img4 = new ScalableButton(box, wxID_ANY, "ams_4_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, - false, 44); - // img4->SetBackgroundColour(*wxWHITE); - auto img1 = new ScalableButton(box, wxID_ANY, "ams_1_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, - false, 44); - // img1->SetBackgroundColour(*wxWHITE); + auto img4 = new ScalableButton(box, wxID_ANY, "ams_4_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); + //img4->SetBackgroundColour(*wxWHITE); + auto img1 = new ScalableButton(box, wxID_ANY, "ams_1_tray", {}, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 44); + //img1->SetBackgroundColour(*wxWHITE); auto txt4 = new wxStaticText(box, wxID_ANY, _L("AMS(4 slots)")); txt4->SetFont(Label::Body_14); txt4->SetBackgroundColour(0xF8F8F8); @@ -1216,28 +1223,28 @@ public: GetAMSCount(1 - index, oth4, oth1); auto val4 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 4 - oth4, ams4); auto val1 = new SpinInput(box, {}, {}, wxDefaultPosition, {FromDIP(60), -1}, 0, 0, 8 - oth1, ams1); - auto event_handler = [index, val4, val1, extruder](auto& evt) { + auto event_handler = [index, val4, val1, extruder](auto &evt) { SetAMSCount(index, val4->GetValue(), val1->GetValue()); UpdateAMSCount(index, extruder); }; val4->Bind(wxEVT_SPINCTRL, event_handler); val1->Bind(wxEVT_SPINCTRL, event_handler); - wxSizer* sizer = new wxBoxSizer(wxVERTICAL); + wxSizer * sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(msg, 0, wxTOP | wxLEFT | wxRIGHT, FromDIP(10)); - wxSizer* sizer2 = new wxBoxSizer(wxVERTICAL); - wxSizer* sizer21 = new wxBoxSizer(wxHORIZONTAL); - sizer21->Add(img4, 0, wxALIGN_CENTRE); - sizer21->Add(txt4, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer21->Add(val4, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer2->Add(sizer21, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, FromDIP(14)); - sizer2->AddSpacer(FromDIP(6)); - wxSizer* sizer22 = new wxBoxSizer(wxHORIZONTAL); - sizer22->Add(img1, 0, wxALIGN_CENTRE); - sizer22->Add(txt1, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer22->Add(val1, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); - sizer2->Add(sizer22, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, FromDIP(14)); - box->SetSizer(sizer2); + wxSizer *sizer2 = new wxBoxSizer(wxVERTICAL); + wxSizer *sizer21 = new wxBoxSizer(wxHORIZONTAL); + sizer21->Add(img4, 0, wxALIGN_CENTRE); + sizer21->Add(txt4, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer21->Add(val4, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer21, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, FromDIP(14)); + sizer2->AddSpacer(FromDIP(6)); + wxSizer *sizer22 = new wxBoxSizer(wxHORIZONTAL); + sizer22->Add(img1, 0, wxALIGN_CENTRE); + sizer22->Add(txt1, 2, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer22->Add(val1, 1, wxLEFT | wxALIGN_CENTRE, FromDIP(10)); + sizer2->Add(sizer22, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, FromDIP(14)); + box->SetSizer(sizer2); sizer->Add(box, 0, wxTOP | wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, FromDIP(14)); SetSizer(sizer); @@ -1245,11 +1252,11 @@ public: Fit(); Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) { - wxPaintDC dc(this); - dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color - dc.SetBrush(*wxTRANSPARENT_BRUSH); - dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); - }); + wxPaintDC dc(this); + dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0); + }); SetBackgroundColour(*wxWHITE); wxGetApp().UpdateDarkUIWin(this); @@ -1257,34 +1264,35 @@ public: static void SetAMSCount(int index, int ams4, int ams1) { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; preset_bundle.extruder_ams_counts.resize(2); - auto& ams_map = preset_bundle.extruder_ams_counts[index]; - ams_map[4] = ams4; - ams_map[1] = ams1; + auto &ams_map = preset_bundle.extruder_ams_counts[index]; + ams_map[4] = ams4; + ams_map[1] = ams1; - std::vector extruder_ams_count = save_extruder_ams_count_to_string(preset_bundle.extruder_ams_counts); - std::string extruder_ams_count_str = boost::algorithm::join(extruder_ams_count, ","); + std::vector extruder_ams_count = save_extruder_ams_count_to_string(preset_bundle.extruder_ams_counts); + std::string extruder_ams_count_str = boost::algorithm::join(extruder_ams_count, ","); wxGetApp().app_config->set("presets", "extruder_ams_count", extruder_ams_count_str); wxGetApp().plater()->update(); // update slice status } - static void GetAMSCount(int index, int& ams4, int& ams1) + static void GetAMSCount(int index, int & ams4, int & ams1) { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; if (preset_bundle.extruder_ams_counts.empty()) { ams4 = 0; ams1 = 0; - } else { + } + else { assert(preset_bundle.extruder_ams_counts.size() == 2); ams4 = preset_bundle.extruder_ams_counts[index][4]; ams1 = preset_bundle.extruder_ams_counts[index][1]; } } - static void UpdateAMSCount(int index, ExtruderGroup* extruder) + static void UpdateAMSCount(int index, ExtruderGroup *extruder) { - std::vector>& ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; + std::vector> &ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; ams_counts.resize(2); std::map& ams_map = ams_counts[index]; if (ams_map.find(4) == ams_map.end()) { @@ -1298,7 +1306,8 @@ public: } }; -ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) : StaticGroup(parent, wxID_ANY, wxString()) +ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title) + : StaticGroup(parent, wxID_ANY, wxString()) { SetFont(Label::Body_10); SetForegroundColour(wxColour("#CECECE")); @@ -1311,18 +1320,16 @@ ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) hover_label = new HoverLabel(this, title); // Nozzle - wxStaticText* label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); + wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter")); label_diameter->SetFont(Label::Body_14); label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - if (index >= 0) - label_diameter->SetMinSize({FromDIP(80), -1}); - auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); - this->combo_diameter = combo_diameter; - wxStaticText* label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); + if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1}); + auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); + this->combo_diameter = combo_diameter; + wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow")); label_flow->SetFont(Label::Body_14); label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - if (index >= 0) - label_flow->SetMinSize({FromDIP(80), -1}); + if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1}); auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); combo_flow->GetDropDown().SetUseContentWidth(true); combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) { @@ -1341,36 +1348,27 @@ ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) this->combo_flow = combo_flow; // AMS - wxStaticText* label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); + wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS")); label_ams->SetFont(Label::Body_14); label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30"))); - // label_ams->SetMinSize({FromDIP(70), -1}); + //label_ams->SetMinSize({FromDIP(70), -1}); if (index >= 0) { btn_edit = new ScalableButton(this, wxID_ANY, "dot"); btn_edit->SetBackgroundColour(extruder_group_chip_bg()); btn_edit->Hide(); - btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto& evt) { - PopupWindow* window = new AMSCountPopupWindow(this, index); - auto size = GetSize(); - auto pos = ClientToScreen({0, size.y + 12}); + btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) { + PopupWindow *window = new AMSCountPopupWindow(this, index); + auto size = GetSize(); + auto pos = ClientToScreen({0, size.y + 12}); size.SetWidth(size.GetWidth() + FromDIP(10)); window->Position(pos, {0, 0}); window->Popup(); }); - auto hovered = std::make_shared(); - for (wxWindow* w : - std::initializer_list{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { - w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent& evt) { - *hovered = w; - btn_edit->SetBitmap_("edit"); - }); - w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent& evt) { - if (*hovered == w) { - btn_edit->SetBitmap_("dot"); - *hovered = nullptr; - } - }); + auto hovered = std::make_shared(); + for (wxWindow *w : std::initializer_list{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) { + w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); }); + w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } }); } } @@ -1392,8 +1390,7 @@ ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2)); hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER); - btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, - false, 14); + btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_up->SetBackgroundColour(*wxWHITE); btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { if (page_cur > 0) @@ -1401,8 +1398,7 @@ ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) update_ams(); }); btn_up->Hide(); - btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, false, 14); + btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14); btn_down->SetBackgroundColour(*wxWHITE); btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) { if (page_cur + 1 < page_num) @@ -1411,25 +1407,25 @@ ExtruderGroup::ExtruderGroup(wxWindow* parent, int index, wxString const& title) }); btn_down->Hide(); - wxBoxSizer* hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL); hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER); hsizer_diameter->Add(combo_diameter, 1, wxEXPAND); - wxBoxSizer* hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL); hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER); hsizer_nozzle->Add(combo_flow, 1, wxEXPAND); if (index < 0) { label_ams->Hide(); ams_not_installed_msg->Hide(); - wxStaticBoxSizer* vsizer = new wxStaticBoxSizer(this, wxVERTICAL); - wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL); - hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL); + hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8)); hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8)); hsizer->AddSpacer(FromDIP(2)); // Avoid badge vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2)); this->sizer = vsizer; } else { - wxStaticBoxSizer* vsizer = new wxStaticBoxSizer(this, wxVERTICAL); + wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL); vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2)); vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2)); @@ -1444,8 +1440,7 @@ void ExtruderGroup::update_ams() static AMSinfo info4; static AMSinfo info1; if (info4.cans.empty()) { - for (size_t i = 0; i < 4; ++i) - info4.cans.push_back({}); + for (size_t i = 0; i < 4; ++i) info4.cans.push_back({}); info1.ams_type = AMSModel::N3S_AMS; info1.cans.push_back({}); } @@ -1496,7 +1491,7 @@ void ExtruderGroup::update_ams() hsizer_ams->AddStretchSpacer(1); if (btn_up->IsShown() && btn_down->IsShown()) { auto vsizer_btn = new wxBoxSizer(wxVERTICAL); - auto size = btn_up->GetSize(); + auto size = btn_up->GetSize(); vsizer_btn->Add(btn_up, 0); vsizer_btn->Add(btn_down, 0); hsizer_ams->Add(vsizer_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(2)); @@ -1510,16 +1505,16 @@ void ExtruderGroup::update_ams() sizer->Layout(); } -void ExtruderGroup::sync_ams(MachineObject const* obj, std::vector const& ams4, std::vector const& ams1) +void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector const &ams4, std::vector const &ams1) { - if (ams_4.empty() && ams4.empty() && ams_1.empty() && ams1.empty()) + if (ams_4.empty() && ams4.empty() + && ams_1.empty() && ams1.empty()) return; - auto sync = [obj](std::vector& infos, std::vector const& ams) -> bool { + auto sync = [obj](std::vector &infos, std::vector const &ams) -> bool { std::vector infos2; for (auto a : ams) { AMSinfo ams_info; - ams_info.parse_ams_info(const_cast(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), - obj->is_support_ams_humidity); + ams_info.parse_ams_info(const_cast(obj), a, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity); infos2.push_back(ams_info); } if (infos == infos2) @@ -1543,14 +1538,12 @@ bool Sidebar::priv::switch_diameter(bool single) if (single) { diameter = single_extruder->combo_diameter->GetValue(); } else { - auto diameter_left = left_extruder->combo_diameter->GetValue(); + auto diameter_left = left_extruder->combo_diameter->GetValue(); auto diameter_right = right_extruder->combo_diameter->GetValue(); if (diameter_left != diameter_right) { std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle); - auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::SentenceCase)); - auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::SentenceCase)); + auto left_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); + auto right_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::SentenceCase)); MessageDialog dlg(this->plater, _L("The software does not support using different diameter of nozzles for one print. " "If the left and right nozzles are inconsistent, we can only proceed with single-head printing. " @@ -1565,14 +1558,15 @@ bool Sidebar::priv::switch_diameter(bool single) diameter = diameter_right; else return false; - } else { + } + else { diameter = diameter_left; } } - + // ORCA: Check if the selected diameter matches the current nozzle diameter in the config Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); - auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); if (nozzle_diameter && nozzle_diameter->size() > 0) { auto current_nozzle_dia = get_diameter_string(nozzle_diameter->values[0]); // If the selected diameter is the same as current nozzle, don't switch profiles @@ -1580,8 +1574,8 @@ bool Sidebar::priv::switch_diameter(bool single) return true; } } - - auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); + + auto preset = wxGetApp().preset_bundle->get_similar_printer_preset({}, diameter.ToStdString()); if (preset == nullptr) { // ORCA add a text. this appears when user tries to change nozzle value but config doesnt have a inherited or compatible preset MessageDialog dlg(this->plater, _L("Configuration incompatible"), _L("Warning"), wxICON_WARNING | wxOK); @@ -1594,7 +1588,9 @@ bool Sidebar::priv::switch_diameter(bool single) static bool is_skip_high_flow_printer(const std::string& printer) { - static const std::set invalidate_list = {"Bambu Lab X1E"}; + static const std::set invalidate_list = { + "Bambu Lab X1E" + }; return invalidate_list.count(printer); }; @@ -1619,8 +1615,7 @@ static std::string serialize_nozzle_config(const std::map 0) - oss << ";"; + if (i > 0) oss << ";"; oss << std::fixed << std::setprecision(1) << deputy_nozzles[i].GetNozzleDiameter() << "," << static_cast(deputy_nozzles[i].GetNozzleFlowType()); } @@ -1628,8 +1623,7 @@ static std::string serialize_nozzle_config(const std::map 0) - oss << ";"; + if (i > 0) oss << ";"; oss << std::fixed << std::setprecision(1) << main_nozzles[i].GetNozzleDiameter() << "," << static_cast(main_nozzles[i].GetNozzleFlowType()); } @@ -1637,11 +1631,10 @@ static std::string serialize_nozzle_config(const std::map> deserialize_nozzle_config(const std::string& config_str) +static std::map> deserialize_nozzle_config(const std::string &config_str) { std::map> nozzle_cfg_map; - if (config_str.empty()) - return nozzle_cfg_map; + if (config_str.empty()) return nozzle_cfg_map; std::vector extruder_parts; boost::split(extruder_parts, config_str, boost::is_any_of("|")); @@ -1650,12 +1643,12 @@ static std::map> deserialize_nozzle_config(const std std::vector nozzles; std::vector parts; boost::split(parts, part_str, boost::is_any_of(";")); - for (const auto& part : parts) { + for (const auto &part : parts) { std::vector values; boost::split(values, part, boost::is_any_of(",")); if (values.size() == 2) { DevNozzle nozzle; - nozzle.m_diameter = std::stof(values[0]); + nozzle.m_diameter = std::stof(values[0]); nozzle.m_nozzle_flow = static_cast(std::stoi(values[1])); nozzles.push_back(nozzle); } @@ -1664,42 +1657,39 @@ static std::map> deserialize_nozzle_config(const std }; if (extruder_parts.size() != 2) { - auto nozzles = get_nozzles_from_string(config_str); - nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; - nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = {DevNozzle()}; + auto nozzles = get_nozzles_from_string(config_str); + nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; + nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = { DevNozzle() }; return nozzle_cfg_map; } if (!extruder_parts[0].empty()) { - auto nozzles = get_nozzles_from_string(extruder_parts[0]); + auto nozzles = get_nozzles_from_string(extruder_parts[0]); nozzle_cfg_map[DEPUTY_EXTRUDER_ID] = nozzles; } if (!extruder_parts[1].empty()) { - auto nozzles = get_nozzles_from_string(extruder_parts[1]); + auto nozzles = get_nozzles_from_string(extruder_parts[1]); nozzle_cfg_map[MAIN_EXTRUDER_ID] = nozzles; } return nozzle_cfg_map; } -static bool is_same_nozzle_config(const std::map>& config1, const std::map>& config2) +static bool is_same_nozzle_config(const std::map> &config1, const std::map> &config2) { - if (config1.size() != config2.size()) - return false; + if (config1.size() != config2.size()) return false; for (const auto& [eid, nozzles1] : config1) { auto it = config2.find(eid); - if (it == config2.end()) - return false; + if (it == config2.end()) return false; - const auto& nozzles2 = it->second; - if (nozzles1.size() != nozzles2.size()) - return false; + const auto &nozzles2 = it->second; + if (nozzles1.size() != nozzles2.size()) return false; auto sorted_nozzles1 = nozzles1; auto sorted_nozzles2 = nozzles2; - auto compare_nozzle = [](const DevNozzle& a, const DevNozzle& b) { + auto compare_nozzle = [](const DevNozzle &a, const DevNozzle &b) { float dia_a = a.GetNozzleDiameter(); float dia_b = b.GetNozzleDiameter(); if (std::abs(dia_a - dia_b) > EPSILON) { @@ -1722,22 +1712,19 @@ static bool is_same_nozzle_config(const std::map>& c return true; } -static std::string serialize_nozzle_option(const NozzleOption& option) -{ +static std::string serialize_nozzle_option(const NozzleOption& option) { std::ostringstream oss; oss << option.diameter << "|"; bool first = true; for (const auto& pair : option.extruder_nozzle_stats) { - if (!first) - oss << ";"; + if (!first) oss << ";"; first = false; oss << pair.first << ":"; bool first_stat = true; for (const auto& stat_pair : pair.second) { - if (!first_stat) - oss << ","; + if (!first_stat) oss << ","; first_stat = false; oss << static_cast(stat_pair.first) << "#" << stat_pair.second; } @@ -1745,15 +1732,12 @@ static std::string serialize_nozzle_option(const NozzleOption& option) return oss.str(); } -static std::optional deserialize_nozzle_option(const std::string& option_str) -{ - if (option_str.empty()) - return std::nullopt; +static std::optional deserialize_nozzle_option(const std::string& option_str) { + if (option_str.empty()) return std::nullopt; std::vector parts; boost::split(parts, option_str, boost::is_any_of("|")); - if (parts.size() != 2) - return std::nullopt; + if (parts.size() != 2) return std::nullopt; NozzleOption option; option.diameter = parts[0]; @@ -1762,13 +1746,11 @@ static std::optional deserialize_nozzle_option(const std::string& boost::split(extruder_parts, parts[1], boost::is_any_of(";")); for (const auto& extruder_part : extruder_parts) { - if (extruder_part.empty()) - continue; + if (extruder_part.empty()) continue; std::vector extruder_data; boost::split(extruder_data, extruder_part, boost::is_any_of(":")); - if (extruder_data.size() != 2) - continue; + if (extruder_data.size() != 2) continue; int extruder_id = std::stoi(extruder_data[0]); std::unordered_map stats; @@ -1781,8 +1763,8 @@ static std::optional deserialize_nozzle_option(const std::string& boost::split(kv, stat_part, boost::is_any_of("#")); if (kv.size() == 2) { NozzleVolumeType type = static_cast(std::stoi(kv[0])); - int count = std::stoi(kv[1]); - stats[type] = count; + int count = std::stoi(kv[1]); + stats[type] = count; } } @@ -1792,21 +1774,16 @@ static std::optional deserialize_nozzle_option(const std::string& return option; } -std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj, - int extruder_count, - bool support_multi_nozzle, - bool is_manual) +std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj, int extruder_count, bool support_multi_nozzle, bool is_manual) { if (extruder_count < 2 || !support_multi_nozzle) { return std::nullopt; } - if (!obj || !obj->GetNozzleSystem()) - return std::nullopt; + if (!obj || !obj->GetNozzleSystem()) return std::nullopt; auto nozzle_system = obj->GetNozzleSystem(); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - if (!preset_bundle) - return std::nullopt; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + if (!preset_bundle) return std::nullopt; std::string curr_dev_id = obj->get_dev_id(); std::map> curr_nozzle_cfg; @@ -1820,8 +1797,8 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj curr_nozzle_cfg[MAIN_EXTRUDER_ID].emplace_back(rack_nozzle.second); } - AppConfig* app_config = wxGetApp().app_config; - std::string saved_dev_id = app_config->get("sync_extruder", "dev_id"); + AppConfig *app_config = wxGetApp().app_config; + std::string saved_dev_id = app_config->get("sync_extruder", "dev_id"); std::string saved_nozzle_config_str = app_config->get("sync_extruder", "nozzle_config"); std::string saved_nozzle_option_str = app_config->get("sync_extruder", "nozzle_option"); std::optional nozzle_option; @@ -1833,20 +1810,20 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj if (!saved_dev_id.empty() && !saved_nozzle_config_str.empty() && !saved_nozzle_option_str.empty() && saved_dev_id == curr_dev_id) { auto saved_nozzle_config = deserialize_nozzle_config(saved_nozzle_config_str); if (is_same_nozzle_config(saved_nozzle_config, curr_nozzle_cfg)) { - nozzle_option = deserialize_nozzle_option(saved_nozzle_option_str); + nozzle_option = deserialize_nozzle_option(saved_nozzle_option_str); can_reuse_saved_option = nozzle_option.has_value(); } if (can_reuse_saved_option) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Reusing saved nozzle option for dev_id: " << curr_dev_id; - auto& project_config = preset_bundle->project_config; - ConfigOptionEnumsGeneric* nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); + auto &project_config = preset_bundle->project_config; + ConfigOptionEnumsGeneric *nozzle_volume_type_opt = project_config.option("nozzle_volume_type"); // Write to preset bundle config for (int extruder_id = 0; extruder_id < extruder_count; ++extruder_id) { NozzleVolumeType volume_type; - int nozzle_count; - bool clear_all = true; + int nozzle_count; + bool clear_all = true; if (!nozzle_option->extruder_nozzle_stats.count(extruder_id)) { nozzle_count = 0; @@ -1863,7 +1840,7 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj clear_all = false; } } else { - for (auto& stat : nozzle_option->extruder_nozzle_stats[extruder_id]) { + for (auto &stat : nozzle_option->extruder_nozzle_stats[extruder_id]) { volume_type = stat.first; nozzle_count = stat.second; setExtruderNozzleCount(preset_bundle, extruder_id, volume_type, nozzle_count, clear_all); @@ -1875,8 +1852,7 @@ std::optional Sidebar::priv::get_nozzle_options(MachineObject* obj // a manual flow switch, and refresh the sidebar badges. setNozzleStatsFromMachine(true); if (nozzle_volume_type_opt) { - for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volume_type_opt->values.size(); - ++extruder_id) + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volume_type_opt->values.size(); ++extruder_id) updateNozzleCountDisplay(preset_bundle, extruder_id, NozzleVolumeType(nozzle_volume_type_opt->values[extruder_id])); } } @@ -1930,7 +1906,7 @@ void Sidebar::priv::show_fila_switch_msg(bool ready) long style = ready ? (wxICON_INFORMATION | wxOK) : (wxICON_WARNING | wxOK); // Orca: drop the vendor "Learn more" tracking link; there is no Orca help page for the switch yet. - MessageDialog dlg(static_cast(wxGetApp().mainframe), msg, _L("Tips"), style); + MessageDialog dlg(static_cast(wxGetApp().mainframe), msg, _L("Tips"), style); dlg.CenterOnParent(); dlg.ShowModal(); } @@ -1980,12 +1956,12 @@ void Sidebar::priv::update_extruder_separator_icon(bool show, bool ready) // left_extruder->GetPosition() and the icon position live in the same coordinate space. wxPoint left_box_pos = left_extruder->GetPosition(); wxPoint ams_local_pos = left_extruder->hsizer_ams->GetPosition(); - wxSize left_size = left_extruder->sizer->GetSize(); - wxSize ams_size = left_extruder->hsizer_ams->GetSize(); - wxSize icon_size = extruder_separator_icon->GetSize(); - int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); - int center_x = left_size.GetWidth() + FromDIP(6); - int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2; + wxSize left_size = left_extruder->sizer->GetSize(); + wxSize ams_size = left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6); + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2; center_x -= icon_size.GetWidth() / 2; center_y -= icon_size.GetHeight() / 2; extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); @@ -2004,19 +1980,19 @@ void Sidebar::priv::update_extruder_separator_icon(bool show, bool ready) m_panel_printer_content->Refresh(); } -bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_manual) +bool Sidebar::priv::sync_extruder_list(bool &only_external_material, bool is_manual) { - MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); - auto printer_name = plater->get_selected_printer_name_in_combox(); + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); + auto printer_name = plater->get_selected_printer_name_in_combox(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " begin sync_extruder_list"; if (obj == nullptr) { plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); return false; } - // if (obj->get_extder_system()->extders.size() != 2) {//wxString(obj->get_preset_printer_model_name(machine_print_name)) - // plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::INCONSISTENT, _L("Sync printer - // information")); return false; - // } + //if (obj->get_extder_system()->extders.size() != 2) {//wxString(obj->get_preset_printer_model_name(machine_print_name)) + // plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::INCONSISTENT, _L("Sync printer information")); + // return false; + //} if (!plater->check_printer_initialized(obj)) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " check_printer_initialized fail"; @@ -2024,18 +2000,16 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man } std::string machine_print_name = obj->get_show_printer_type(); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); - Preset* machine_preset = get_printer_preset(obj); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + Preset* machine_preset = get_printer_preset(obj); if (!machine_preset) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: machine_preset empty"; return false; } if (machine_print_name != target_model_id) { - MessageDialog dlg(this->plater, - _L("The currently selected machine preset is inconsistent with the connected printer type.\n" - "Are you sure to continue syncing?"), - _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); + MessageDialog dlg(this->plater, _L("The currently selected machine preset is inconsistent with the connected printer type.\n" + "Are you sure to continue syncing?"), _L("Sync printer information"), wxICON_WARNING | wxYES | wxNO); if (dlg.ShowModal() == wxID_NO) { return false; } @@ -2049,11 +2023,11 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man }); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " go on sync_extruder_list"; - const Preset& cur_preset = preset_bundle->printers.get_selected_preset(); - int extruder_nums = preset_bundle->get_printer_extruder_count(); + const Preset &cur_preset = preset_bundle->printers.get_selected_preset(); + int extruder_nums = preset_bundle->get_printer_extruder_count(); std::vector extruder_map(extruder_nums); std::iota(extruder_map.begin(), extruder_map.end(), 0); - const ConfigOptionInts* physical_extruder_map = cur_preset.config.option("physical_extruder_map"); + const ConfigOptionInts *physical_extruder_map = cur_preset.config.option("physical_extruder_map"); if (physical_extruder_map != nullptr) { assert(physical_extruder_map->values.size() == extruder_nums); extruder_map = physical_extruder_map->values; @@ -2065,12 +2039,11 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man // value > 1 (nil-guarded, matching the manual/ToolOrdering gates), which no shipping single-nozzle or // dual-extruder profile sets. When the machine is multi-nozzle but no option resolves (e.g. user cancelled), // abort the sync. - const ConfigOptionIntsNullable* extruder_max_nozzle_count = cur_preset.config.option( - "extruder_max_nozzle_count"); + const ConfigOptionIntsNullable *extruder_max_nozzle_count = cur_preset.config.option("extruder_max_nozzle_count"); bool support_multi_nozzle = extruder_max_nozzle_count != nullptr && std::any_of(extruder_max_nozzle_count->values.begin(), extruder_max_nozzle_count->values.end(), [](int val) { return val > 1 && val != ConfigOptionIntsNullable::nil_value(); }); - auto nozzle_option = get_nozzle_options(obj, extruder_nums, support_multi_nozzle, is_manual); + auto nozzle_option = get_nozzle_options(obj, extruder_nums, support_multi_nozzle, is_manual); if (!nozzle_option && support_multi_nozzle) return false; @@ -2078,13 +2051,12 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man nozzle_diameters.resize(extruder_nums); std::vector target_types(extruder_nums, NozzleVolumeType::nvtStandard); for (size_t index = 0; index < extruder_nums; ++index) { - int extruder_id = extruder_map[index]; - nozzle_diameters[extruder_id] = nozzle_option ? atof(nozzle_option->diameter.c_str()) : - obj->GetExtderSystem()->GetNozzleDiameter(index); - NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; + int extruder_id = extruder_map[index]; + nozzle_diameters[extruder_id] = nozzle_option ? atof(nozzle_option->diameter.c_str()) : obj->GetExtderSystem()->GetNozzleDiameter(index); + NozzleVolumeType target_type = NozzleVolumeType::nvtStandard; std::optional select_type; if (nozzle_option && nozzle_option->extruder_nozzle_stats.count(index)) { - const auto& stats = nozzle_option->extruder_nozzle_stats[index]; + const auto &stats = nozzle_option->extruder_nozzle_stats[index]; if (stats.size() > 1) // The extruder holds nozzles of several flow types: select the mixed-flow mode. select_type = NozzleVolumeType::nvtHybrid; @@ -2093,8 +2065,7 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man } if (obj->is_nozzle_flow_type_supported()) { if (obj->GetExtderSystem()->GetNozzleFlowType(index) == NozzleFlowType::NONE_FLOWTYPE) { - MessageDialog dlg(this->plater, - _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), + MessageDialog dlg(this->plater, _L("There are unset nozzle types. Please set the nozzle types of all extruders before synchronizing."), _L("Sync extruder infomation"), wxICON_WARNING | wxOK); dlg.ShowModal(); continue; @@ -2122,29 +2093,23 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man auto switcher_pos = ams.second->GetSwitcherPos(); if (!switcher_pos) continue; - int switcher_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) : - static_cast(switcher_pos.value()); + int switcher_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) + : static_cast(switcher_pos.value()); if (extruder_id != switcher_id) continue; } const bool is_n3s = ams.second->GetAmsType() == DevAms::N3S; // Main (first) extruder is id 0, deputy is id 1. if (extruder_id == 0) { - if (is_n3s) - ++main_1; - else - ++main_4; + if (is_n3s) ++main_1; else ++main_4; } else if (extruder_id == 1) { - if (is_n3s) - ++deputy_1; - else - ++deputy_4; + if (is_n3s) ++deputy_1; else ++deputy_4; } } } only_external_material = !obj->GetFilaSystem()->HasAms(); - int main_index = obj->is_main_extruder_on_left() ? 0 : 1; - int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; + int main_index = obj->is_main_extruder_on_left() ? 0 : 1; + int deputy_index = obj->is_main_extruder_on_left() ? 1 : 0; if (extruder_nums > 1) { int left_index = left_extruder->combo_diameter->FindString(get_diameter_string(nozzle_diameters[0])); @@ -2169,7 +2134,7 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man } // set nozzle volume type after switching prset, so this value can override the old value stored in conf - auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); + auto printer_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER)); for (size_t idx = 0; idx < target_types.size(); ++idx) { printer_tab->set_extruder_volume_type(idx, target_types[idx]); } @@ -2184,17 +2149,17 @@ bool Sidebar::priv::sync_extruder_list(bool& only_external_material, bool is_man // Copy the live switch state into the project so slicing and the send dialog honor it. Both // resolve to false unless the switch is installed and calibrated, so nothing changes without one. - auto& project_config = wxGetApp().preset_bundle->project_config; - if (auto* dynamic_filament = project_config.opt("enable_filament_dynamic_map")) + auto &project_config = wxGetApp().preset_bundle->project_config; + if (auto *dynamic_filament = project_config.opt("enable_filament_dynamic_map")) dynamic_filament->value = is_fila_switch_ready(); - if (auto* has_switcher = project_config.opt("has_filament_switcher")) + if (auto *has_switcher = project_config.opt("has_filament_switcher")) has_switcher->value = is_fila_switch_ready(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " finish sync_extruder_list"; return true; } -void Sidebar::priv::update_sync_status(const MachineObject* obj) +void Sidebar::priv::update_sync_status(const MachineObject *obj) { StateColor not_synced_colour(std::pair(wxColour("#009688"), StateColor::Normal)); auto clear_all_sync_status = [this]() { @@ -2208,8 +2173,8 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) single_extruder->ShowBadge(false); single_extruder->sync_ams(nullptr, {}, {}); update_extruder_separator_icon(false, false); - // btn_sync_printer->SetBorderColor(not_synced_colour); - // btn_sync_printer->SetIcon("printer_sync"); + //btn_sync_printer->SetBorderColor(not_synced_colour); + //btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); }; @@ -2228,7 +2193,7 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) fila_switch_warning_shown = false; } - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (!preset_bundle) { clear_all_sync_status(); return; @@ -2236,7 +2201,7 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) bool printer_synced = false; // 1. update printer status - const Preset& cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); + const Preset &cur_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { panel_printer_preset->ShowBadge(true); printer_synced = true; @@ -2252,23 +2217,25 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) struct ExtruderInfo { float diameter{0.4}; - // int nozzle_volue_type{0}; - int ams_4{0}; - int ams_1{0}; - std::vector ams_v4; - std::vector ams_v1; + //int nozzle_volue_type{0}; + int ams_4{0}; + int ams_1{0}; + std::vector ams_v4; + std::vector ams_v1; - bool operator==(const ExtruderInfo& other) const + bool operator==(const ExtruderInfo &other) const { - return abs(diameter - other.diameter) < EPSILON && /*nozzle_volue_type == other.nozzle_volue_type - &&*/ ams_4 == other.ams_4 && ams_1 == other.ams_1; + return abs(diameter - other.diameter) < EPSILON + && /*nozzle_volue_type == other.nozzle_volue_type + &&*/ ams_4 == other.ams_4 + && ams_1 == other.ams_1; } }; - auto is_same_nozzle_info = [obj](const ExtruderInfo& left, const ExtruderInfo& right) { + auto is_same_nozzle_info = [obj](const ExtruderInfo &left, const ExtruderInfo &right) { bool is_same_nozzle_type = true; if (obj->is_nozzle_flow_type_supported()) - is_same_nozzle_type = true; // left.nozzle_volue_type == right.nozzle_volue_type; // TODO: Orca hack + is_same_nozzle_type = true;//left.nozzle_volue_type == right.nozzle_volue_type; // TODO: Orca hack return abs(left.diameter - right.diameter) < EPSILON && is_same_nozzle_type; }; @@ -2293,11 +2260,10 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) } std::vector extruder_infos(extruder_nums); - std::vector nozzle_volume_types = - wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; - // for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { - // extruder_infos[i].nozzle_volue_type = nozzle_volume_types[i]; - // } + std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; + //for (size_t i = 0; i < nozzle_volume_types.size(); ++i) { + // extruder_infos[i].nozzle_volue_type = nozzle_volume_types[i]; + //} std::vector> extruder_ams_counts = wxGetApp().preset_bundle->extruder_ams_counts; if (extruder_ams_counts.size() >= extruder_nums) { @@ -2315,11 +2281,12 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) double value = 0.0; single_extruder->diameter.ToDouble(&value); extruder_infos[0].diameter = float(value); - } else if (extruder_nums == 2) { + } + else if(extruder_nums == 2){ double value = 0.0; left_extruder->diameter.ToDouble(&value); extruder_infos[0].diameter = float(value); - + value = 0.0; right_extruder->diameter.ToDouble(&value); extruder_infos[1].diameter = float(value); @@ -2328,11 +2295,11 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) std::vector machine_extruder_infos(obj->GetExtderSystem()->GetTotalExtderCount()); const auto& extruders = obj->GetExtderSystem()->GetExtruders(); - for (const DevExtder& extruder : extruders) { - // machine_extruder_infos[extruder.GetExtId()].nozzle_volue_type = int(extruder.GetNozzleFlowType()) - 1; - machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); + for (const DevExtder &extruder : extruders) { + //machine_extruder_infos[extruder.GetExtId()].nozzle_volue_type = int(extruder.GetNozzleFlowType()) - 1; + machine_extruder_infos[extruder.GetExtId()].diameter = extruder.GetNozzleDiameter(); } - for (auto& item : obj->GetFilaSystem()->GetAmsList()) { + for (auto &item : obj->GetFilaSystem()->GetAmsList()) { int extruder_id; // With the switch ready every AMS feeds both extruders, so attribute each to the extruder its // input track feeds. Without a switch the binding is a single extruder @@ -2342,10 +2309,10 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) auto switcher_pos = item.second->GetSwitcherPos(); if (!switcher_pos) continue; - extruder_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) : - static_cast(switcher_pos.value()); + extruder_id = obj->is_main_extruder_on_left() ? (1 - static_cast(switcher_pos.value())) + : static_cast(switcher_pos.value()); } else { - const auto& uniq_extruder_id = item.second->GetUniqueBindedExtruderId(); + const auto &uniq_extruder_id = item.second->GetUniqueBindedExtruderId(); if (!uniq_extruder_id) continue; extruder_id = uniq_extruder_id.value(); @@ -2354,7 +2321,8 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) if (extruder_id >= machine_extruder_infos.size()) continue; - if (item.second->GetAmsType() == DevAms::N3S) { // N3S + if (item.second->GetAmsType() == DevAms::N3S) + { // N3S machine_extruder_infos[extruder_id].ams_1++; machine_extruder_infos[extruder_id].ams_v1.push_back(item.second); } else { @@ -2372,17 +2340,20 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) panel_nozzle_dia->ShowBadge(true); // ORCA add support for nozzle sync single_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); extruder_synced[0] = true; - } else { + } + else { single_extruder->ShowBadge(false); panel_nozzle_dia->ShowBadge(false); // ORCA add support for nozzle sync single_extruder->sync_ams(obj, {}, {}); } - } else if (extruder_nums == 2) { + } + else if (extruder_nums == 2) { if (extruder_infos[0] == machine_extruder_infos[0]) { left_extruder->ShowBadge(true); left_extruder->sync_ams(obj, machine_extruder_infos[0].ams_v4, machine_extruder_infos[0].ams_v1); extruder_synced[0] = true; - } else { + } + else { left_extruder->ShowBadge(false); left_extruder->sync_ams(obj, {}, {}); } @@ -2391,7 +2362,8 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) right_extruder->ShowBadge(true); right_extruder->sync_ams(obj, machine_extruder_infos[1].ams_v4, machine_extruder_infos[1].ams_v1); extruder_synced[1] = true; - } else { + } + else { right_extruder->ShowBadge(false); right_extruder->sync_ams(obj, {}, {}); } @@ -2400,12 +2372,13 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) StateColor synced_colour(std::pair(wxColour("#CECECE"), StateColor::Normal)); bool all_extruder_synced = std::all_of(extruder_synced.begin(), extruder_synced.end(), [](bool value) { return value; }); if (printer_synced && all_extruder_synced) { - // btn_sync_printer->SetBorderColor(synced_colour); - // btn_sync_printer->SetIcon("ams_nozzle_sync"); + // btn_sync_printer->SetBorderColor(synced_colour); + // btn_sync_printer->SetIcon("ams_nozzle_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_ok"); - } else { - // btn_sync_printer->SetBorderColor(not_synced_colour); - // btn_sync_printer->SetIcon("printer_sync"); + } + else { + // btn_sync_printer->SetBorderColor(not_synced_colour); + // btn_sync_printer->SetIcon("printer_sync"); m_printer_bbl_sync->SetBitmap_("printer_sync_not"); } @@ -2418,24 +2391,25 @@ void Sidebar::priv::update_sync_status(const MachineObject* obj) last_filament_ams_list = wxGetApp().preset_bundle->filament_ams_list; const auto print_tech = wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology(); if (print_tech == ptFFF && !is_same_ams_list) { - for (PlaterPresetComboBox* cb : combos_filament) + for (PlaterPresetComboBox *cb : combos_filament) cb->update(); } } -} + } -void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent& e) -{ - if (m_last_slice_state != p->plater->is_background_process_slicing()) { - m_last_slice_state = p->plater->is_background_process_slicing(); - // btn_sync->Enable(!m_last_slice_state); - p->m_printer_bbl_sync->Enable(!m_last_slice_state); - ams_btn->Enable(!m_last_slice_state); - Refresh(); - } -} +void Sidebar::update_sync_ams_btn_enable(wxUpdateUIEvent &e) + { + if (m_last_slice_state != p->plater->is_background_process_slicing()) { + m_last_slice_state = p->plater->is_background_process_slicing(); + //btn_sync->Enable(!m_last_slice_state); + p->m_printer_bbl_sync->Enable(!m_last_slice_state); + ams_btn->Enable(!m_last_slice_state); + Refresh(); + } + } -Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) +Sidebar::Sidebar(Plater *parent) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(39 * wxGetApp().em_unit(), -1)), p(new priv(parent)) { Choice::register_dynamic_list("support_filament", &dynamic_physical_filament_list); Choice::register_dynamic_list("support_interface_filament", &dynamic_physical_filament_list); @@ -2452,9 +2426,10 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // but this cause the bad layout of the sidebar, when all infoboxes appear. // As a result we can see the empty block at the bottom of the sidebar // But if we set this value to 5, layout will be better - // p->scrolled->SetScrollRate(0, 5); + //p->scrolled->SetScrollRate(0, 5); p->scrolled->SetBackgroundColour(*wxWHITE); + SetFont(wxGetApp().normal_font()); #ifndef __APPLE__ #ifdef _WIN32 @@ -2466,14 +2441,14 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, #endif int em = wxGetApp().em_unit(); - // BBS refine layout and styles - // Sizer in the scrolled area + //BBS refine layout and styles + // Sizer in the scrolled area auto* scrolled_sizer = m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); p->scrolled->SetSizer(scrolled_sizer); - wxColour title_bg = wxColour(248, 248, 248); - wxColour inactive_text = wxColour(86, 86, 86); - wxColour active_text = wxColour(0, 0, 0); + wxColour title_bg = wxColour(248, 248, 248); + wxColour inactive_text = wxColour(86, 86, 86); + wxColour active_text = wxColour(0, 0, 0); wxColour static_line_col = wxColour(166, 169, 170); #ifdef __WINDOWS__ @@ -2488,7 +2463,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->m_panel_printer_title->SetBackgroundColor(title_bg); p->m_panel_printer_title->SetBackgroundColor2(0xF1F1F1); - p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); + p->m_printer_icon = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer"); p->m_text_printer_settings = new Label(p->m_panel_printer_title, _L("Printer"), LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END); p->m_printer_icon->Bind(wxEVT_BUTTON, [](wxCommandEvent& e) { @@ -2499,7 +2474,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // ORCA use connect button on titlebar p->m_printer_connect = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "monitor_signal_strong"); p->m_printer_connect->SetToolTip(_L("Connection")); - p->m_printer_connect->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { + p->m_printer_connect->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { PhysicalPrinterDialog dlg(this->GetParent()); dlg.ShowModal(); }); @@ -2507,7 +2482,9 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // ORCA use sync button on titlebar p->m_printer_bbl_sync = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "printer_sync_not"); p->m_printer_bbl_sync->SetToolTip(_L("Synchronize nozzle information and the number of AMS")); - p->m_printer_bbl_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { deal_btn_sync(); }); + p->m_printer_bbl_sync->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + deal_btn_sync(); + }); p->m_printer_setting = new ScalableButton(p->m_panel_printer_title, wxID_ANY, "settings"); p->m_printer_setting->Bind(wxEVT_BUTTON, [](wxCommandEvent &e) { @@ -2515,17 +2492,15 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // wxGetApp().params_dialog()->Popup(); // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_PRINTERS); - }); + }); wxBoxSizer* h_sizer_title = new wxBoxSizer(wxHORIZONTAL); h_sizer_title->Add(p->m_printer_icon, 0, wxALIGN_CENTRE | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); h_sizer_title->AddSpacer(FromDIP(SidebarProps::ElementSpacing())); h_sizer_title->Add(p->m_text_printer_settings, 1, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); - // h_sizer_title->AddStretchSpacer(); - h_sizer_title->Add(p->m_printer_connect, 0, wxALIGN_CENTER | wxRIGHT, - FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks - h_sizer_title->Add(p->m_printer_bbl_sync, 0, wxALIGN_CENTER | wxRIGHT, - FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + //h_sizer_title->AddStretchSpacer(); + h_sizer_title->Add(p->m_printer_connect , 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks + h_sizer_title->Add(p->m_printer_bbl_sync, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(SidebarProps::WideSpacing())); // used larger margin to prevent accidental clicks h_sizer_title->Add(p->m_printer_setting, 0, wxALIGN_CENTER); h_sizer_title->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); h_sizer_title->SetMinSize(-1, 3 * em); @@ -2537,20 +2512,19 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // add printer title scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0); - p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this](auto& e) { + p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) { if (!p || !p->combo_printer || !p->m_text_printer_settings || !p->m_panel_printer_content || !m_scrolled_sizer) return; // ORCA Show printer name on title when its folded to inform user without expanding it again - bool isShown = p->m_panel_printer_content->IsShown(); - wxString title = _L("Printer") + wxString(!isShown ? "" : (" | " + p->combo_printer->GetValue())); + bool isShown = p->m_panel_printer_content->IsShown(); + wxString title = _L("Printer") + wxString(!isShown ? "" : (" | " + p->combo_printer->GetValue())); p->m_text_printer_settings->SetLabel(title); p->m_panel_printer_content->Show(!isShown); p->m_panel_printer_separator->Show(isShown); m_scrolled_sizer->Layout(); }); // ORCA add bottom border for seperation wile sections folded - p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, - wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string + p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string p->m_panel_printer_separator->SetBackgroundColour("#FFFFFF"); scrolled_sizer->Add(p->m_panel_printer_separator, 0, wxEXPAND); @@ -2559,8 +2533,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->m_panel_printer_content = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); p->m_panel_printer_content->SetBackgroundColour(wxColour(255, 255, 255)); - struct PanelColors - { + struct PanelColors { wxColour bg_normal = "#FFFFFF"; wxColour bg_focus = "#E5F0EE"; wxColour bd_normal = "#DBDBDB"; @@ -2573,17 +2546,18 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->panel_printer_preset->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); p->panel_printer_preset->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_printer_preset->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_printer->wxEvtHandler::ProcessEvent(evt); }); + p->panel_printer_preset->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { + p->combo_printer->wxEvtHandler::ProcessEvent(evt); + }); // ORCA Hide Cover automatically if there is not enough space - p->panel_printer_preset->Bind(wxEVT_SIZE, [this](auto& e) { + p->panel_printer_preset->Bind(wxEVT_SIZE, [this](auto & e) { auto current_width = e.GetSize().GetWidth(); auto narrow_width = FromDIP(235); - auto label_width = p->combo_printer->GetTextExtent(p->combo_printer->GetStringSelection()).GetWidth(); - auto min_width = label_width + FromDIP(25 + PRINTER_PANEL_SIZE.GetWidth()); - if (((min_width < narrow_width && min_width > current_width) || (current_width < narrow_width && min_width > narrow_width)) && - p->image_printer->IsShown()) + auto label_width = p->combo_printer->GetTextExtent(p->combo_printer->GetStringSelection()).GetWidth(); + auto min_width = label_width + FromDIP(25 + PRINTER_PANEL_SIZE.GetWidth()); + if(((min_width < narrow_width && min_width > current_width) || (current_width < narrow_width && min_width > narrow_width)) && p->image_printer->IsShown()) p->image_printer->Hide(); - else if ((current_width > min_width || !(current_width < narrow_width)) && !p->image_printer->IsShown()) + else if((current_width > min_width || !(current_width < narrow_width)) && !p->image_printer->IsShown()) p->image_printer->Show(); e.Skip(); }); @@ -2594,18 +2568,16 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->btn_edit_printer = new ScalableButton(p->panel_printer_preset, wxID_ANY, "edit"); p->btn_edit_printer->SetToolTip(_L("Click to edit preset")); p->btn_edit_printer->Hide(); // hide for first launch - p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color, printer_preset_hovered](wxCommandEvent) { + p->btn_edit_printer->Bind(wxEVT_BUTTON, [this, panel_color, printer_preset_hovered](wxCommandEvent){ p->editing_filament = -1; if (p->combo_printer->switch_to_tab()) p->editing_filament = 0; - // ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because - // callbacks are not re-entrant, leaving widgets in an inconsistent state + // ORCA: FIX crash on wxGTK, directly modifying UI (self->Hide() / parent->Layout()) inside a button event can crash because callbacks are not re-entrant, leaving widgets in an inconsistent state wxGetApp().CallAfter([this, panel_color, printer_preset_hovered]() { - // ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked - // colors when opening printer settings + // ORCA clicking edit button not triggers wxEVT_KILL_FOCUS wxEVT_LEAVE_WINDOW make changes manually to prevent stucked colors when opening printer settings if (!p || !p->panel_printer_preset || !p->btn_edit_printer) return; - p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); + p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); printer_preset_hovered->clear(); p->btn_edit_printer->Hide(); p->panel_printer_preset->Layout(); @@ -2613,15 +2585,16 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, }); ScalableBitmap bitmap_printer(p->panel_printer_preset, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); - p->image_printer = new wxStaticBitmap(p->panel_printer_preset, wxID_ANY, bitmap_printer.bmp(), wxDefaultPosition, - FromDIP(PRINTER_THUMBNAIL_SIZE), 0); - p->image_printer->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_printer->wxEvtHandler::ProcessEvent(evt); }); + p->image_printer = new wxStaticBitmap(p->panel_printer_preset, wxID_ANY, bitmap_printer.bmp(), wxDefaultPosition, FromDIP(PRINTER_THUMBNAIL_SIZE), 0); + p->image_printer->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { + p->combo_printer->wxEvtHandler::ProcessEvent(evt); + }); p->combo_printer = new PlaterPresetComboBox(p->panel_printer_preset, Preset::TYPE_PRINTER); p->combo_printer->SetBorderWidth(0); p->combo_printer->SetMaxSize(wxSize(-1, FromDIP(30))); // limiting height makes badge visible // ORCA paint whole combobox on focus - auto printer_focus_bg = [this, panel_color](bool focused) { + auto printer_focus_bg = [this, panel_color](bool focused){ auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_printer_preset->SetBackgroundColor(bg_color); p->panel_printer_preset->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); @@ -2643,15 +2616,14 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, }); */ // ORCA use Show/Hide to gain text area instead using blank icon. also manages hover effect for border - for (wxWindow* w : - std::initializer_list{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent& e) { + for (wxWindow *w : std::initializer_list{p->panel_printer_preset, p->btn_edit_printer, p->image_printer, p->combo_printer}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) { printer_preset_hovered->insert(w); - if (!p->combo_printer->HasFocus()) + if(!p->combo_printer->HasFocus()) p->panel_printer_preset->SetBorderColor(panel_color.bd_hover); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent& e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_preset_hovered](wxMouseEvent &e) { printer_preset_hovered->erase(w); if (printer_preset_hovered->empty() && !p->combo_printer->HasFocus()) p->panel_printer_preset->SetBorderColor(panel_color.bd_normal); @@ -2661,7 +2633,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // Perform show/hide in wxEVT_IDLE after enter/leave events have settled. // This prevents the extraneous enter/leave events generated by the // layout change itself from causing a feedback loop. - Bind(wxEVT_IDLE, [this, printer_preset_hovered](wxIdleEvent& e) { + Bind(wxEVT_IDLE, [this, printer_preset_hovered](wxIdleEvent &e) { auto hovered = !printer_preset_hovered->empty(); if (p->btn_edit_printer->IsShown() != hovered) { if (hovered) { @@ -2678,18 +2650,19 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->panel_nozzle_dia->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); p->panel_nozzle_dia->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_nozzle_dia->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); }); + p->panel_nozzle_dia->Bind(wxEVT_LEFT_DOWN, [this](auto & evt) { + p->combo_nozzle_dia->wxEvtHandler::ProcessEvent(evt); + }); p->label_nozzle_title = new Label(p->panel_nozzle_dia, _L("Nozzle"), LB_PROPAGATE_MOUSE_EVENT); p->label_nozzle_title->SetFont(Label::Body_10); - p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, - wxCB_READONLY); + p->combo_nozzle_dia = new ComboBox(p->panel_nozzle_dia, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); p->combo_nozzle_dia->SetBorderWidth(0); p->combo_nozzle_dia->GetDropDown().SetUseContentWidth(true); p->combo_nozzle_dia->SetMinSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // requires a static value in here p->combo_nozzle_dia->SetMaxSize(FromDIP(wxSize(PRINTER_PANEL_SIZE.GetWidth() - 4, 26))); // using -1 with wxEXPAND has issues - p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto& e) { + p->combo_nozzle_dia->Bind(wxEVT_COMBOBOX, [this](auto &e) { auto evt_combo = (*p->single_extruder).combo_diameter; evt_combo->SetSelection(e.GetSelection()); wxCommandEvent evt(wxEVT_COMBOBOX, evt_combo->GetId()); @@ -2699,7 +2672,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, e.Skip(); }); // ORCA paint whole combobox on focus - auto nozzle_focus_bg = [this, panel_color](bool focused) { + auto nozzle_focus_bg = [this, panel_color](bool focused){ auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_nozzle_dia->SetBackgroundColor(bg_color); p->panel_nozzle_dia->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); @@ -2710,23 +2683,21 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->combo_nozzle_dia->Bind(wxEVT_SET_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(true ); e.Skip();}); p->combo_nozzle_dia->Bind(wxEVT_KILL_FOCUS, [nozzle_focus_bg](auto& e) {nozzle_focus_bg(false); e.Skip();}); - p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", - LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); + p->label_nozzle_type = new Label(p->panel_nozzle_dia, "Brass", LB_PROPAGATE_MOUSE_EVENT | wxST_ELLIPSIZE_END | wxALIGN_CENTRE_HORIZONTAL); p->label_nozzle_type->SetFont(Label::Body_10); p->label_nozzle_type->SetMinSize(FromDIP(wxSize(56, -1))); p->label_nozzle_type->SetMaxSize(FromDIP(wxSize(56, -1))); // highlight border on hover auto nozzle_dia_hovered = std::make_shared>(); - for (wxWindow* w : - std::initializer_list{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent& e) { + for (wxWindow *w : std::initializer_list{p->panel_nozzle_dia, p->label_nozzle_title, p->label_nozzle_type, p->combo_nozzle_dia}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) { nozzle_dia_hovered->insert(w); - if (!p->combo_nozzle_dia->HasFocus()) + if(!p->combo_nozzle_dia->HasFocus()) p->panel_nozzle_dia->SetBorderColor(panel_color.bd_hover); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent& e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, nozzle_dia_hovered](wxMouseEvent &e) { nozzle_dia_hovered->erase(w); if (nozzle_dia_hovered->empty() && !p->combo_nozzle_dia->HasFocus()) p->panel_nozzle_dia->SetBorderColor(panel_color.bd_normal); @@ -2734,10 +2705,10 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, }); } - wxGridSizer* nozzle_dia_sizer = new wxGridSizer(3, 1, FromDIP(2), 0); - nozzle_dia_sizer->Add(p->label_nozzle_title, 1, wxALIGN_CENTER | wxTOP, FromDIP(2)); - nozzle_dia_sizer->Add(p->combo_nozzle_dia, 0, wxALIGN_CENTER); - nozzle_dia_sizer->Add(p->label_nozzle_type, 1, wxALIGN_CENTER | wxBOTTOM, FromDIP(1)); + wxGridSizer *nozzle_dia_sizer = new wxGridSizer(3, 1, FromDIP(2), 0); + nozzle_dia_sizer->Add(p->label_nozzle_title, 1, wxALIGN_CENTER | wxTOP , FromDIP(2)); + nozzle_dia_sizer->Add(p->combo_nozzle_dia , 0, wxALIGN_CENTER); + nozzle_dia_sizer->Add(p->label_nozzle_type , 1, wxALIGN_CENTER | wxBOTTOM, FromDIP(1)); p->panel_nozzle_dia->SetSizer(nozzle_dia_sizer); @@ -2746,39 +2717,38 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); - p->panel_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { + p->panel_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { on_leave_image_printer_bed(evt); p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); }); - // ScalableButton *wiki_bed = new ScalableButton(p->panel_printer_bed, wxID_ANY, "help"); - // wiki_bed->Bind(wxEVT_BUTTON, [](wxCommandEvent) { - // wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); - // }); + //ScalableButton *wiki_bed = new ScalableButton(p->panel_printer_bed, wxID_ANY, "help"); + //wiki_bed->Bind(wxEVT_BUTTON, [](wxCommandEvent) { + // wxLaunchDefaultBrowser("https://wiki.bambulab.com/en/x1/manual/compatibility-and-parameter-settings-of-filaments"); + //}); ScalableBitmap bitmap_bed(p->panel_printer_bed, "printer_placeholder", PRINTER_THUMBNAIL_SIZE.GetHeight()); p->image_printer_bed = new wxStaticBitmap(p->panel_printer_bed, wxID_ANY, bitmap_bed.bmp(), wxDefaultPosition, wxDefaultSize, 0); - p->image_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto& evt) { + p->image_printer_bed->Bind(wxEVT_LEFT_DOWN, [this](auto &evt) { on_leave_image_printer_bed(evt); p->combo_printer_bed->wxEvtHandler::ProcessEvent(evt); }); - p->combo_printer_bed = new ComboBox(p->panel_printer_bed, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, - wxCB_READONLY); + p->combo_printer_bed = new ComboBox(p->panel_printer_bed, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY); p->combo_printer_bed->SetBorderWidth(0); p->combo_printer_bed->GetDropDown().SetUseContentWidth(true); - p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow - p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow reset_bed_type_combox_choices(true); - p->combo_printer_bed->Bind(wxEVT_COMBOBOX, [this](auto& e) { - auto image_path = get_cur_select_bed_image(); + p->combo_printer_bed->Bind(wxEVT_COMBOBOX, [this](auto &e) { + auto image_path = get_cur_select_bed_image(); p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); e.Skip(); }); // ORCA paint whole combobox on focus - auto bed_focus_bg = [this, panel_color](bool focused) { + auto bed_focus_bg = [this, panel_color](bool focused){ auto bg_color = StateColor::darkModeColorFor(focused ? panel_color.bg_focus : panel_color.bg_normal); p->panel_printer_bed->SetBackgroundColor(bg_color); p->panel_printer_bed->SetBorderColor(focused ? panel_color.bd_focus : panel_color.bd_normal); @@ -2790,34 +2760,34 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, // highlight border on hover auto printer_bed_hovered = std::make_shared>(); - for (wxWindow* w : std::initializer_list{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) { - w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent& e) { + for (wxWindow *w : std::initializer_list{p->panel_printer_bed, p->image_printer_bed, p->combo_printer_bed}) { + w->Bind(wxEVT_ENTER_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) { printer_bed_hovered->insert(w); - if (!p->combo_printer_bed->HasFocus()) + if(!p->combo_printer_bed->HasFocus()) p->panel_printer_bed->SetBorderColor(panel_color.bd_hover); - if (w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open + if(w == p->image_printer_bed && !p->combo_printer_bed->is_drop_down()) // dont trigger while combo open on_enter_image_printer_bed(e); e.Skip(); }); - w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent& e) { + w->Bind(wxEVT_LEAVE_WINDOW, [this, w, panel_color, printer_bed_hovered](wxMouseEvent &e) { printer_bed_hovered->erase(w); if (printer_bed_hovered->empty() && !p->combo_printer_bed->HasFocus()) p->panel_printer_bed->SetBorderColor(panel_color.bd_normal); - if (w == p->image_printer_bed) + if(w == p->image_printer_bed) on_leave_image_printer_bed(e); e.Skip(); }); } - wxBoxSizer* bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *bed_type_sizer = new wxBoxSizer(wxHORIZONTAL); bed_type_sizer->Add(p->combo_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); bed_type_sizer->Add(p->image_printer_bed, 0, wxALL | wxALIGN_CENTER_VERTICAL, FromDIP(2)); p->panel_printer_bed->SetSizer(bed_type_sizer); - AppConfig* app_config = wxGetApp().app_config; + AppConfig *app_config = wxGetApp().app_config; std::string str_bed_type = app_config->get("curr_bed_type"); - int bed_type_value = atoi(str_bed_type.c_str()); + int bed_type_value = atoi(str_bed_type.c_str()); // hotfix: btDefault is added as the first one in BedType, and app_config should not be btDefault if (bed_type_value == 0) { app_config->set("curr_bed_type", "1"); @@ -2834,7 +2804,7 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, if (item.first == str_bed_type) bed_type = (BedType)item.second; }*/ - BedType bed_type = (BedType) bed_type_value; + BedType bed_type = (BedType)bed_type_value; project_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); /* ORCA THIS PART MOVED TO TITLEBAR @@ -2864,10 +2834,13 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, }); p->btn_sync_printer = btn_sync; */ - p->timer_sync_printer->Bind(wxEVT_TIMER, [this](wxTimerEvent& e) { p->flush_printer_sync(); }); + p->timer_sync_printer->Bind(wxEVT_TIMER, [this] (wxTimerEvent & e) { + p->flush_printer_sync(); + }); + - p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); - p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); + p->left_extruder = new ExtruderGroup(p->m_panel_printer_content, 0, _L("Left Nozzle")); + p->right_extruder = new ExtruderGroup(p->m_panel_printer_content, 1, _L("Right Nozzle")); p->single_extruder = new ExtruderGroup(p->m_panel_printer_content, -1, _L("Nozzle")); // manuallySetNozzleCount refreshes the badge and the plater itself; it is a no-op unless the // printer has a multi-nozzle extruder (and the edit button is only enabled then, see @@ -2877,24 +2850,24 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->single_extruder->SetEditEnabled(false); // Orca: keep the floating switcher icon aligned with the left extruder's AMS row when the // extruder card is resized (the overlay is absolutely positioned, not managed by a sizer). - p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent& evt) { + p->left_extruder->Bind(wxEVT_SIZE, [this](wxSizeEvent &evt) { if (p->extruder_separator_icon && p->extruder_separator_icon->IsShown()) { wxPoint left_box_pos = p->left_extruder->GetPosition(); wxPoint ams_local_pos = p->left_extruder->hsizer_ams->GetPosition(); - wxSize left_size = p->left_extruder->sizer->GetSize(); - wxSize ams_size = p->left_extruder->hsizer_ams->GetSize(); - wxSize icon_size = p->extruder_separator_icon->GetSize(); - int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); - int center_x = left_size.GetWidth() + FromDIP(6) - icon_size.GetWidth() / 2; - int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2 - icon_size.GetHeight() / 2; + wxSize left_size = p->left_extruder->sizer->GetSize(); + wxSize ams_size = p->left_extruder->hsizer_ams->GetSize(); + wxSize icon_size = p->extruder_separator_icon->GetSize(); + int ams_abs_y = left_box_pos.y + ams_local_pos.y + FromDIP(4); + int center_x = left_size.GetWidth() + FromDIP(6) - icon_size.GetWidth() / 2; + int center_y = ams_abs_y + (ams_size.GetHeight() - icon_size.GetHeight()) / 2 - icon_size.GetHeight() / 2; p->extruder_separator_icon->SetPosition(wxPoint(center_x, center_y)); if (p->m_panel_printer_content) p->m_panel_printer_content->Refresh(); } evt.Skip(); }); - auto switch_diameter = [this](wxCommandEvent& evt) { - auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); + auto switch_diameter = [this](wxCommandEvent & evt) { + auto extruder = dynamic_cast(dynamic_cast(evt.GetEventObject())->GetParent()); p->is_switching_diameter = true; p->switch_diameter(extruder == p->single_extruder); p->is_switching_diameter = false; @@ -2911,394 +2884,379 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, } { - // Orca: Sidebar - Filament titlebar UI - // add filament title - p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); - p->m_panel_filament_title->SetBackgroundColor(title_bg); - p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); - p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) { - if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || - !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) - return; - // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament - // button also block fold/unfold feature when user clicks to spacing between icons - int exclude_pt = p->m_bpButton_set_filament->GetPosition().x; // maximum fixed item - if (p->m_purge_mode_btn->IsShown()) - exclude_pt = p->m_purge_mode_btn->GetPosition().x; - else if (p->m_flushing_volume_btn->IsShown()) - exclude_pt = p->m_flushing_volume_btn->GetPosition().x; - else if (p->m_bpButton_add_filament->IsShown()) - exclude_pt = p->m_bpButton_add_filament->GetPosition().x - FromDIP(30); // reserve spacing for delete button - else if (ams_btn->IsShown()) - exclude_pt = ams_btn->GetPosition().x; - if (e.GetPosition().x > exclude_pt) - return; - bool isShown = p->m_filament_area_wrapper->IsShown(); - p->m_filament_area_wrapper->Show(!isShown); - p->m_panel_filament_separator->Show(isShown); - m_scrolled_sizer->Layout(); - CallAfter([this] { update_filaments_counter(true); }); // call after all UI processing done - }); + // Orca: Sidebar - Filament titlebar UI + // add filament title + p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE); + p->m_panel_filament_title->SetBackgroundColor(title_bg); + p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1); + p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) { + if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn) + return; + // ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button + // also block fold/unfold feature when user clicks to spacing between icons + int exclude_pt = p->m_bpButton_set_filament->GetPosition().x; // maximum fixed item + if (p->m_purge_mode_btn->IsShown()) exclude_pt = p->m_purge_mode_btn->GetPosition().x; + else if (p->m_flushing_volume_btn->IsShown()) exclude_pt = p->m_flushing_volume_btn->GetPosition().x; + else if (p->m_bpButton_add_filament->IsShown()) exclude_pt = p->m_bpButton_add_filament->GetPosition().x - FromDIP(30); // reserve spacing for delete button + else if (ams_btn->IsShown()) exclude_pt = ams_btn->GetPosition().x; + if (e.GetPosition().x > exclude_pt) + return; + bool isShown = p->m_filament_area_wrapper->IsShown(); + p->m_filament_area_wrapper->Show(!isShown); + p->m_panel_filament_separator->Show(isShown); + m_scrolled_sizer->Layout(); - wxBoxSizer* bSizer39; - bSizer39 = new wxBoxSizer(wxHORIZONTAL); - p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); - p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Project Filaments"), LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - bSizer39->Add(p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); - bSizer39->SetMinSize(-1, FromDIP(30)); + CallAfter([this]{update_filaments_counter(true);}); // call after all UI processing done + }); - p->m_staticText_filament_count = new Label(p->m_panel_filament_title, "(0)", LB_PROPAGATE_MOUSE_EVENT); - bSizer39->Add(p->m_staticText_filament_count, 0, wxALIGN_CENTER); - bSizer39->Add(FromDIP(10), 0, 0, 0, 0); + wxBoxSizer* bSizer39; + bSizer39 = new wxBoxSizer( wxHORIZONTAL ); + p->m_filament_icon = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "filament"); + p->m_staticText_filament_settings = new Label(p->m_panel_filament_title, _L("Project Filaments"), LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_filament_icon, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + bSizer39->Add(p->m_staticText_filament_settings, 0, wxALIGN_CENTER | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ElementSpacing())); + bSizer39->SetMinSize(-1, FromDIP(30)); - p->m_panel_filament_title->SetSizer(bSizer39); - p->m_panel_filament_title->Layout(); - scrolled_sizer->Add(p->m_panel_filament_title, 0, wxEXPAND | wxALL, 0); - // ORCA add bottom border for seperation wile sections folded - p->m_panel_filament_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, - wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string - p->m_panel_filament_separator->SetBackgroundColour("#FFFFFF"); - scrolled_sizer->Add(p->m_panel_filament_separator, 0, wxEXPAND); + p->m_staticText_filament_count = new Label(p->m_panel_filament_title, "(0)", LB_PROPAGATE_MOUSE_EVENT); + bSizer39->Add(p->m_staticText_filament_count, 0, wxALIGN_CENTER ); + bSizer39->Add(FromDIP(10), 0, 0, 0, 0); - bSizer39->AddStretchSpacer(1); + p->m_panel_filament_title->SetSizer( bSizer39 ); + p->m_panel_filament_title->Layout(); + scrolled_sizer->Add(p->m_panel_filament_title, 0, wxEXPAND | wxALL, 0); + // ORCA add bottom border for seperation wile sections folded + p->m_panel_filament_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string + p->m_panel_filament_separator->SetBackgroundColour("#FFFFFF"); + scrolled_sizer->Add(p->m_panel_filament_separator, 0, wxEXPAND); - p->m_purge_mode_btn = new Button(p->m_panel_filament_title, _L("Purge mode")); - p->m_purge_mode_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + bSizer39->AddStretchSpacer(1); - p->m_purge_mode_btn->Bind(wxEVT_BUTTON, [](wxCommandEvent& e) { - auto& preset_bundle = *wxGetApp().preset_bundle; - auto support_fast_purge_opt = preset_bundle.printers.get_edited_preset().config.option( - "support_fast_purge_mode"); - bool support_fast_purge = support_fast_purge_opt ? support_fast_purge_opt->value : false; - auto dlg_type = support_fast_purge ? PurgeModeDialogType::FastMode : PurgeModeDialogType::MultiNozzle; - PurgeModeDialog dlg(static_cast(wxGetApp().mainframe), dlg_type); - if (dlg.ShowModal() == wxID_OK) { - preset_bundle.project_config.set_key_value("prime_volume_mode", - new ConfigOptionEnum(dlg.get_selected_mode())); - wxGetApp().plater()->update(); - } - }); + p->m_purge_mode_btn = new Button(p->m_panel_filament_title, _L("Purge mode")); + p->m_purge_mode_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - bSizer39->Add(p->m_purge_mode_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - bSizer39->Hide(p->m_purge_mode_btn); // hidden on launch; shown only for printers that support purge mode selection - - // BBS - // add wiping dialog - // wiping_dialog_button->SetFont(wxGetApp().normal_font()); - p->m_flushing_volume_btn = new Button(p->m_panel_filament_title, _L("Flushing volumes")); - p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); - p->m_flushing_volume_btn->SetId(wxID_RESET); - auto has_modify = is_flush_config_modified(); - set_flushing_volume_warning(has_modify); - - p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent, this](wxCommandEvent& e) { - open_flushing_dialog(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); - p->plater->get_view3D_canvas3D()->reload_scene(true); - p->plater->update(); - })); - - bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); - bSizer39->Hide(p->m_flushing_volume_btn); // ORCA Ensure button is hidden on launch while 1 filament exist - - ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); - add_btn->SetToolTip(_L("Add one filament")); - add_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { - add_filament(); - update_filaments_counter(); - }); - p->m_bpButton_add_filament = add_btn; - - // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden - - ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); - del_btn->SetToolTip(_L("Remove last filament")); - del_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { - delete_filament(); - update_filaments_counter(); - }); - p->m_bpButton_del_filament = del_btn; - - bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, - FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position - // change when remove icon automatically hidden - - bSizer39->Hide(p->m_bpButton_del_filament); // ORCA Ensure button is hidden on launch while 1 filament exist - - ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, - wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 - ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); - ams_btn->Bind(wxEVT_BUTTON, [this, scrolled_sizer](wxCommandEvent& e) { sync_ams_list(); }); - - ams_btn->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); - p->m_bpButton_ams_filament = ams_btn; - - bSizer39->Add(ams_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); - // bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); - - ScalableButton* set_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "settings"); - set_btn->SetToolTip(_L("Set filaments to use")); - set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - p->editing_filament = -1; - // wxGetApp().params_dialog()->Popup(); - // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); - wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); - }); - p->m_bpButton_set_filament = set_btn; - - bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); - bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); - - // ---- Wrapper panel for collapse/expand of all filament content ---- - p->m_filament_area_wrapper = new wxPanel(p->scrolled, wxID_ANY); - p->m_filament_area_wrapper->SetBackgroundColour(*wxWHITE); - auto* wrapper_sizer = new wxBoxSizer(wxVERTICAL); - - // add filament content - p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, - wxTAB_TRAVERSAL); - p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); - p->m_panel_filament_content->SetScrollRate(0, 5); - // p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); - p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); - - // wxBoxSizer* bSizer_filament_content; - // bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); - - // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout - // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel - p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); - p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - - p->combos_filament.push_back(nullptr); - - /* first filament item */ - init_filament_combo(&p->combos_filament[0], 0); - - // bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); - wxSizer* sizer_filaments2 = new wxBoxSizer(wxVERTICAL); - sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); - p->m_panel_filament_content->SetSizer(sizer_filaments2); - p->m_panel_filament_content->Layout(); - - update_filaments_area_height(); // ORCA - - wrapper_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND); - - // ---- Mixed-color filament section ---- - // A mixed filament is a virtual slot realized from 2-3 physical filaments at slicing time. - // Everything here stays hidden until at least two physical filaments exist, so a single - // filament setup looks exactly as before. - { - // 1) "+ Add Mixed Filament" button, shown only while no mixed filament exists yet. - p->m_btn_add_mixed_filament = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); - p->m_btn_add_mixed_filament->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); - p->m_btn_add_mixed_filament->SetMinSize(wxSize(-1, FromDIP(23))); - { - auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); - auto* icon_add = new ScalableButton(p->m_btn_add_mixed_filament, wxID_ANY, "add_filament", wxEmptyString, wxDefaultSize, - wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); - auto* add_label = new wxStaticText(p->m_btn_add_mixed_filament, wxID_ANY, _L("Add Mixed Filament"), wxDefaultPosition, - wxDefaultSize, 0); - add_label->SetFont(::Label::Body_13); - btn_sizer->AddStretchSpacer(); - btn_sizer->Add(icon_add, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); - btn_sizer->Add(add_label, 0, wxALIGN_CENTER_VERTICAL); - btn_sizer->AddStretchSpacer(); - p->m_btn_add_mixed_filament->SetSizer(btn_sizer); - p->m_btn_add_mixed_filament->SetCursor(wxCursor(wxCURSOR_HAND)); - // Whole panel is the hit target, so forward clicks from the children too. - auto on_click = [this](wxMouseEvent&) { add_mixed_filament(); }; - p->m_btn_add_mixed_filament->Bind(wxEVT_LEFT_UP, on_click); - add_label->Bind(wxEVT_LEFT_UP, on_click); - icon_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); - } - wrapper_sizer->Add(p->m_btn_add_mixed_filament, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(8)); - - // 2) Title row with add / remove buttons, shown once a mixed filament exists. - p->m_panel_mixed_title = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); - p->m_panel_mixed_title->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - { - auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); - p->m_text_mixed_title = new wxStaticText(p->m_panel_mixed_title, wxID_ANY, _L("Mixed Filament")); - p->m_text_mixed_title->SetFont(::Label::Head_14); - title_sizer->Add(p->m_text_mixed_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); - title_sizer->AddStretchSpacer(); - - p->m_btn_mixed_del = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_filament"); - p->m_btn_mixed_del->SetToolTip(_L("Remove last mixed filament")); - p->m_btn_mixed_del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { - auto* plater_ptr = dynamic_cast(GetParent()); - if (!plater_ptr) - return; - auto mixed_indices = plater_ptr->mixed_filament_config_indices(); - if (!mixed_indices.empty()) - delete_mixed_filament_at(mixed_indices.size() - 1); - }); - title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - - p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament"); - p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament")); - p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); - title_sizer->Add(p->m_btn_mixed_add, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); - title_sizer->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); - - p->m_panel_mixed_title->SetSizer(title_sizer); - } - wrapper_sizer->Add(p->m_panel_mixed_title, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); - - // 3) Mixed filament rows, in their own scroll area so a long mixed list does not - // push the physical filament list off screen. - p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, - wxTAB_TRAVERSAL); - p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); - p->m_mixed_scroll_area->SetScrollRate(0, 5); - p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - { - auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); - p->m_panel_mixed_content = new wxPanel(p->m_mixed_scroll_area, wxID_ANY); - p->m_panel_mixed_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - - // Two columns, same idiom as sizer_filaments. - p->m_sizer_mixed_filaments = new wxBoxSizer(wxHORIZONTAL); - p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); - - auto* sizer_mixed2 = new wxBoxSizer(wxVERTICAL); - sizer_mixed2->Add(p->m_sizer_mixed_filaments, 0, wxEXPAND, 0); - p->m_panel_mixed_content->SetSizer(sizer_mixed2); - mix_scroll_sizer->Add(p->m_panel_mixed_content, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); - p->m_mixed_scroll_area->SetSizer(mix_scroll_sizer); - } - p->m_mixed_scroll_area->EnableScrolling(false, true); - p->m_mixed_scroll_area->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); - p->m_mixed_scroll_area->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { - int w = p->m_mixed_scroll_area->GetClientSize().GetWidth(); - if (w > 0) - p->m_mixed_scroll_area->SetVirtualSize(w, p->m_mixed_scroll_area->GetVirtualSize().GetHeight()); - e.Skip(); - }); - wrapper_sizer->Add(p->m_mixed_scroll_area, 0, wxEXPAND, 0); - - // 4) Warning bar for mixes whose components were deleted or whose types disagree. - p->m_panel_mixed_warning = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); - p->m_panel_mixed_warning->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); - { - auto* warn_sizer = new wxBoxSizer(wxHORIZONTAL); - p->m_text_mixed_warning = - new wxStaticText(p->m_panel_mixed_warning, wxID_ANY, - _L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); - p->m_text_mixed_warning->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B"))); - p->m_text_mixed_warning->SetFont(::Label::Body_12); - p->m_text_mixed_warning->Wrap(FromDIP(360)); - warn_sizer->Add(p->m_text_mixed_warning, 1, wxALL, FromDIP(6)); - p->m_panel_mixed_warning->SetSizer(warn_sizer); - } - wrapper_sizer->Add(p->m_panel_mixed_warning, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8)); - - // Hidden until update_mixed_filament_list() decides otherwise. - p->m_btn_add_mixed_filament->Hide(); - p->m_panel_mixed_title->Hide(); - p->m_mixed_scroll_area->Hide(); - p->m_panel_mixed_content->Hide(); - p->m_panel_mixed_warning->Hide(); + p->m_purge_mode_btn->Bind(wxEVT_BUTTON, [](wxCommandEvent &e) { + auto &preset_bundle = *wxGetApp().preset_bundle; + auto support_fast_purge_opt = preset_bundle.printers.get_edited_preset().config.option("support_fast_purge_mode"); + bool support_fast_purge = support_fast_purge_opt ? support_fast_purge_opt->value : false; + auto dlg_type = support_fast_purge ? PurgeModeDialogType::FastMode : PurgeModeDialogType::MultiNozzle; + PurgeModeDialog dlg(static_cast(wxGetApp().mainframe), dlg_type); + if (dlg.ShowModal() == wxID_OK) { + preset_bundle.project_config.set_key_value("prime_volume_mode", new ConfigOptionEnum(dlg.get_selected_mode())); + wxGetApp().plater()->update(); } - // ---- End mixed-color filament section ---- + }); - p->m_filament_area_wrapper->SetSizer(wrapper_sizer); - p->m_filament_area_wrapper->Layout(); - scrolled_sizer->Add( - p->m_filament_area_wrapper, 0, wxEXPAND | wxTOP | wxBOTTOM, - FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament - // ---- End filament area ---- + bSizer39->Add(p->m_purge_mode_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + bSizer39->Hide(p->m_purge_mode_btn); // hidden on launch; shown only for printers that support purge mode selection + + // BBS + // add wiping dialog + //wiping_dialog_button->SetFont(wxGetApp().normal_font()); + p->m_flushing_volume_btn = new Button(p->m_panel_filament_title, _L("Flushing volumes")); + p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); + p->m_flushing_volume_btn->SetId(wxID_RESET); + auto has_modify = is_flush_config_modified(); + set_flushing_volume_warning(has_modify); + + p->m_flushing_volume_btn->Bind(wxEVT_BUTTON, ([parent, this](wxCommandEvent &e) { + open_flushing_dialog(parent, SimpleEvent(EVT_SCHEDULE_BACKGROUND_PROCESS, parent)); + p->plater->get_view3D_canvas3D()->reload_scene(true); + p->plater->update(); + })); + + bSizer39->Add(p->m_flushing_volume_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4)); + bSizer39->Hide(p->m_flushing_volume_btn); // ORCA Ensure button is hidden on launch while 1 filament exist + + ScalableButton* add_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "add_filament"); + add_btn->SetToolTip(_L("Add one filament")); + add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e){ + add_filament(); + update_filaments_counter(); + }); + p->m_bpButton_add_filament = add_btn; + + // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "delete_filament"); + del_btn->SetToolTip(_L("Remove last filament")); + del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + delete_filament(); + update_filaments_counter(); + }); + p->m_bpButton_del_filament = del_btn; + + bSizer39->Add(del_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + bSizer39->Add(add_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::IconSpacing())); // ORCA Moved add button after delete button to prevent add button position change when remove icon automatically hidden + + bSizer39->Hide(p->m_bpButton_del_filament); // ORCA Ensure button is hidden on launch while 1 filament exist + + ams_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "ams_fila_sync", wxEmptyString, wxDefaultSize, wxDefaultPosition, + wxBU_EXACTFIT | wxNO_BORDER, false, 16); // ORCA match icon size with other icons as 16x16 + ams_btn->SetToolTip(_L("Synchronize filament list from AMS")); + ams_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + sync_ams_list(); + }); + + ams_btn->Bind(wxEVT_UPDATE_UI, &Sidebar::update_sync_ams_btn_enable, this); + p->m_bpButton_ams_filament = ams_btn; + + bSizer39->Add(ams_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + //bSizer39->Add(FromDIP(10), 0, 0, 0, 0 ); + + ScalableButton* set_btn = new ScalableButton(p->m_panel_filament_title, wxID_ANY, "settings"); + set_btn->SetToolTip(_L("Set filaments to use")); + set_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { + p->editing_filament = -1; + // wxGetApp().params_dialog()->Popup(); + // wxGetApp().get_tab(Preset::TYPE_FILAMENT)->restore_last_select_item(); + wxGetApp().run_wizard(ConfigWizard::RR_USER, ConfigWizard::SP_FILAMENTS); + }); + p->m_bpButton_set_filament = set_btn; + + bSizer39->Add(set_btn, 0, wxALIGN_CENTER | wxLEFT, FromDIP(SidebarProps::WideSpacing())); + bSizer39->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + // ---- Wrapper panel for collapse/expand of all filament content ---- + p->m_filament_area_wrapper = new wxPanel(p->scrolled, wxID_ANY); + p->m_filament_area_wrapper->SetBackgroundColour(*wxWHITE); + auto* wrapper_sizer = new wxBoxSizer(wxVERTICAL); + + // add filament content + p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); + p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); + p->m_panel_filament_content->SetScrollRate(0, 5); + //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); + p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); + + //wxBoxSizer* bSizer_filament_content; + //bSizer_filament_content = new wxBoxSizer( wxHORIZONTAL ); + + // Orca: Sidebar - Filament content UI: setup filament selection combos panel layout + // Creates a two-column grid layout for filament selection dropdowns within the scrollable panel + p->sizer_filaments = new wxBoxSizer(wxHORIZONTAL); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->sizer_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + p->combos_filament.push_back(nullptr); + + /* first filament item */ + init_filament_combo(&p->combos_filament[0], 0); + + //bSizer_filament_content->Add(p->sizer_filaments, 1, wxALIGN_CENTER | wxALL); + wxSizer *sizer_filaments2 = new wxBoxSizer(wxVERTICAL); + sizer_filaments2->Add(p->sizer_filaments, 0, wxEXPAND, 0); + p->m_panel_filament_content->SetSizer(sizer_filaments2); + p->m_panel_filament_content->Layout(); + + update_filaments_area_height(); // ORCA + + wrapper_sizer->Add(p->m_panel_filament_content, 0, wxEXPAND); + + // ---- Mixed-color filament section ---- + // A mixed filament is a virtual slot realized from 2-3 physical filaments at slicing time. + // Everything here stays hidden until at least two physical filaments exist, so a single + // filament setup looks exactly as before. + { + // 1) "+ Add Mixed Filament" button, shown only while no mixed filament exists yet. + p->m_btn_add_mixed_filament = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_btn_add_mixed_filament->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); + p->m_btn_add_mixed_filament->SetMinSize(wxSize(-1, FromDIP(23))); + { + auto* btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto* icon_add = new ScalableButton(p->m_btn_add_mixed_filament, wxID_ANY, "add_filament", wxEmptyString, + wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 16); + auto* add_label = new wxStaticText(p->m_btn_add_mixed_filament, wxID_ANY, _L("Add Mixed Filament"), + wxDefaultPosition, wxDefaultSize, 0); + add_label->SetFont(::Label::Body_13); + btn_sizer->AddStretchSpacer(); + btn_sizer->Add(icon_add, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + btn_sizer->Add(add_label, 0, wxALIGN_CENTER_VERTICAL); + btn_sizer->AddStretchSpacer(); + p->m_btn_add_mixed_filament->SetSizer(btn_sizer); + p->m_btn_add_mixed_filament->SetCursor(wxCursor(wxCURSOR_HAND)); + // Whole panel is the hit target, so forward clicks from the children too. + auto on_click = [this](wxMouseEvent&) { add_mixed_filament(); }; + p->m_btn_add_mixed_filament->Bind(wxEVT_LEFT_UP, on_click); + add_label->Bind(wxEVT_LEFT_UP, on_click); + icon_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + } + wrapper_sizer->Add(p->m_btn_add_mixed_filament, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, FromDIP(8)); + + // 2) Title row with add / remove buttons, shown once a mixed filament exists. + p->m_panel_mixed_title = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_title->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* title_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_title = new wxStaticText(p->m_panel_mixed_title, wxID_ANY, _L("Mixed Filament")); + p->m_text_mixed_title->SetFont(::Label::Head_14); + title_sizer->Add(p->m_text_mixed_title, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::TitlebarMargin())); + title_sizer->AddStretchSpacer(); + + p->m_btn_mixed_del = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_filament"); + p->m_btn_mixed_del->SetToolTip(_L("Remove last mixed filament")); + p->m_btn_mixed_del->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + auto* plater_ptr = dynamic_cast(GetParent()); + if (!plater_ptr) return; + auto mixed_indices = plater_ptr->mixed_filament_config_indices(); + if (!mixed_indices.empty()) + delete_mixed_filament_at(mixed_indices.size() - 1); + }); + title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + + p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament"); + p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament")); + p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); }); + title_sizer->Add(p->m_btn_mixed_add, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing())); + title_sizer->AddSpacer(FromDIP(SidebarProps::TitlebarMargin())); + + p->m_panel_mixed_title->SetSizer(title_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_title, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(8)); + + // 3) Mixed filament rows, in their own scroll area so a long mixed list does not + // push the physical filament list off screen. + p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); + p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); + p->m_mixed_scroll_area->SetScrollRate(0, 5); + p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); + p->m_panel_mixed_content = new wxPanel(p->m_mixed_scroll_area, wxID_ANY); + p->m_panel_mixed_content->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + + // Two columns, same idiom as sizer_filaments. + p->m_sizer_mixed_filaments = new wxBoxSizer(wxHORIZONTAL); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + p->m_sizer_mixed_filaments->Add(new wxBoxSizer(wxVERTICAL), 1, wxEXPAND); + + auto* sizer_mixed2 = new wxBoxSizer(wxVERTICAL); + sizer_mixed2->Add(p->m_sizer_mixed_filaments, 0, wxEXPAND, 0); + p->m_panel_mixed_content->SetSizer(sizer_mixed2); + mix_scroll_sizer->Add(p->m_panel_mixed_content, 0, wxEXPAND | wxBOTTOM, FromDIP(8)); + p->m_mixed_scroll_area->SetSizer(mix_scroll_sizer); + } + p->m_mixed_scroll_area->EnableScrolling(false, true); + p->m_mixed_scroll_area->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_DEFAULT); + p->m_mixed_scroll_area->Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { + int w = p->m_mixed_scroll_area->GetClientSize().GetWidth(); + if (w > 0) + p->m_mixed_scroll_area->SetVirtualSize(w, p->m_mixed_scroll_area->GetVirtualSize().GetHeight()); + e.Skip(); + }); + wrapper_sizer->Add(p->m_mixed_scroll_area, 0, wxEXPAND, 0); + + // 4) Warning bar for mixes whose components were deleted or whose types disagree. + p->m_panel_mixed_warning = new wxPanel(p->m_filament_area_wrapper, wxID_ANY); + p->m_panel_mixed_warning->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); + { + auto* warn_sizer = new wxBoxSizer(wxHORIZONTAL); + p->m_text_mixed_warning = new wxStaticText(p->m_panel_mixed_warning, wxID_ANY, + _L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + p->m_text_mixed_warning->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#D01B1B"))); + p->m_text_mixed_warning->SetFont(::Label::Body_12); + p->m_text_mixed_warning->Wrap(FromDIP(360)); + warn_sizer->Add(p->m_text_mixed_warning, 1, wxALL, FromDIP(6)); + p->m_panel_mixed_warning->SetSizer(warn_sizer); + } + wrapper_sizer->Add(p->m_panel_mixed_warning, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(8)); + + // Hidden until update_mixed_filament_list() decides otherwise. + p->m_btn_add_mixed_filament->Hide(); + p->m_panel_mixed_title->Hide(); + p->m_mixed_scroll_area->Hide(); + p->m_panel_mixed_content->Hide(); + p->m_panel_mixed_warning->Hide(); + } + // ---- End mixed-color filament section ---- + + p->m_filament_area_wrapper->SetSizer(wrapper_sizer); + p->m_filament_area_wrapper->Layout(); + scrolled_sizer->Add(p->m_filament_area_wrapper, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(SidebarProps::ContentMarginV())); // ORCA use vertical margin on parent otherwise it shows scrollbar even on 1 filament + // ---- End filament area ---- } { - // add project title - auto params_panel = ((MainFrame*) parent->GetParent())->m_param_panel; - if (params_panel) { - params_panel->get_top_panel()->Reparent(p->scrolled); - auto spliter_1 = new ::StaticLine(p->scrolled); - spliter_1->SetLineColour("#A6A9AA"); - scrolled_sizer->Add(spliter_1, 0, wxEXPAND); - scrolled_sizer->Add(params_panel->get_top_panel(), 0, wxEXPAND); - auto spliter_2 = new ::StaticLine(p->scrolled); - spliter_2->SetLineColour("#CECECE"); - scrolled_sizer->Add(spliter_2, 0, wxEXPAND); + //add project title + auto params_panel = ((MainFrame*)parent->GetParent())->m_param_panel; + if (params_panel) { + params_panel->get_top_panel()->Reparent(p->scrolled); + auto spliter_1 = new ::StaticLine(p->scrolled); + spliter_1->SetLineColour("#A6A9AA"); + scrolled_sizer->Add(spliter_1, 0, wxEXPAND); + scrolled_sizer->Add(params_panel->get_top_panel(), 0, wxEXPAND); + auto spliter_2 = new ::StaticLine(p->scrolled); + spliter_2->SetLineColour("#CECECE"); + scrolled_sizer->Add(spliter_2, 0, wxEXPAND); + } + + //add project content + p->sizer_params = new wxBoxSizer(wxVERTICAL); + + // ORCA: Update search box to modern style + p->m_search_bar = new StaticBox(p->scrolled); + p->m_search_bar->SetCornerRadius(0); + p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + + p->m_search_item = new TextInput(p->m_search_bar, wxEmptyString, wxEmptyString, "", wxDefaultPosition, wxDefaultSize, 0 | wxBORDER_NONE); + p->m_search_item->SetIcon(*BitmapCache().load_svg("search", FromDIP(16), FromDIP(16))); // ORCA: Add search icon to search box + + wxTextCtrl* text_ctrl = p->m_search_item->GetTextCtrl(); + text_ctrl->SetHint(_L("Search plate, object and part.")); + text_ctrl->SetForegroundColour(wxColour("#262E30")); + text_ctrl->SetFont(Label::Body_13); + text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically + + text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) { + if (p->dia->IsShown()) { + e.Skip(); + return; } - - // add project content - p->sizer_params = new wxBoxSizer(wxVERTICAL); - - // ORCA: Update search box to modern style - p->m_search_bar = new StaticBox(p->scrolled); - p->m_search_bar->SetCornerRadius(0); - p->m_search_bar->SetBorderColor(wxColour("#CECECE")); - - p->m_search_item = new TextInput(p->m_search_bar, wxEmptyString, wxEmptyString, "", wxDefaultPosition, wxDefaultSize, - 0 | wxBORDER_NONE); - p->m_search_item->SetIcon(*BitmapCache().load_svg("search", FromDIP(16), FromDIP(16))); // ORCA: Add search icon to search box - - wxTextCtrl* text_ctrl = p->m_search_item->GetTextCtrl(); - text_ctrl->SetHint(_L("Search plate, object and part.")); - text_ctrl->SetForegroundColour(wxColour("#262E30")); - text_ctrl->SetFont(Label::Body_13); - text_ctrl->SetSize(wxSize(-1, FromDIP(16))); // Centers text vertically - - text_ctrl->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& e) { - if (p->dia->IsShown()) { - e.Skip(); - return; - } - p->m_search_bar->SetBorderColor(wxColour("#009688")); - wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); + p->m_search_bar->SetBorderColor(wxColour("#009688")); + wxPoint pos = this->p->m_search_bar->ClientToScreen(wxPoint(0, 0)); #ifndef __WXGTK__ - pos.y += this->p->m_search_bar->GetRect().height; + pos.y += this->p->m_search_bar->GetRect().height; #else this->p->m_search_item->Enable(false); #endif - p->dia->SetPosition(pos); - p->dia->Popup(); - e.Skip(); // required to show caret - }); + p->dia->SetPosition(pos); + p->dia->Popup(); + e.Skip(); // required to show caret + }); - auto search_sizer = new wxBoxSizer(wxHORIZONTAL); - search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND | wxLEFT | wxRIGHT, - FromDIP(1)); - search_sizer->Add(p->m_search_item, 1, wxEXPAND | wxALL, FromDIP(2)); - p->m_search_bar->SetSizer(search_sizer); - p->m_search_bar->Layout(); - search_sizer->Fit(p->m_search_bar); + auto search_sizer = new wxBoxSizer(wxHORIZONTAL); + search_sizer->Add(new wxWindow(p->m_search_bar, wxID_ANY, wxDefaultPosition, wxSize(0, 0)), 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(1)); + search_sizer->Add(p->m_search_item, 1, wxEXPAND | wxALL, FromDIP(2)); + p->m_search_bar->SetSizer(search_sizer); + p->m_search_bar->Layout(); + search_sizer->Fit(p->m_search_bar); - p->m_object_list = new ObjectList(p->scrolled); - p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) { + p->m_object_list = new ObjectList(p->scrolled); + p->m_object_list->Bind(wxCUSTOMEVT_EXIT_SEARCH, [this](wxCommandEvent&) { #ifdef __WXGTK__ - this->p->m_search_item->Enable(true); + this->p->m_search_item->Enable(true); #endif - this->p->m_search_bar->SetBorderColor(wxColour("#CECECE")); - this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close - }); + this->p->m_search_bar->SetBorderColor(wxColour("#CECECE")); + this->p->m_search_item->GetTextCtrl()->SetValue(""); // reset value when close + }); - p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0); - p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0); - scrolled_sizer->Add(p->sizer_params, 2, wxEXPAND | wxLEFT, 0); - p->m_object_list->Hide(); - p->m_search_bar->Hide(); - // Frequently Object Settings - p->object_settings = new ObjectSettings(p->scrolled); + p->sizer_params->Add(p->m_search_bar, 0, wxALL | wxEXPAND, 0); + p->sizer_params->Add(p->m_object_list, 1, wxEXPAND | wxTOP, 0); + scrolled_sizer->Add(p->sizer_params, 2, wxEXPAND | wxLEFT, 0); + p->m_object_list->Hide(); + p->m_search_bar->Hide(); + // Frequently Object Settings + p->object_settings = new ObjectSettings(p->scrolled); - p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item); + p->dia = new Search::SearchObjectDialog(p->m_object_list, p->scrolled->GetParent(), p->m_search_item); #if !NEW_OBJECT_SETTING - p->object_settings->Hide(); - p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10); + p->object_settings->Hide(); + p->sizer_params->Add(p->object_settings->get_sizer(), 0, wxEXPAND | wxTOP, 5 * em / 10); #else - if (params_panel) { - params_panel->Reparent(p->scrolled); - scrolled_sizer->Add(params_panel, 3, wxEXPAND); - } + if (params_panel) { + params_panel->Reparent(p->scrolled); + scrolled_sizer->Add(params_panel, 3, wxEXPAND); + } #endif } @@ -3306,71 +3264,67 @@ Sidebar::Sidebar(Plater* parent) : wxPanel(parent, wxID_ANY, wxDefaultPosition, p->object_layers->Hide(); p->sizer_params->Add(p->object_layers->get_sizer(), 0, wxEXPAND | wxTOP, 0); - auto* sizer = new wxBoxSizer(wxVERTICAL); + auto *sizer = new wxBoxSizer(wxVERTICAL); sizer->Add(p->scrolled, 1, wxEXPAND); SetSizer(sizer); } Sidebar::~Sidebar() {} -void Sidebar::on_enter_image_printer_bed(wxMouseEvent& evt) -{ - // p->image_printer_bed->Bind(wxEVT_LEAVE_WINDOW, &Sidebar::on_leave_image_printer_bed, this); - auto pos = p->panel_printer_bed->GetScreenPosition(); - auto rect = p->panel_printer_bed->GetRect(); - wxPoint temp_pos(pos.x + rect.GetWidth() + FromDIP(3), pos.y); +void Sidebar::on_enter_image_printer_bed(wxMouseEvent &evt) { + //p->image_printer_bed->Bind(wxEVT_LEAVE_WINDOW, &Sidebar::on_leave_image_printer_bed, this); + auto pos = p->panel_printer_bed->GetScreenPosition(); + auto rect = p->panel_printer_bed->GetRect(); + wxPoint temp_pos(pos.x + rect.GetWidth() + FromDIP(3), pos.y); if (p->big_bed_image_popup == nullptr) p->big_bed_image_popup = new ImageDPIFrame(); - auto image_path = get_cur_select_bed_image(); - p->big_bed_image_popup->set_bitmap( - create_scaled_bitmap("big_" + image_path, p->big_bed_image_popup, p->big_bed_image_popup->get_image_px())); + auto image_path = get_cur_select_bed_image(); + p->big_bed_image_popup->set_bitmap(create_scaled_bitmap("big_" + image_path, p->big_bed_image_popup, p->big_bed_image_popup->get_image_px())); p->big_bed_image_popup->set_title(p->combo_printer_bed->GetString(p->combo_printer_bed->GetSelection())); p->big_bed_image_popup->SetCanFocus(false); p->big_bed_image_popup->SetPosition(temp_pos); p->big_bed_image_popup->on_show(); } -void Sidebar::on_leave_image_printer_bed(wxMouseEvent& evt) -{ - // auto pos_x = evt.GetX(); - // auto pos_y = evt.GetY(); - // auto rect = p->image_printer_bed->GetRect(); - // if ((pos_x <= 0 || pos_y <= 0 || pos_x >= rect.GetWidth()) && p->big_bed_image_popup) { +void Sidebar::on_leave_image_printer_bed(wxMouseEvent &evt) { + //auto pos_x = evt.GetX(); + //auto pos_y = evt.GetY(); + //auto rect = p->image_printer_bed->GetRect(); + //if ((pos_x <= 0 || pos_y <= 0 || pos_x >= rect.GetWidth()) && p->big_bed_image_popup) { if (p->big_bed_image_popup) { bool was_visible = p->big_bed_image_popup->IsShown(); p->big_bed_image_popup->on_hide(); - if (!p->combo_printer_bed->is_drop_down() && was_visible) - p->combo_printer_bed->SetFocus(); // set focus back to bed type combo. this prevents weird look if focus on other item + if(!p->combo_printer_bed->is_drop_down() && was_visible) + p->combo_printer_bed->SetFocus(); // set focus back to bed type combo. this prevents weird look if focus on other item } } -void Sidebar::on_change_color_mode(bool is_dark) -{ - const ModelObjectPtrs& mos = wxGetApp().model().objects; +void Sidebar::on_change_color_mode(bool is_dark) { + const ModelObjectPtrs &mos = wxGetApp().model().objects; for (int i = 0; i < mos.size(); i++) { - wxGetApp().obj_list()->update_info_items(i, nullptr, false, true); + wxGetApp().obj_list()->update_info_items(i,nullptr,false,true); } + } void Sidebar::create_printer_preset() { CreatePrinterPresetDialog dlg(wxGetApp().mainframe); - int res = dlg.ShowModal(); + int res = dlg.ShowModal(); if (wxID_OK == res) { wxGetApp().load_current_presets(); wxGetApp().mainframe->update_side_preset_ui(); update_ui_from_settings(); update_all_preset_comboboxes(); CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::PRINTER); - int res = success_dlg.ShowModal(); + int res = success_dlg.ShowModal(); if (res == wxID_OK) { p->editing_filament = -1; - if (p->combo_printer->switch_to_tab()) - p->editing_filament = 0; + if (p->combo_printer->switch_to_tab()) p->editing_filament = 0; } } } -void Sidebar::init_filament_combo(PlaterPresetComboBox** combo, const int filament_idx) +void Sidebar::init_filament_combo(PlaterPresetComboBox **combo, const int filament_idx) { *combo = new PlaterPresetComboBox(p->m_panel_filament_content, Slic3r::Preset::TYPE_FILAMENT); (*combo)->set_filament_idx(filament_idx); @@ -3384,10 +3338,8 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox** combo, const int filame combo_and_btn_sizer->AddSpacer(FromDIP((filament_idx % 2) == 0 ? 12 : 3)); // Content Margin (*combo)->clr_picker->SetLabel(wxString::Format("%d", filament_idx + 1)); - combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, - FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) - combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2)) - ->SetMinSize({-1, 30 * wxGetApp().em_unit() / 10}); // ORCA ensure height matches with PlaterPresetComboBox + combo_and_btn_sizer->Add((*combo)->clr_picker, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + combo_and_btn_sizer->Add(*combo, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, 30 * wxGetApp().em_unit() / 10}); // ORCA ensure height matches with PlaterPresetComboBox /* BBS hide del_btn ScalableButton* del_btn = new ScalableButton(p->m_panel_filament_content, wxID_ANY, "delete_filament"); @@ -3410,15 +3362,16 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox** combo, const int filame edit_btn->Bind(wxEVT_BUTTON, [this, edit_btn, combobox, filament_idx](wxCommandEvent) { bool single_or_bbl = should_show_SEMM_buttons(); bool is_multi_material = p->combos_filament.size() > 1; - if (single_or_bbl && is_multi_material) { - // MULTI MATERIAL Show menu + if(single_or_bbl && is_multi_material) { + // MULTI MATERIAL Show menu auto menu = p->plater->filament_action_menu(filament_idx); - wxPoint pt{0, edit_btn->GetSize().GetHeight() + FromDIP(2)}; - pt = edit_btn->ClientToScreen(pt); - pt = wxGetApp().mainframe->ScreenToClient(pt); + wxPoint pt { 0, edit_btn->GetSize().GetHeight() + FromDIP(2) }; + pt = edit_btn->ClientToScreen(pt); + pt = wxGetApp().mainframe->ScreenToClient(pt); p->m_menu_filament_id = filament_idx; p->plater->PopupMenu(menu, (int) pt.x, pt.y); - } else { + } + else { // SINGLE MATERIAL / MULTI EXTRUDER / TOOLCHANGER / IDEX Opens Dialog directly p->editing_filament = filament_idx; combobox->switch_to_tab(); @@ -3426,16 +3379,14 @@ void Sidebar::init_filament_combo(PlaterPresetComboBox** combo, const int filame }); combobox->edit_btn = edit_btn; - combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, - FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) + combo_and_btn_sizer->Add(edit_btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::ElementSpacing()) - FromDIP(2)); // ElementSpacing - 2 (from combo box)) combo_and_btn_sizer->AddSpacer(FromDIP(SidebarProps::ContentMargin())); // BBS: filament double columns - auto side = filament_idx % 2; - auto /***/ sizer_filaments = this->p->sizer_filaments->GetItem(side)->GetSizer(); - if (side == 1 && filament_idx > 1) - sizer_filaments->Remove(filament_idx / 2); + auto side = filament_idx % 2; + auto /***/sizer_filaments = this->p->sizer_filaments->GetItem(side)->GetSizer(); + if (side == 1 && filament_idx > 1) sizer_filaments->Remove(filament_idx / 2); sizer_filaments->Add(combo_and_btn_sizer, 1, wxEXPAND); if (side == 0 && filament_idx > 0) { sizer_filaments = this->p->sizer_filaments->GetItem(1)->GetSizer(); @@ -3448,14 +3399,14 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) if (current_extruder_count >= p->combos_filament.size()) return; while (p->combos_filament.size() > current_extruder_count) { - const int last = p->combos_filament.size() - 1; + const int last = p->combos_filament.size() - 1; auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); sizer_filaments->Remove(last / 2); (*p->combos_filament[last]).Destroy(); p->combos_filament.pop_back(); } // BBS: filament double columns - auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto sizer_filaments0 = this->p->sizer_filaments->GetItem((size_t)0)->GetSizer(); auto sizer_filaments1 = this->p->sizer_filaments->GetItem(1)->GetSizer(); if (current_extruder_count < 2) { sizer_filaments1->Clear(); @@ -3471,27 +3422,27 @@ void Sidebar::remove_unused_filament_combos(const size_t current_extruder_count) void Sidebar::update_all_preset_comboboxes() { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); - auto p_mainframe = wxGetApp().mainframe; - auto cfg = preset_bundle.printers.get_edited_preset().config; - const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + auto p_mainframe = wxGetApp().mainframe; + auto cfg = preset_bundle.printers.get_edited_preset().config; + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || use_printer_agents; if (preset_bundle.use_bbl_network()) { - // only show connection button for not-BBL printer - // p->btn_connect_printer->Hide(); + //only show connection button for not-BBL printer + //p->btn_connect_printer->Hide(); p->m_printer_connect->Hide(); - // only show sync-ams button for BBL printer + //only show sync-ams button for BBL printer p->m_bpButton_ams_filament->Show(); - // update print button default value for bbl or third-party printer + //update print button default value for bbl or third-party printer p_mainframe->set_print_button_to_default(MainFrame::PrintSelectType::ePrintPlate); } else { - // p->btn_connect_printer->Show(); - // ORCA: hide the physical-printer connection button when printer agents are enabled + //p->btn_connect_printer->Show(); + // ORCA: hide the physical-printer connection button when printer agents are enabled p->m_printer_connect->Show(!use_printer_agents); // ORCA: show/hide sync-ams button based on filament sync mode @@ -3504,19 +3455,19 @@ void Sidebar::update_all_preset_comboboxes() // Orca: with "Support 3MF as gcode" (use_3mf) the local export is a .gcode.3mf bundle, so when no // printer host/IP is configured the default action is "Export plate sliced file" (mirrors the // print dropdown) instead of "Export G-code file". - auto print_btn_type = cfg.opt_bool("use_3mf") ? MainFrame::PrintSelectType::eExportSlicedFile : - MainFrame::PrintSelectType::eExportGcode; - wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); + auto print_btn_type = cfg.opt_bool("use_3mf") ? MainFrame::PrintSelectType::eExportSlicedFile + : MainFrame::PrintSelectType::eExportGcode; + wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); wxString apikey; - if (url.empty()) + if(url.empty()) url = wxString::Format("file://%s/web/orca/missing_connection.html", from_u8(resources_dir())); else { const auto host_type = cfg.option>("host_type")->value; if (cfg.has("printhost_apikey") && (host_type != htSimplyPrint)) apikey = cfg.opt_string("printhost_apikey"); - print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) ? - MainFrame::PrintSelectType::ePrintPlate : - MainFrame::PrintSelectType::eSendGcode; + print_btn_type = (preset_bundle.is_bbl_vendor() || wxGetApp().app_config->get_bool("use_printer_agents")) + ? MainFrame::PrintSelectType::ePrintPlate + : MainFrame::PrintSelectType::eSendGcode; } if (use_printer_agents) @@ -3524,27 +3475,29 @@ void Sidebar::update_all_preset_comboboxes() else if (!use_native_device_tab) p_mainframe->load_printer_url(url, apikey); + p_mainframe->set_print_button_to_default(print_btn_type); + } if (cfg.opt_bool("pellet_modded_printer")) { - p->m_staticText_filament_settings->SetLabel(_L("Pellets")); + p->m_staticText_filament_settings->SetLabel(_L("Pellets")); p->m_filament_icon->SetBitmap_("pellets"); } else { - p->m_staticText_filament_settings->SetLabel(_L("Filament")); + p->m_staticText_filament_settings->SetLabel(_L("Filament")); p->m_filament_icon->SetBitmap_("filament"); } show_SEMM_buttons(); - // p->m_staticText_filament_settings->Update(); + //p->m_staticText_filament_settings->Update(); if (is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")) { p->combo_printer_bed->Enable(); // Orca: don't update bed type if loading project if (!p->plater->is_loading_project()) { bool has_changed = reset_bed_type_combox_choices(); - bool flag = m_begin_sync_printer_status && !has_changed; + bool flag = m_begin_sync_printer_status && !has_changed; if (!(flag)) { auto str_bed_type = wxGetApp().app_config->get_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), "curr_bed_type"); @@ -3554,7 +3507,7 @@ void Sidebar::update_all_preset_comboboxes() bed_type_value = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); } - set_bed_type_accord_combox((BedType) bed_type_value); + set_bed_type_accord_combox((BedType) bed_type_value); } else { BedType bed_type = preset_bundle.printers.get_edited_preset().get_default_bed_type(&preset_bundle); set_bed_type_accord_combox(bed_type); @@ -3574,10 +3527,10 @@ void Sidebar::update_all_preset_comboboxes() p->panel_printer_bed->Show(is_bbl_vendor || cfg.opt_bool("support_multi_bed_types")); // Update the print choosers to only contain the compatible presets, update the dirty flags. - // BBS + //BBS // Update the printer choosers, update the dirty flags. - // p->combo_printer->update(); + //p->combo_printer->update(); // Update the filament choosers to only contain the compatible presets, update the color preview, // update the dirty flags. if (print_tech == ptFFF) { @@ -3596,12 +3549,13 @@ void Sidebar::update_all_preset_comboboxes() void Sidebar::update_presets(Preset::Type preset_type) { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + const auto print_tech = preset_bundle.printers.get_edited_preset().printer_technology(); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%") % preset_type; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%")%preset_type; switch (preset_type) { - case Preset::TYPE_FILAMENT: { + case Preset::TYPE_FILAMENT: + { // BBS #if 0 const size_t extruder_cnt = print_tech != ptFFF ? 1 : @@ -3610,16 +3564,16 @@ void Sidebar::update_presets(Preset::Type preset_type) #else const size_t filament_cnt = p->combos_filament.size(); #endif - const std::string& name = preset_bundle.filaments.get_selected_preset_name(); + const std::string &name = preset_bundle.filaments.get_selected_preset_name(); if (p->editing_filament >= 0) { preset_bundle.set_filament_preset(p->editing_filament, name); } else if (filament_cnt == 1) { // Single filament printer, synchronize the filament presets. - Preset* preset = preset_bundle.filaments.find_preset(name, false); + Preset *preset = preset_bundle.filaments.find_preset(name, false); if (preset) { - if (preset->is_compatible) - preset_bundle.set_filament_preset(0, name); + if (preset->is_compatible) preset_bundle.set_filament_preset(0, name); } + } for (size_t i = 0; i < filament_cnt; i++) @@ -3630,22 +3584,25 @@ void Sidebar::update_presets(Preset::Type preset_type) } case Preset::TYPE_PRINT: - // wxGetApp().mainframe->m_param_panel; - // p->combo_print->update(); + //wxGetApp().mainframe->m_param_panel; + //p->combo_print->update(); { - Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); - if (print_tab) { - print_tab->get_combo_box()->update(); - } - break; + Tab* print_tab = wxGetApp().get_tab(Preset::TYPE_PRINT); + if (print_tab) { + print_tab->get_combo_box()->update(); } - case Preset::TYPE_SLA_PRINT:; // p->combo_sla_print->update(); + break; + } + case Preset::TYPE_SLA_PRINT: + ;// p->combo_sla_print->update(); break; - case Preset::TYPE_SLA_MATERIAL:; // p->combo_sla_material->update(); + case Preset::TYPE_SLA_MATERIAL: + ;// p->combo_sla_material->update(); break; - case Preset::TYPE_PRINTER: { + case Preset::TYPE_PRINTER: + { update_all_preset_comboboxes(); p->show_preset_comboboxes(); @@ -3659,17 +3616,17 @@ void Sidebar::update_presets(Preset::Type preset_type) Preset& printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset(); if (auto printer_structure_opt = printer_preset.config.option>("printer_structure")) { - wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = (printer_structure_opt->value == - PrinterStructure::psI3); - } else + wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3); + } + else wxGetApp().plater()->get_current_canvas3D()->get_arrange_settings().align_to_y_axis = false; // Update dual extrudes - auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); - auto extruder_variants = printer_preset.config.option("extruder_variant_list"); + auto* nozzle_diameter = dynamic_cast(printer_preset.config.option("nozzle_diameter")); + auto extruder_variants = printer_preset.config.option("extruder_variant_list"); std::string printer_model = printer_preset.config.option("printer_model")->value; - bool isBBL = preset_bundle.is_bbl_vendor(); + bool isBBL = preset_bundle.is_bbl_vendor(); bool is_dual_extruder = extruder_variants->size() == 2; // why: agent mode drives the native device tab, so the sidebar lays out like BBL // (no physical-printer connect button). @@ -3680,40 +3637,36 @@ void Sidebar::update_presets(Preset::Type preset_type) // UI left = DEPUTY_EXTRUDER_ID(1), UI right = MAIN_EXTRUDER_ID(0) if (is_dual_extruder) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); - auto left_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::TitleCase); - auto right_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::TitleCase); + auto left_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); + auto right_title = DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase); p->left_extruder->SetTitle(_L(left_title)); p->right_extruder->SetTitle(_L(right_title)); } - auto extruders_def = printer_preset.config.def()->get("extruder_type"); - auto extruders = printer_preset.config.option("extruder_type"); - auto nozzle_volumes_def = wxGetApp().preset_bundle->project_config.def()->get("nozzle_volume_type"); - auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); - auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); - auto diameter = printer_preset.config.opt_string("printer_variant"); + auto extruders_def = printer_preset.config.def()->get("extruder_type"); + auto extruders = printer_preset.config.option("extruder_type"); + auto nozzle_volumes_def = wxGetApp().preset_bundle->project_config.def()->get("nozzle_volume_type"); + auto nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type"); + auto diameters = wxGetApp().preset_bundle->printers.diameters_of_selected_printer(); + auto diameter = printer_preset.config.opt_string("printer_variant"); auto extruder_max_nozzle_count = printer_preset.config.option("extruder_max_nozzle_count"); - auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants, - diameter, extruder_max_nozzle_count](ExtruderGroup& extruder, int index) { + auto update_extruder_variant = [printer_model, extruders_def, extruders, nozzle_volumes_def, nozzle_volumes, extruder_variants,diameter,extruder_max_nozzle_count](ExtruderGroup & extruder, int index) { extruder.combo_flow->Clear(); - auto type = extruders_def->enum_labels[extruders->values[index]]; + auto type = extruders_def->enum_labels[extruders->values[index]]; int select = -1; for (size_t i = 0; i < nozzle_volumes_def->enum_labels.size(); ++i) { // get_at falls back to the first entry when a profile defines no per-extruder value, // so extruders without an explicit sub-nozzle count never offer Hybrid. A nullable-int // nil is INT_MAX (> 1) and would otherwise falsely pass the gate, so exclude it too. if (boost::algorithm::contains(extruder_variants->values[index], type + " " + nozzle_volumes_def->enum_labels[i]) || - extruder_max_nozzle_count->get_at(index) > 1 && - extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && - nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid) { - if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow && - (diameter == "0.2" || is_skip_high_flow_printer(printer_model))) + extruder_max_nozzle_count->get_at(index) > 1 && extruder_max_nozzle_count->get_at(index) != ConfigOptionIntsNullable::nil_value() && + nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == nvtHybrid) { + if (nozzle_volumes_def->enum_keys_map->at(nozzle_volumes_def->enum_values[i]) == NozzleVolumeType::nvtHighFlow &&(diameter == "0.2" || + is_skip_high_flow_printer(printer_model))) continue; if (nozzle_volumes->values[index] == i) select = extruder.combo_flow->GetCount(); - extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*) i); + extruder.combo_flow->Append(_L(nozzle_volumes_def->enum_labels[i]), {}, (void*)i); } } if (select == -1) @@ -3727,7 +3680,7 @@ void Sidebar::update_presets(Preset::Type preset_type) // ORCA get the actual nozzle diameter from printer config auto nozzle_dia = get_diameter_string(nozzle_diameter->values[extruder_index]); // ORCA try to add nozzle diameter from config if list is empty. fixes blank nozzle combo box when preset has no alias - if (diameters[0].empty() && !nozzle_dia.empty()) { + if(diameters[0].empty() && !nozzle_dia.empty()){ diameters[0] = nozzle_dia; } // Orca: Check if the actual nozzle diameter exists in the list, if not add it as a custom option @@ -3745,38 +3698,34 @@ void Sidebar::update_presets(Preset::Type preset_type) auto image_path = get_cur_select_bed_image(); if (is_dual_extruder) { std::string printer_type = printer_preset.get_printer_type(wxGetApp().preset_bundle); - p->left_extruder->SetTitle( - _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::TitleCase))); - p->right_extruder->SetTitle( - _L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, - ToolHeadNameCase::TitleCase))); + p->left_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, DEPUTY_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); + p->right_extruder->SetTitle(_L(DevPrinterConfigUtil::get_toolhead_display_name(printer_type, MAIN_EXTRUDER_ID, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase))); AMSCountPopupWindow::UpdateAMSCount(0, p->left_extruder); AMSCountPopupWindow::UpdateAMSCount(1, p->right_extruder); update_extruder_variant(*p->left_extruder, 0); update_extruder_variant(*p->right_extruder, 1); - // if (!p->is_switching_diameter) { - update_extruder_diameter(0, *p->left_extruder); - update_extruder_diameter(1, *p->right_extruder); + //if (!p->is_switching_diameter) { + update_extruder_diameter(0, *p->left_extruder); + update_extruder_diameter(1, *p->right_extruder); //} p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); } else { AMSCountPopupWindow::UpdateAMSCount(0, p->single_extruder); update_extruder_variant(*p->single_extruder, 0); - // if (!p->is_switching_diameter) - update_extruder_diameter(0, *p->single_extruder); + //if (!p->is_switching_diameter) + update_extruder_diameter(0, *p->single_extruder); // ORCA sync unified nozzle combo box p->combo_nozzle_dia->Clear(); for (size_t i = 0; i < diameters.size(); ++i) p->combo_nozzle_dia->Append(diameters[i], {}); p->combo_nozzle_dia->SetSelection((*p->single_extruder).combo_diameter->GetSelection()); - + // ORCA update nozzle type - const auto& full_config = wxGetApp().preset_bundle->full_config(); - wxString nozzle_type = "-"; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + wxString nozzle_type = "-"; const ConfigOptionEnumsGenericNullable* cfg_nozzle_type = full_config.option("nozzle_type"); - if (cfg_nozzle_type != nullptr) { + if(cfg_nozzle_type != nullptr){ std::vector nozzle_types(cfg_nozzle_type->size()); for (size_t idx = 0; idx < cfg_nozzle_type->size(); ++idx) nozzle_types[idx] = NozzleType(cfg_nozzle_type->values[idx]); @@ -3784,8 +3733,8 @@ void Sidebar::update_presets(Preset::Type preset_type) nozzle_types[0] == ntHardenedSteel ? "Hardened Steel" : nozzle_types[0] == ntStainlessSteel ? "Stainless Steel" : nozzle_types[0] == ntTungstenCarbide ? "Tungsten Carbide" : - nozzle_types[0] == ntBrass ? "Brass" : - "-" // Undefined + nozzle_types[0] == ntBrass ? "Brass" + : "-" // Undefined ); } p->label_nozzle_type->SetLabel(nozzle_type); @@ -3811,17 +3760,19 @@ void Sidebar::update_presets(Preset::Type preset_type) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit."); } -// BBS +//BBS void Sidebar::update_presets_from_to(Slic3r::Preset::Type preset_type, std::string from, std::string to) { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%, from %2% to %3%") % preset_type % from % to; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, preset_type %1%, from %2% to %3%")%preset_type %from %to; switch (preset_type) { - case Preset::TYPE_FILAMENT: { + case Preset::TYPE_FILAMENT: + { const size_t filament_cnt = p->combos_filament.size(); - for (auto it = preset_bundle.filament_presets.begin(); it != preset_bundle.filament_presets.end(); it++) { + for (auto it = preset_bundle.filament_presets.begin(); it != preset_bundle.filament_presets.end(); it++) + { if ((*it).compare(from) == 0) { (*it) = to; } @@ -3840,8 +3791,7 @@ void Sidebar::update_presets_from_to(Slic3r::Preset::Type preset_type, std::stri BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": exit!"); } -BedType Sidebar::get_cur_select_bed_type() -{ +BedType Sidebar::get_cur_select_bed_type() { int selection = p->combo_printer_bed->GetSelection(); if (selection < 0 && selection >= m_cur_combox_bed_types.size()) { p->combo_printer_bed->SetSelection(0); @@ -3853,14 +3803,13 @@ BedType Sidebar::get_cur_select_bed_type() std::string Sidebar::get_cur_select_bed_image() { - auto select_bed_type = get_cur_select_bed_type(); - // auto series_suffix_str = m_cur_image_bed_type.empty() ? "" : ("_" + m_cur_image_bed_type); - auto image_path = bed_type_thumbnails[select_bed_type]; // + series_suffix_str; + auto select_bed_type = get_cur_select_bed_type(); + //auto series_suffix_str = m_cur_image_bed_type.empty() ? "" : ("_" + m_cur_image_bed_type); + auto image_path = bed_type_thumbnails[select_bed_type];// + series_suffix_str; return image_path; } -void Sidebar::set_bed_type_accord_combox(BedType bed_type) -{ +void Sidebar::set_bed_type_accord_combox(BedType bed_type) { for (size_t i = 0; i < m_cur_combox_bed_types.size(); i++) { if (m_cur_combox_bed_types[i] == bed_type) { p->combo_printer_bed->SelectAndNotify(i); @@ -3884,28 +3833,28 @@ bool Sidebar::reset_bed_type_combox_choices(bool is_sidebar_init) } if (m_last_combo_bedtype_count != 0 && pm) { auto cur_count = (int) BedType::btCount - 1 - pm->not_support_bed_types.size(); - if (cur_count == m_last_combo_bedtype_count) { // no change + if (cur_count == m_last_combo_bedtype_count) {//no change return false; } } - const ConfigOptionDef* bed_type_def = print_config_def.get("curr_bed_type"); + const ConfigOptionDef *bed_type_def = print_config_def.get("curr_bed_type"); p->combo_printer_bed->Clear(); m_cur_combox_bed_types.clear(); - if (pm && bed_type_def && bed_type_def->enum_keys_map) { + if (pm &&bed_type_def && bed_type_def->enum_keys_map) { int index = 0; for (auto item : bed_type_def->enum_labels) { index++; - bool find = std::find(pm->not_support_bed_types.begin(), pm->not_support_bed_types.end(), item) != - pm->not_support_bed_types.end(); + bool find = std::find(pm->not_support_bed_types.begin(), pm->not_support_bed_types.end(), item) != pm->not_support_bed_types.end(); if (find) { continue; } - m_cur_combox_bed_types.emplace_back(BedType(index)); // BedType //btPC =1 + m_cur_combox_bed_types.emplace_back(BedType(index));//BedType //btPC =1 p->combo_printer_bed->AppendString(_L(item)); } - } else { + } + else { m_cur_image_bed_type = ""; - int index = 0; + int index = 0; for (auto item : bed_type_def->enum_labels) { index++; m_cur_combox_bed_types.emplace_back(BedType(index)); // BedType //btPC =1 @@ -3930,6 +3879,7 @@ void Sidebar::change_top_border_for_mode_sizer(bool increase_border) #endif } + // ---- Mixed-color filament sidebar support ---- // The mixed rows get their own scroll area, capped by Orca's filaments_area_preferred_count // row budget rather than BBS's fixed 3-row / 12-filament limit. @@ -3939,11 +3889,11 @@ void Sidebar::recalc_filament_scroll_sizes() return; // Same preferred-row budget the physical list uses, so both lists cap consistently. - auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); - auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); - const int row_h = combo_sizer ? combo_sizer->GetSize().GetHeight() : 0; + auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); + auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); + const int row_h = combo_sizer ? combo_sizer->GetSize().GetHeight() : 0; int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); - const int max_h = (row_h > 0) ? preferred_rows * row_h : -1; + const int max_h = (row_h > 0) ? preferred_rows * row_h : -1; auto content_size = p->m_mixed_scroll_area->GetSizer()->GetMinSize(); if (max_h > 0 && content_size.y > max_h) { @@ -3954,9 +3904,9 @@ void Sidebar::recalc_filament_scroll_sizes() } p->m_mixed_scroll_area->SetMinSize({0, content_size.y}); } -static std::string blend_mixed_color(const std::vector& comp_ids, - const std::vector& ratios, - const std::vector& color_strs) +static std::string blend_mixed_color(const std::vector &comp_ids, + const std::vector &ratios, + const std::vector &color_strs) { std::vector hex_colors; hex_colors.reserve(comp_ids.size()); @@ -3968,8 +3918,7 @@ static std::string blend_mixed_color(const std::vector& comp_ids, void Sidebar::update_mixed_filament_list() { auto* plater = dynamic_cast(GetParent()); - if (!plater) - return; + if (!plater) return; wxWindowUpdateLocker noUpdates(this); @@ -3979,17 +3928,17 @@ void Sidebar::update_mixed_filament_list() const wxColour mc_dim = StateColor::darkModeColorFor(wxColour("#ACACAC")); auto& project_config = wxGetApp().preset_bundle->project_config; - auto mixed_indices = plater->mixed_filament_config_indices(); - size_t num_physical = p->combos_filament.size(); + auto mixed_indices = plater->mixed_filament_config_indices(); + size_t num_physical = p->combos_filament.size(); - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - auto* components_opt = project_config.option("filament_mixed_components"); - auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); - auto* colours_opt = project_config.option("filament_colour"); - auto* grad_opt = project_config.option("filament_mixed_gradient"); - auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* components_opt = project_config.option("filament_mixed_components"); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + auto* colours_opt = project_config.option("filament_colour"); + auto* grad_opt = project_config.option("filament_mixed_gradient"); + auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); - bool can_mix = (num_physical >= 2); + bool can_mix = (num_physical >= 2); bool has_mixed = can_mix && !mixed_indices.empty(); // Check integrity of mixed filament component references @@ -4011,11 +3960,11 @@ void Sidebar::update_mixed_filament_list() ft = preset->config.get_filament_type(display_type); } } - if (ft.empty()) - ft = "PLA"; + if (ft.empty()) ft = "PLA"; physical_types.push_back(ft); } - auto type_mismatch_slots = check_mixed_filament_type_consistency(is_mixed_opt->values, components_opt->values, physical_types); + auto type_mismatch_slots = check_mixed_filament_type_consistency( + is_mixed_opt->values, components_opt->values, physical_types); for (size_t s : type_mismatch_slots) broken_set.insert(s); broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); @@ -4035,8 +3984,8 @@ void Sidebar::update_mixed_filament_list() auto* notify = wxGetApp().plater()->get_notification_manager(); if (notify) notify->push_notification(NotificationType::BBLMixedFilamentBroken, - NotificationManager::NotificationLevel::ErrorNotificationLevel, - _u8L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); + NotificationManager::NotificationLevel::ErrorNotificationLevel, + _u8L("Mixed filament has invalid or mismatched components. Please re-edit affected entries.")); } else { auto* notify = wxGetApp().plater()->get_notification_manager(); if (notify) @@ -4057,7 +4006,7 @@ void Sidebar::update_mixed_filament_list() auto make_swatch_panel = [this](wxWindow* parent, const wxColour& col, unsigned int num) -> wxPanel* { int swatch_sz = FromDIP(20); - auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + auto* panel = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); bool is_dark = wxGetApp().dark_mode(); panel->Bind(wxEVT_PAINT, [panel, col, num, is_dark](wxPaintEvent&) { @@ -4079,13 +4028,14 @@ void Sidebar::update_mixed_filament_list() dc.SetFont(::Label::Body_14); wxSize txt_sz = dc.GetTextExtent(txt); dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); - dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, (sz.GetHeight() - txt_sz.GetHeight()) / 2); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, + (sz.GetHeight() - txt_sz.GetHeight()) / 2); }); return panel; }; for (size_t i = 0; i < mixed_indices.size(); ++i) { - size_t cfg_idx = mixed_indices[i]; + size_t cfg_idx = mixed_indices[i]; auto* combo_and_btn_sizer = new wxBoxSizer(wxHORIZONTAL); combo_and_btn_sizer->Add(FromDIP(10), 0, 0, 0, 0); @@ -4109,13 +4059,15 @@ void Sidebar::update_mixed_filament_list() while (std::getline(iss, tok, ',')) { float v = 0; if (std::sscanf(tok.c_str(), "%f", &v) == 1) - comp_ratios.push_back((int) (v * 100 + 0.5f)); + comp_ratios.push_back((int)(v * 100 + 0.5f)); } } if (!comp_ids.empty() && comp_ratios.size() != comp_ids.size()) { - BOOST_LOG_TRIVIAL(warning) << "Mixed filament slot " << cfg_idx << ": ratio count (" << comp_ratios.size() - << ") != component count (" << comp_ids.size() << "), resetting to even distribution"; - int n = (int) comp_ids.size(); + BOOST_LOG_TRIVIAL(warning) << "Mixed filament slot " << cfg_idx + << ": ratio count (" << comp_ratios.size() + << ") != component count (" << comp_ids.size() + << "), resetting to even distribution"; + int n = (int)comp_ids.size(); comp_ratios.assign(n, 100 / n); comp_ratios[0] += 100 - (100 / n) * n; } @@ -4136,8 +4088,8 @@ void Sidebar::update_mixed_filament_list() } } - bool is_gradient = false; - int gradient_direction = 0; + bool is_gradient = false; + int gradient_direction = 0; if (grad_opt && cfg_idx < grad_opt->values.size()) is_gradient = grad_opt->values[cfg_idx]; if (is_gradient && grad_range_opt && cfg_idx < grad_range_opt->values.size()) { @@ -4147,18 +4099,20 @@ void Sidebar::update_mixed_filament_list() gradient_direction = (v0 > v1) ? 0 : 1; } - std::string mix_color_str = (colours_opt && cfg_idx < colours_opt->values.size()) ? colours_opt->values[cfg_idx] : "#888888"; + std::string mix_color_str = (colours_opt && cfg_idx < colours_opt->values.size()) + ? colours_opt->values[cfg_idx] : "#888888"; wxColour mix_col(mix_color_str); - unsigned int mix_num = (unsigned int) (cfg_idx + 1); + unsigned int mix_num = (unsigned int)(cfg_idx + 1); // The swatch fades bottom to top over the model's height, sampled the same way // the slicer builds the sublayers, so it matches the editor's Effect Preview. The // ramp comes back empty for every slot that is not a two component gradient mix. - const int swatch_sz = FromDIP(20); + const int swatch_sz = FromDIP(20); const std::vector gradient_ramp = mixed_gradient_ramp(project_config, cfg_idx, swatch_sz); if (!gradient_ramp.empty()) { - auto* grad_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY, wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); + auto* grad_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY, + wxDefaultPosition, wxSize(swatch_sz, swatch_sz)); grad_panel->SetMinSize(wxSize(swatch_sz, swatch_sz)); grad_panel->SetBackgroundStyle(wxBG_STYLE_PAINT); grad_panel->Bind(wxEVT_PAINT, [grad_panel, gradient_ramp, mix_num](wxPaintEvent&) { @@ -4171,12 +4125,13 @@ void Sidebar::update_mixed_filament_list() // The number sits at the swatch's middle, so take its contrast from the // colour printed at mid height rather than from either endpoint. dc.SetTextForeground(gradient_ramp[gradient_ramp.size() / 2].GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); - dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, (sz.GetHeight() - txt_sz.GetHeight()) / 2); + dc.DrawText(txt, (sz.GetWidth() - txt_sz.GetWidth()) / 2, + (sz.GetHeight() - txt_sz.GetHeight()) / 2); }); combo_and_btn_sizer->Add(grad_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); } else { - combo_and_btn_sizer->Add(make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num), 0, - wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); + combo_and_btn_sizer->Add(make_swatch_panel(p->m_panel_mixed_content, mix_col, mix_num), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4)); } auto* content_panel = new wxPanel(p->m_panel_mixed_content, wxID_ANY); @@ -4194,11 +4149,11 @@ void Sidebar::update_mixed_filament_list() // Build per-component colour list for the lambda std::vector cp_colours; - std::vector cp_valid; + std::vector cp_valid; std::vector cp_ids = comp_ids; - std::vector cp_ratios = comp_ratios; - bool cp_is_gradient = is_gradient; - int cp_gradient_dir = gradient_direction; + std::vector cp_ratios = comp_ratios; + bool cp_is_gradient = is_gradient; + int cp_gradient_dir = gradient_direction; for (size_t ci = 0; ci < comp_ids.size(); ++ci) { bool valid = (comp_ids[ci] >= 1 && comp_ids[ci] <= physical_colors.size()); cp_valid.push_back(valid); @@ -4207,16 +4162,17 @@ void Sidebar::update_mixed_filament_list() // Reorder for gradient display: from -> to std::vector draw_ids; - std::vector draw_ratios; - std::vector draw_colours; - std::vector draw_valid; + std::vector draw_ratios; + std::vector draw_colours; + std::vector draw_valid; if (cp_is_gradient && cp_ids.size() == 2) { - int fi = (cp_gradient_dir == 0) ? 0 : 1; - int ti = 1 - fi; - draw_ids = {cp_ids[fi], cp_ids[ti]}; - draw_ratios = {cp_ratios.size() > (size_t) fi ? cp_ratios[fi] : 0, cp_ratios.size() > (size_t) ti ? cp_ratios[ti] : 0}; - draw_colours = {cp_colours[fi], cp_colours[ti]}; - draw_valid = {cp_valid[fi], cp_valid[ti]}; + int fi = (cp_gradient_dir == 0) ? 0 : 1; + int ti = 1 - fi; + draw_ids = { cp_ids[fi], cp_ids[ti] }; + draw_ratios = { cp_ratios.size() > (size_t)fi ? cp_ratios[fi] : 0, + cp_ratios.size() > (size_t)ti ? cp_ratios[ti] : 0 }; + draw_colours = { cp_colours[fi], cp_colours[ti] }; + draw_valid = { cp_valid[fi], cp_valid[ti] }; } else { draw_ids = cp_ids; draw_ratios = cp_ratios; @@ -4224,9 +4180,11 @@ void Sidebar::update_mixed_filament_list() draw_valid = cp_valid; } - content_panel->Bind(wxEVT_PAINT, [content_panel, mc_bg, mc_border, mc_text, mc_dim, cp_pad, cp_swatch_sz, cp_sep_margin, - cp_pct_left, cp_gap, cp_pct_gap, cp_is_dark, cp_is_gradient, draw_ids, draw_ratios, - draw_colours, draw_valid](wxPaintEvent&) { + content_panel->Bind(wxEVT_PAINT, [content_panel, mc_bg, mc_border, mc_text, mc_dim, + cp_pad, cp_swatch_sz, cp_sep_margin, cp_pct_left, + cp_gap, cp_pct_gap, cp_is_dark, + cp_is_gradient, + draw_ids, draw_ratios, draw_colours, draw_valid](wxPaintEvent&) { wxBufferedPaintDC dc(content_panel); wxSize sz = content_panel->GetClientSize(); @@ -4235,22 +4193,24 @@ void Sidebar::update_mixed_filament_list() dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight()); dc.SetFont(::Label::Body_13); - int x = cp_pad; - int y_swatch = (sz.GetHeight() - cp_swatch_sz) / 2; - int text_h = dc.GetTextExtent(wxT("A")).GetHeight(); - int y_text = y_swatch + (cp_swatch_sz - text_h) / 2; - int avail = sz.GetWidth() - cp_pad; + int x = cp_pad; + int y_swatch = (sz.GetHeight() - cp_swatch_sz) / 2; + int text_h = dc.GetTextExtent(wxT("A")).GetHeight(); + int y_text = y_swatch + (cp_swatch_sz - text_h) / 2; + int avail = sz.GetWidth() - cp_pad; wxString ellipsis = wxT("..."); - int ellipsis_w = dc.GetTextExtent(ellipsis).GetWidth(); + int ellipsis_w = dc.GetTextExtent(ellipsis).GetWidth(); - auto fits = [&](int needed) -> bool { return (x + needed) <= (avail - ellipsis_w); }; + auto fits = [&](int needed) -> bool { + return (x + needed) <= (avail - ellipsis_w); + }; size_t n = draw_ids.size(); for (size_t ci = 0; ci < n; ++ci) { // Separator: "+" or arrow if (ci > 0) { wxString sep = cp_is_gradient ? wxT("\u2192") : wxT("+"); - int sep_w = dc.GetTextExtent(sep).GetWidth() + cp_sep_margin * 2; + int sep_w = dc.GetTextExtent(sep).GetWidth() + cp_sep_margin * 2; if (!fits(sep_w + cp_swatch_sz)) { dc.SetTextForeground(mc_text); dc.DrawText(ellipsis, x, y_text); @@ -4284,27 +4244,29 @@ void Sidebar::update_mixed_filament_list() dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); } dc.SetFont(::Label::Body_14); - wxString num = wxString::Format("%u", draw_ids[ci]); + wxString num = wxString::Format("%u", draw_ids[ci]); wxSize num_sz = dc.GetTextExtent(num); dc.SetTextForeground(col.GetLuminance() > 0.5 ? wxColour(50, 58, 61) : *wxWHITE); - dc.DrawText(num, x + (cp_swatch_sz - num_sz.GetWidth()) / 2, y_swatch + (cp_swatch_sz - num_sz.GetHeight()) / 2); + dc.DrawText(num, x + (cp_swatch_sz - num_sz.GetWidth()) / 2, + y_swatch + (cp_swatch_sz - num_sz.GetHeight()) / 2); dc.SetFont(::Label::Body_13); } else { dc.SetBrush(wxBrush(mc_bg)); dc.SetPen(wxPen(mc_dim, 1)); dc.DrawRectangle(x, y_swatch, cp_swatch_sz, cp_swatch_sz); - wxString dash = wxT("\u2014"); + wxString dash = wxT("\u2014"); wxSize dash_sz = dc.GetTextExtent(dash); dc.SetTextForeground(mc_dim); - dc.DrawText(dash, x + (cp_swatch_sz - dash_sz.GetWidth()) / 2, y_swatch + (cp_swatch_sz - dash_sz.GetHeight()) / 2); + dc.DrawText(dash, x + (cp_swatch_sz - dash_sz.GetWidth()) / 2, + y_swatch + (cp_swatch_sz - dash_sz.GetHeight()) / 2); } x += cp_swatch_sz + cp_gap; // Ratio text (skip for gradient) if (!cp_is_gradient) { - int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; + int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; wxString pct = wxString::Format("%d%%", r); - int pct_w = dc.GetTextExtent(pct).GetWidth(); + int pct_w = dc.GetTextExtent(pct).GetWidth(); if (!fits(pct_w)) { dc.SetTextForeground(mc_text); dc.DrawText(ellipsis, x, y_text); @@ -4321,8 +4283,7 @@ void Sidebar::update_mixed_filament_list() { wxString tip; for (size_t ci = 0; ci < draw_ids.size(); ++ci) { - if (ci > 0) - tip += cp_is_gradient ? wxT(" \u2192 ") : wxT(" + "); + if (ci > 0) tip += cp_is_gradient ? wxT(" \u2192 ") : wxT(" + "); int r = (ci < draw_ratios.size()) ? draw_ratios[ci] : 0; tip += wxString::Format("%u (%d%%)", draw_ids[ci], r); } @@ -4341,19 +4302,22 @@ void Sidebar::update_mixed_filament_list() combo_and_btn_sizer->Add(content_panel, 1, wxALL | wxEXPAND, FromDIP(2))->SetMinSize({-1, FromDIP(30)}); - auto* menu_btn = new ScalableButton(p->m_panel_mixed_content, wxID_ANY, is_broken ? "error" : "menu_filament"); + auto* menu_btn = new ScalableButton(p->m_panel_mixed_content, wxID_ANY, + is_broken ? "error" : "menu_filament"); menu_btn->SetToolTip(is_broken ? _L("Mixed filament has broken component references") : _L("Edit / Delete / Merge")); menu_btn->Bind(wxEVT_BUTTON, [this, panel_idx, cfg_idx](wxCommandEvent&) { wxMenu menu; auto* edit_item = menu.Append(wxID_ANY, _L("Edit")); - menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { edit_mixed_filament(panel_idx); }, edit_item->GetId()); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { + edit_mixed_filament(panel_idx); + }, edit_item->GetId()); - wxMenu* sub_menu = new wxMenu(); + wxMenu* sub_menu = new wxMenu(); std::vector icons = get_extruder_color_icons(true); - int filaments_cnt = icons.size(); + int filaments_cnt = icons.size(); for (int j = 0; j < filaments_cnt; ++j) { - if ((size_t) j == cfg_idx) + if ((size_t)j == cfg_idx) continue; wxString item_name; @@ -4361,8 +4325,10 @@ void Sidebar::update_mixed_filament_list() if (is_target_mixed) { item_name = wxString::Format(_L("Filament %d"), j + 1); } else { - auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[j]); - item_name = preset ? from_u8(preset->label(false)) : wxString::Format(_L("Filament %d"), j + 1); + auto preset = wxGetApp().preset_bundle->filaments.find_preset( + wxGetApp().preset_bundle->filament_presets[j]); + item_name = preset ? from_u8(preset->label(false)) + : wxString::Format(_L("Filament %d"), j + 1); } auto* mi = new wxMenuItem(sub_menu, wxID_ANY, item_name); @@ -4370,7 +4336,9 @@ void Sidebar::update_mixed_filament_list() mi->SetBitmap(*icons[j]); #endif sub_menu->Append(mi); - sub_menu->Bind(wxEVT_MENU, [this, cfg_idx, j](wxCommandEvent&) { change_filament(cfg_idx, j); }, mi->GetId()); + sub_menu->Bind(wxEVT_MENU, [this, cfg_idx, j](wxCommandEvent&) { + change_filament(cfg_idx, j); + }, mi->GetId()); } if (filaments_cnt > 1) menu.AppendSubMenu(sub_menu, _L("Merge with")); @@ -4381,7 +4349,9 @@ void Sidebar::update_mixed_filament_list() // ORCA use delete item on end of menu to prevent accidental clicks. clicking to submenus(merge) already not allowed by OS auto* del_item = menu.Append(wxID_ANY, _L("Delete")); - menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { delete_mixed_filament_at(panel_idx); }, del_item->GetId()); + menu.Bind(wxEVT_MENU, [this, panel_idx](wxCommandEvent&) { + delete_mixed_filament_at(panel_idx); + }, del_item->GetId()); PopupMenu(&menu); }); @@ -4389,10 +4359,9 @@ void Sidebar::update_mixed_filament_list() combo_and_btn_sizer->Add(FromDIP(16), 0, 0, 0, 0); - int side = i % 2; + int side = i % 2; auto* col = (side == 0) ? left_col : right_col; - if (side == 1 && i > 1) - col->Remove(i / 2); + if (side == 1 && i > 1) col->Remove(i / 2); col->Add(combo_and_btn_sizer, 1, wxEXPAND); if (side == 0 && i > 0) { right_col->AddStretchSpacer(1); @@ -4421,8 +4390,7 @@ void Sidebar::update_mixed_filament_list() p->m_mixed_filament_broken = false; if (!broken_slots.empty()) { std::set broken_1based; - for (size_t s : broken_slots) - broken_1based.insert(s + 1); + for (size_t s : broken_slots) broken_1based.insert(s + 1); auto* curr_plate = plater->get_partplate_list().get_curr_plate(); if (curr_plate) { @@ -4431,7 +4399,7 @@ void Sidebar::update_mixed_filament_list() continue; // Check object-level extruder int obj_ext = obj->config.has("extruder") ? obj->config.extruder() : 1; - if (broken_1based.count((size_t) obj_ext)) { + if (broken_1based.count((size_t)obj_ext)) { p->m_mixed_filament_broken = true; break; } @@ -4439,40 +4407,26 @@ void Sidebar::update_mixed_filament_list() for (auto* vol : obj->volumes) { // Check volume-level extruder int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; - if (broken_1based.count((size_t) vol_ext)) { - found = true; - break; - } + if (broken_1based.count((size_t)vol_ext)) { found = true; break; } // Check color painting data (mmu segmentation facets) if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { for (size_t broken_slot : broken_1based) { - if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) { - found = true; - break; - } + if (vol->mmu_segmentation_facets.has_facets(*vol, EnforcerBlockerType(broken_slot))) + { found = true; break; } } - if (found) - break; + if (found) break; } } - if (found) { - p->m_mixed_filament_broken = true; - break; - } + if (found) { p->m_mixed_filament_broken = true; break; } // Check height range modifier extruder overrides for (auto& [range, cfg] : obj->layer_config_ranges) { if (cfg.has("extruder")) { int layer_ext = cfg.option("extruder")->getInt(); - if (layer_ext > 0 && broken_1based.count((size_t) layer_ext)) { - found = true; - break; - } + if (layer_ext > 0 && broken_1based.count((size_t)layer_ext)) + { found = true; break; } } } - if (found) { - p->m_mixed_filament_broken = true; - break; - } + if (found) { p->m_mixed_filament_broken = true; break; } } } } @@ -4488,34 +4442,31 @@ void Sidebar::update_mixed_filament_list() mf->update_slice_print_status(MainFrame::eEventObjectUpdate, false); } - if (auto* tab = dynamic_cast(wxGetApp().plate_tab)) + if (auto *tab = dynamic_cast(wxGetApp().plate_tab)) tab->update_mixed_filament_seq_state(); + } bool Sidebar::has_broken_mixed_filament() const { auto* plater = dynamic_cast(GetParent()); - if (!plater) - return false; + if (!plater) return false; return has_broken_mixed_filament(plater->get_partplate_list().get_curr_plate()); } bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const { - if (!plate) - return false; + if (!plate) return false; auto* plater = dynamic_cast(GetParent()); - if (!plater) - return false; + if (!plater) return false; auto& project_config = wxGetApp().preset_bundle->project_config; - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - auto* comp_strs_opt = project_config.option("filament_mixed_components"); - if (!is_mixed_opt || !comp_strs_opt) - return false; + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* comp_strs_opt = project_config.option("filament_mixed_components"); + if (!is_mixed_opt || !comp_strs_opt) return false; size_t num_physical = p->combos_filament.size(); - auto broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, comp_strs_opt->values, num_physical); + auto broken_slots = check_mixed_filament_integrity(is_mixed_opt->values, comp_strs_opt->values, num_physical); // Type consistency check { @@ -4530,20 +4481,18 @@ bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const ft = preset->config.get_filament_type(display_type); } } - if (ft.empty()) - ft = "PLA"; + if (ft.empty()) ft = "PLA"; physical_types.push_back(ft); } - auto type_mismatch_slots = check_mixed_filament_type_consistency(is_mixed_opt->values, comp_strs_opt->values, physical_types); + auto type_mismatch_slots = check_mixed_filament_type_consistency( + is_mixed_opt->values, comp_strs_opt->values, physical_types); broken_slots.insert(broken_slots.end(), type_mismatch_slots.begin(), type_mismatch_slots.end()); } - if (broken_slots.empty()) - return false; + if (broken_slots.empty()) return false; std::set broken_1based; - for (size_t s : broken_slots) - broken_1based.insert(s + 1); + for (size_t s : broken_slots) broken_1based.insert(s + 1); // Scan model objects on the given plate for raw extruder assignments // (don't use get_extruders() which expands mixed slots) @@ -4552,12 +4501,12 @@ bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const continue; // Check object-level extruder int obj_ext = entry->config.has("extruder") ? entry->config.extruder() : 1; - if (broken_1based.count((size_t) obj_ext)) + if (broken_1based.count((size_t)obj_ext)) return true; for (auto* vol : entry->volumes) { // Check volume-level extruder int vol_ext = vol->config.has("extruder") ? vol->config.extruder() : obj_ext; - if (broken_1based.count((size_t) vol_ext)) + if (broken_1based.count((size_t)vol_ext)) return true; // Check color painting data (mmu segmentation facets) if (vol->is_model_part() && !vol->mmu_segmentation_facets.empty()) { @@ -4571,7 +4520,7 @@ bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const for (auto& [range, cfg] : entry->layer_config_ranges) { if (cfg.has("extruder")) { int layer_ext = cfg.option("extruder")->getInt(); - if (layer_ext > 0 && broken_1based.count((size_t) layer_ext)) + if (layer_ext > 0 && broken_1based.count((size_t)layer_ext)) return true; } } @@ -4581,9 +4530,9 @@ bool Sidebar::has_broken_mixed_filament(const PartPlate* plate) const } void Sidebar::collect_physical_filament_info(std::vector& color_strs, - std::vector& names, - std::vector& types, - std::vector* config_indices) + std::vector& names, + std::vector& types, + std::vector* config_indices) { color_strs.clear(); names.clear(); @@ -4591,9 +4540,9 @@ void Sidebar::collect_physical_filament_info(std::vector& color_str if (config_indices) config_indices->clear(); - size_t num_physical = p->combos_filament.size(); + size_t num_physical = p->combos_filament.size(); auto& project_config = wxGetApp().preset_bundle->project_config; - auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); std::vector physical_indices; const size_t total = wxGetApp().preset_bundle->filament_presets.size(); physical_indices.reserve(num_physical); @@ -4623,7 +4572,7 @@ void Sidebar::collect_physical_filament_info(std::vector& color_str auto& preset_bundle = *wxGetApp().preset_bundle; for (size_t i = 0; i < num_physical; ++i) { const size_t cfg_idx = physical_indices[i]; - Preset* preset = nullptr; + Preset* preset = nullptr; if (cfg_idx < preset_bundle.filament_presets.size()) preset = preset_bundle.filaments.find_preset(preset_bundle.filament_presets[cfg_idx]); std::string ft; @@ -4631,8 +4580,7 @@ void Sidebar::collect_physical_filament_info(std::vector& color_str std::string display_type; ft = preset->config.get_filament_type(display_type); } - if (ft.empty()) - ft = "PLA"; + if (ft.empty()) ft = "PLA"; types.push_back(ft); } } @@ -4648,19 +4596,22 @@ static std::string serialize_mixed_gradient_curve_if_custom(const MixedFilamentR if (!(result.components.size() == 2 && !result.gradient_curve.empty())) return {}; - const double y0 = (result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; - const double y1 = (result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; + const double y0 = (result.gradient_direction == 0) ? kGradientMaxRatio : kGradientMinRatio; + const double y1 = (result.gradient_direction == 0) ? kGradientMinRatio : kGradientMaxRatio; const double eps = 1e-4; if (result.gradient_curve.size() == 2) { const auto& a0 = result.gradient_curve[0]; const auto& a1 = result.gradient_curve[1]; // Default curve also requires no tangent overrides; any finite tangent // means the user bent the segment, so we must serialize it. - const bool is_default = std::abs(a0.x - 0.0) < eps && std::abs(a1.x - 1.0) < eps && std::abs(a0.y - y0) < eps && - std::abs(a1.y - y1) < eps && !std::isfinite(a0.m_in) && !std::isfinite(a0.m_out) && - !std::isfinite(a1.m_in) && !std::isfinite(a1.m_out); - if (is_default) - return {}; + const bool is_default = + std::abs(a0.x - 0.0) < eps + && std::abs(a1.x - 1.0) < eps + && std::abs(a0.y - y0) < eps + && std::abs(a1.y - y1) < eps + && !std::isfinite(a0.m_in) && !std::isfinite(a0.m_out) + && !std::isfinite(a1.m_in) && !std::isfinite(a1.m_out); + if (is_default) return {}; } Slic3r::GradientCurve gc; @@ -4668,9 +4619,10 @@ static std::string serialize_mixed_gradient_curve_if_custom(const MixedFilamentR return Slic3r::serialize_gradient_curve(gc); } -static bool create_mixed_filament_from_result(Sidebar* sidebar, - const MixedFilamentResult& result, - const std::vector& color_strs) +static bool create_mixed_filament_from_result( + Sidebar* sidebar, + const MixedFilamentResult& result, + const std::vector& color_strs) { if (!sidebar || result.components.size() < 2 || result.ratios.size() < 2) return false; @@ -4684,16 +4636,15 @@ static bool create_mixed_filament_from_result(Sidebar* sidebar, return false; auto& project_config = wxGetApp().preset_bundle->project_config; - size_t total = wxGetApp().preset_bundle->filament_presets.size(); - size_t new_idx = total; + size_t total = wxGetApp().preset_bundle->filament_presets.size(); + size_t new_idx = total; std::string mixed_color = blend_mixed_color(result.components, result.ratios, color_strs); wxGetApp().preset_bundle->set_num_filaments(total + 1, mixed_color); auto* multi_colour_opt = project_config.option("filament_multi_colour"); if (multi_colour_opt) { - while (multi_colour_opt->values.size() <= new_idx) - multi_colour_opt->values.push_back(""); + while (multi_colour_opt->values.size() <= new_idx) multi_colour_opt->values.push_back(""); multi_colour_opt->values[new_idx] = mixed_color; } @@ -4701,69 +4652,60 @@ static bool create_mixed_filament_from_result(Sidebar* sidebar, // size-guarded so a sizing bug degrades into a no-op rather than a heap overwrite. { auto* is_mixed_opt = project_config.option("filament_is_mixed"); - while (is_mixed_opt->values.size() <= new_idx) - is_mixed_opt->values.push_back(false); + while (is_mixed_opt->values.size() <= new_idx) is_mixed_opt->values.push_back(false); is_mixed_opt->values[new_idx] = true; } std::string comp_str; for (size_t i = 0; i < result.components.size(); ++i) { - if (i > 0) - comp_str += ","; + if (i > 0) comp_str += ","; comp_str += std::to_string(result.components[i]); } { auto* comp_opt = project_config.option("filament_mixed_components"); - while (comp_opt->values.size() <= new_idx) - comp_opt->values.push_back(std::string{}); + while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{}); comp_opt->values[new_idx] = comp_str; } int ratio_sum = 0; - for (int r : result.ratios) - ratio_sum += r; - if (ratio_sum <= 0) - ratio_sum = 100; + for (int r : result.ratios) ratio_sum += r; + if (ratio_sum <= 0) ratio_sum = 100; std::string ratio_str; { CNumericLocalesSetter c_locale_setter; for (size_t i = 0; i < result.ratios.size(); ++i) { - if (i > 0) - ratio_str += ","; + if (i > 0) ratio_str += ","; char buf[32]; - std::snprintf(buf, sizeof(buf), "%.4f", (float) result.ratios[i] / ratio_sum); + std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum); ratio_str += buf; } } { auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); - while (ratios_opt->values.size() <= new_idx) - ratios_opt->values.push_back(std::string{}); + while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{}); ratios_opt->values[new_idx] = ratio_str; } if (!project_config.option("filament_mixed_gradient")) project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); if (!project_config.option("filament_mixed_gradient_range")) - project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""})); + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""}) ); if (!project_config.option("filament_mixed_gradient_curve")) - project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""})); + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""}) ); if (!project_config.option("filament_mixed_gradient_per_part")) project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); { auto* grad_opt = project_config.option("filament_mixed_gradient"); - while (grad_opt->values.size() <= new_idx) - grad_opt->values.push_back(false); + while (grad_opt->values.size() <= new_idx) grad_opt->values.push_back(false); grad_opt->values[new_idx] = result.gradient_enabled; } { auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); - while (grad_range_opt->values.size() <= new_idx) - grad_range_opt->values.push_back(""); + while (grad_range_opt->values.size() <= new_idx) grad_range_opt->values.push_back(""); if (result.gradient_enabled && result.components.size() == 2) { - const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; grad_range_opt->values[new_idx] = fmt; } else { grad_range_opt->values[new_idx] = ""; @@ -4771,14 +4713,12 @@ static bool create_mixed_filament_from_result(Sidebar* sidebar, } { auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); - while (grad_curve_opt->values.size() <= new_idx) - grad_curve_opt->values.push_back(""); + while (grad_curve_opt->values.size() <= new_idx) grad_curve_opt->values.push_back(""); grad_curve_opt->values[new_idx] = serialize_mixed_gradient_curve_if_custom(result); } { auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); - while (per_part_opt->values.size() <= new_idx) - per_part_opt->values.push_back(false); + while (per_part_opt->values.size() <= new_idx) per_part_opt->values.push_back(false); per_part_opt->values[new_idx] = result.gradient_enabled && result.per_part_gradient; } @@ -4801,14 +4741,11 @@ static bool create_mixed_filament_from_result(Sidebar* sidebar, void Sidebar::add_mixed_filament() { auto* plater = dynamic_cast(GetParent()); - if (!plater) - return; + if (!plater) return; size_t num_physical = p->combos_filament.size(); - if (num_physical < 2) - return; - if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) - return; + if (num_physical < 2) return; + if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax)) return; std::vector color_strs, names, types; collect_physical_filament_info(color_strs, names, types); @@ -4823,12 +4760,10 @@ void Sidebar::add_mixed_filament() void Sidebar::edit_mixed_filament(size_t panel_idx) { auto* plater = dynamic_cast(GetParent()); - if (!plater) - return; + if (!plater) return; auto mixed_indices = plater->mixed_filament_config_indices(); - if (panel_idx >= mixed_indices.size()) - return; + if (panel_idx >= mixed_indices.size()) return; size_t cfg_idx = mixed_indices[panel_idx]; std::vector color_strs, names, types; @@ -4837,7 +4772,7 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) auto& project_config = wxGetApp().preset_bundle->project_config; MixedFilamentResult existing; auto* components_opt = project_config.option("filament_mixed_components"); - auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); // Parse existing components if (components_opt && cfg_idx < components_opt->values.size()) { @@ -4859,16 +4794,18 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) while (std::getline(iss, tok, ',')) { float v = 0; if (std::sscanf(tok.c_str(), "%f", &v) == 1) - existing.ratios.push_back((int) (v * 100 + 0.5f)); + existing.ratios.push_back((int)(v * 100 + 0.5f)); } } if (existing.components.size() < 2) { existing.components = {1, 2}; - existing.ratios = {50, 50}; + existing.ratios = {50, 50}; } else if (existing.ratios.size() != existing.components.size()) { - BOOST_LOG_TRIVIAL(warning) << "Mixed filament edit: ratio count (" << existing.ratios.size() << ") != component count (" - << existing.components.size() << "), resetting to even distribution"; - int n = (int) existing.components.size(); + BOOST_LOG_TRIVIAL(warning) << "Mixed filament edit: ratio count (" + << existing.ratios.size() << ") != component count (" + << existing.components.size() + << "), resetting to even distribution"; + int n = (int)existing.components.size(); existing.ratios.assign(n, 100 / n); existing.ratios[0] += 100 - (100 / n) * n; } @@ -4886,7 +4823,7 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) } auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); if (existing.gradient_enabled && grad_curve_opt && cfg_idx < grad_curve_opt->values.size()) { - auto curve = Slic3r::parse_gradient_curve(grad_curve_opt->values[cfg_idx]); + auto curve = Slic3r::parse_gradient_curve(grad_curve_opt->values[cfg_idx]); existing.gradient_curve = curve.points; } auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); @@ -4896,33 +4833,28 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) MixedFilamentDialog dlg(this, existing, color_strs, names, types); if (dlg.ShowModal() == wxID_OK) { auto result = dlg.get_result(); - if (result.components.size() < 2 || result.ratios.size() < 2) - return; + if (result.components.size() < 2 || result.ratios.size() < 2) return; // Serialize components std::string comp_str; for (size_t i = 0; i < result.components.size(); ++i) { - if (i > 0) - comp_str += ","; + if (i > 0) comp_str += ","; comp_str += std::to_string(result.components[i]); } components_opt->values[cfg_idx] = comp_str; // Serialize ratios int ratio_sum = 0; - for (int r : result.ratios) - ratio_sum += r; - if (ratio_sum <= 0) - ratio_sum = 100; + for (int r : result.ratios) ratio_sum += r; + if (ratio_sum <= 0) ratio_sum = 100; std::string ratio_str; { CNumericLocalesSetter c_locale_setter; for (size_t i = 0; i < result.ratios.size(); ++i) { - if (i > 0) - ratio_str += ","; + if (i > 0) ratio_str += ","; char buf[32]; - std::snprintf(buf, sizeof(buf), "%.4f", (float) result.ratios[i] / ratio_sum); + std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum); ratio_str += buf; } } @@ -4932,24 +4864,22 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) if (!project_config.option("filament_mixed_gradient")) project_config.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false})); if (!project_config.option("filament_mixed_gradient_range")) - project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""})); + project_config.set_key_value("filament_mixed_gradient_range", new ConfigOptionStrings({""}) ); if (!project_config.option("filament_mixed_gradient_curve")) - project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""})); + project_config.set_key_value("filament_mixed_gradient_curve", new ConfigOptionStrings({""}) ); if (!project_config.option("filament_mixed_gradient_per_part")) project_config.set_key_value("filament_mixed_gradient_per_part", new ConfigOptionBools({false})); { auto* grad_opt = project_config.option("filament_mixed_gradient"); - while (grad_opt->values.size() <= cfg_idx) - grad_opt->values.push_back(false); + while (grad_opt->values.size() <= cfg_idx) grad_opt->values.push_back(false); grad_opt->values[cfg_idx] = result.gradient_enabled; } { auto* grad_range_opt = project_config.option("filament_mixed_gradient_range"); - while (grad_range_opt->values.size() <= cfg_idx) - grad_range_opt->values.push_back(""); + while (grad_range_opt->values.size() <= cfg_idx) grad_range_opt->values.push_back(""); if (result.gradient_enabled && result.components.size() == 2) { - const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; + const char* fmt = (result.gradient_direction == 0) ? "0.9000,0.1000" : "0.1000,0.9000"; grad_range_opt->values[cfg_idx] = fmt; } else { grad_range_opt->values[cfg_idx] = ""; @@ -4957,20 +4887,18 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) } { auto* grad_curve_opt = project_config.option("filament_mixed_gradient_curve"); - while (grad_curve_opt->values.size() <= cfg_idx) - grad_curve_opt->values.push_back(""); + while (grad_curve_opt->values.size() <= cfg_idx) grad_curve_opt->values.push_back(""); grad_curve_opt->values[cfg_idx] = serialize_mixed_gradient_curve_if_custom(result); } { auto* per_part_opt = project_config.option("filament_mixed_gradient_per_part"); - while (per_part_opt->values.size() <= cfg_idx) - per_part_opt->values.push_back(false); + while (per_part_opt->values.size() <= cfg_idx) per_part_opt->values.push_back(false); per_part_opt->values[cfg_idx] = result.gradient_enabled && result.per_part_gradient; } // Compute blended color std::string blended = blend_mixed_color(result.components, result.ratios, color_strs); - auto* colours_opt = project_config.option("filament_colour"); + auto* colours_opt = project_config.option("filament_colour"); if (colours_opt && cfg_idx < colours_opt->values.size()) colours_opt->values[cfg_idx] = blended; @@ -4990,12 +4918,10 @@ void Sidebar::edit_mixed_filament(size_t panel_idx) void Sidebar::delete_mixed_filament_at(size_t panel_idx) { auto* plater = dynamic_cast(GetParent()); - if (!plater) - return; + if (!plater) return; auto mixed_indices = plater->mixed_filament_config_indices(); - if (panel_idx >= mixed_indices.size()) - return; + if (panel_idx >= mixed_indices.size()) return; size_t cfg_idx = mixed_indices[panel_idx]; delete_filament(cfg_idx, -1); @@ -5009,7 +4935,7 @@ void Sidebar::decompose_filament_color(int filament_idx) return; auto& project_config = wxGetApp().preset_bundle->project_config; - auto* colours_opt = project_config.option("filament_colour"); + auto* colours_opt = project_config.option("filament_colour"); if (!colours_opt || static_cast(filament_idx) >= colours_opt->values.size()) return; @@ -5027,7 +4953,8 @@ void Sidebar::decompose_filament_color(int filament_idx) auto& pb = *wxGetApp().preset_bundle; for (size_t i = 0; i < physical_config_indices.size(); ++i) { const size_t ci = physical_config_indices[i]; - Preset* pr = (ci < pb.filament_presets.size()) ? pb.filaments.find_preset(pb.filament_presets[ci]) : nullptr; + Preset* pr = (ci < pb.filament_presets.size()) + ? pb.filaments.find_preset(pb.filament_presets[ci]) : nullptr; decompose_types.push_back(filament_type_for_color_decompose(pr)); } } @@ -5040,16 +4967,19 @@ void Sidebar::decompose_filament_color(int filament_idx) } } - ColorDecomposeDialog dlg(this, source_physical_idx == size_t(-1) ? -1 : static_cast(source_physical_idx), target_color, color_strs, - names, decompose_types, wxGetApp().preset_bundle->filament_presets.size(), - static_cast(EnforcerBlockerType::ExtruderMax), physical_config_indices); + ColorDecomposeDialog dlg(this, + source_physical_idx == size_t(-1) ? -1 : static_cast(source_physical_idx), + target_color, color_strs, names, decompose_types, + wxGetApp().preset_bundle->filament_presets.size(), + static_cast(EnforcerBlockerType::ExtruderMax), + physical_config_indices); int modal_res = dlg.ShowModal(); if (modal_res == wxID_OK) { ColorDecomposeResult dialog_result = dlg.get_result(); MixedFilamentResult mixed_result; std::vector missing_components; - if (!prepare_decompose_mixed_result(dialog_result, static_cast(filament_idx), source_physical_idx, color_strs, - decompose_types, physical_config_indices, mixed_result, missing_components)) + if (!prepare_decompose_mixed_result(dialog_result, static_cast(filament_idx), source_physical_idx, + color_strs, decompose_types, physical_config_indices, mixed_result, missing_components)) return; if (!confirm_create_decompose_missing_components(this, missing_components)) @@ -5080,7 +5010,7 @@ void Sidebar::update_filaments_area_height() // ORCA use a height with user preference auto left_sizer = p->sizer_filaments->GetItem((size_t) 0)->GetSizer(); auto combo_sizer = left_sizer->GetItem((size_t) 0)->GetSizer(); - int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); + int preferred_rows = std::ceil(0.5 * std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count"))); auto height_with_borders = combo_sizer->GetSize().GetHeight(); // gets height from sizer instead static numbers p->m_panel_filament_content->SetMaxSize(wxSize{-1, preferred_rows * height_with_borders}); @@ -5096,18 +5026,18 @@ void Sidebar::update_filaments_area_height() void Sidebar::update_filaments_counter(bool force_layout) // ORCA { - int current_count = p->combos_filament.size(); - int preferred_count = std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count")); - bool isShown = p->m_panel_filament_content->IsShown(); - auto counter = p->m_staticText_filament_count; + int current_count = p->combos_filament.size(); + int preferred_count = std::stoi(wxGetApp().app_config->get("filaments_area_preferred_count")); + bool isShown = p->m_panel_filament_content->IsShown(); + auto counter = p->m_staticText_filament_count; counter->SetLabel("(" + std::to_string(current_count) + ")"); // update counter on every change - if (current_count > preferred_count || !isShown) + if(current_count > preferred_count || !isShown) counter->Show(); else if (isShown) // hide when list is visible and short enough counter->Hide(); - if (force_layout) + if(force_layout) m_scrolled_sizer->Layout(); } @@ -5115,7 +5045,8 @@ void Sidebar::msw_rescale() { SetMinSize(wxSize(39 * wxGetApp().em_unit(), -1)); p->m_panel_printer_title->GetSizer()->SetMinSize(-1, 3 * wxGetApp().em_unit()); - p->m_panel_filament_title->GetSizer()->SetMinSize(-1, 3 * wxGetApp().em_unit()); + p->m_panel_filament_title->GetSizer() + ->SetMinSize(-1, 3 * wxGetApp().em_unit()); p->m_printer_icon->msw_rescale(); p->m_printer_connect->msw_rescale(); p->m_printer_bbl_sync->msw_rescale(); @@ -5137,15 +5068,14 @@ void Sidebar::msw_rescale() p->panel_printer_bed->SetMinSize(FromDIP(PRINTER_PANEL_SIZE)); p->panel_printer_bed->SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); p->combo_printer_bed->Rescale(); - p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow - p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18, -1))); // ORCA show only arrow - bool isDual = static_cast(p->panel_printer_preset->GetSizer())->GetOrientation() == wxVERTICAL; + p->combo_printer_bed->SetMinSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + p->combo_printer_bed->SetMaxSize(FromDIP(wxSize(18,-1))); // ORCA show only arrow + bool isDual = static_cast(p->panel_printer_preset->GetSizer())->GetOrientation() == wxVERTICAL; auto image_path = get_cur_select_bed_image(); p->image_printer_bed->SetBitmap(create_scaled_bitmap(image_path, this, PRINTER_THUMBNAIL_SIZE.GetHeight())); - if (p->big_bed_image_popup) { // ORCA force rebuild frame. current wxwidget version not supports wxBITMAP_SCALE_FILL flag on - // wxStaticBitmap also wxImage scaledImage = bit_map.ConvertToImage(); - // scaledImage.Rescale(FromDIP(m_image_px), FromDIP(m_image_px), wxIMAGE_QUALITY_HIGH); didnt worked as - // expected and it requires use on set_bitmap. so that will try to scale everytime + if (p->big_bed_image_popup){ // ORCA force rebuild frame. current wxwidget version not supports wxBITMAP_SCALE_FILL flag on wxStaticBitmap + // also wxImage scaledImage = bit_map.ConvertToImage(); scaledImage.Rescale(FromDIP(m_image_px), FromDIP(m_image_px), wxIMAGE_QUALITY_HIGH); + // didnt worked as expected and it requires use on set_bitmap. so that will try to scale everytime p->big_bed_image_popup->Destroy(); p->big_bed_image_popup = nullptr; } @@ -5159,25 +5089,25 @@ void Sidebar::msw_rescale() p->m_flushing_volume_btn->Rescale(); set_flushing_volume_warning(is_flush_config_modified()); // ORCA reapply appearance - // BBS + //BBS p->left_extruder->Rescale(); p->right_extruder->Rescale(); p->single_extruder->Rescale(); - // p->btn_sync_printer->SetPaddingSize({FromDIP(6), FromDIP(12)}); - // p->btn_sync_printer->SetMinSize(BTN_SYNC_SIZE); - // p->btn_sync_printer->Rescale(); + //p->btn_sync_printer->SetPaddingSize({FromDIP(6), FromDIP(12)}); + //p->btn_sync_printer->SetMinSize(BTN_SYNC_SIZE); + //p->btn_sync_printer->Rescale(); #if 0 if (p->mode_sizer) p->mode_sizer->msw_rescale(); #endif - // for (PlaterPresetComboBox* combo : std::vector { p->combo_print, - // //p->combo_sla_print, - // //p->combo_sla_material, - // //p->combo_printer - // } ) - // combo->msw_rescale(); + //for (PlaterPresetComboBox* combo : std::vector { p->combo_print, + // //p->combo_sla_print, + // //p->combo_sla_material, + // //p->combo_printer + // } ) + // combo->msw_rescale(); for (PlaterPresetComboBox* combo : p->combos_filament) combo->msw_rescale(); @@ -5186,11 +5116,11 @@ void Sidebar::msw_rescale() update_filaments_area_height(); // ORCA resize after combos scaled // BBS - // p->frequently_changed_parameters->msw_rescale(); - // obj_list()->msw_rescale(); + //p->frequently_changed_parameters->msw_rescale(); + //obj_list()->msw_rescale(); // BBS TODO: add msw_rescale for newly added windows // BBS - // p->object_manipulation->msw_rescale(); + //p->object_manipulation->msw_rescale(); p->object_settings->msw_rescale(); p->m_search_item->Rescale(); p->m_search_item->GetTextCtrl()->SetSize(wxSize(-1, FromDIP(16))); @@ -5228,7 +5158,7 @@ void Sidebar::sys_color_changed() for (wxWindow* win : std::vector{ p->scrolled, p->presets_panel }) wxGetApp().UpdateAllStaticTextDarkUI(win); #endif - // p->btn_sync_printer->SetIcon("printer_sync"); + //p->btn_sync_printer->SetIcon("printer_sync"); p->m_printer_bbl_sync->msw_rescale(); p->m_printer_connect->msw_rescale(); // for (wxWindow* btn : std::vector{ p->btn_reslice, p->btn_export_gcode }) @@ -5253,7 +5183,7 @@ void Sidebar::sys_color_changed() #endif p->object_settings->sys_color_changed(); - // BBS: remove print related combos + //BBS: remove print related combos #if 0 for (PlaterPresetComboBox* combo : std::vector{ p->combo_print, p->combo_sla_print, @@ -5272,12 +5202,12 @@ void Sidebar::sys_color_changed() p->image_printer->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); p->image_printer_bed->SetSize(FromDIP(PRINTER_THUMBNAIL_SIZE)); - for (ExtruderGroup* ext : {p->left_extruder, p->right_extruder, p->single_extruder}) + for (ExtruderGroup *ext : {p->left_extruder, p->right_extruder, p->single_extruder}) if (ext) ext->sys_color_changed(); // call a kill focus event to ensure new colors applied - for (ComboBox* combo : std::vector{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}) { + for (ComboBox* combo : std::vector{p->combo_printer, p->combo_nozzle_dia, p->combo_printer_bed}){ wxFocusEvent fakeEvent(wxEVT_KILL_FOCUS); fakeEvent.SetEventObject(combo); combo->HandleWindowEvent(fakeEvent); @@ -5287,12 +5217,12 @@ void Sidebar::sys_color_changed() obj_list()->sys_color_changed(); obj_layers()->sys_color_changed(); // BBS - // p->object_manipulation->sys_color_changed(); + //p->object_manipulation->sys_color_changed(); // btn...->msw_rescale() updates icon on button, so use it - // p->btn_send_gcode->msw_rescale(); - // p->btn_eject_device->msw_rescale(); - // p->btn_export_gcode_removable->msw_rescale(); + //p->btn_send_gcode->msw_rescale(); +// p->btn_eject_device->msw_rescale(); + //p->btn_export_gcode_removable->msw_rescale(); p->scrolled->Layout(); @@ -5302,11 +5232,14 @@ void Sidebar::sys_color_changed() p->searcher.dlg_sys_color_changed(); } -void Sidebar::search() { p->searcher.search(); } +void Sidebar::search() +{ + p->searcher.search(); +} void Sidebar::jump_to_option(const std::string& opt_key, Preset::Type type, const std::wstring& category) { - // const Search::Option& opt = p->searcher.get_option(opt_key, type); + //const Search::Option& opt = p->searcher.get_option(opt_key, type); if (type == Preset::TYPE_PRINT) { auto tab = dynamic_cast(wxGetApp().params_panel()->get_current_tab()); if (tab && tab->has_key(opt_key)) { @@ -5324,7 +5257,7 @@ void Sidebar::jump_to_option(size_t selected) jump_to_option(opt.opt_key(), opt.type, opt.category); // Switch to the Settings NotePad - // wxGetApp().mainframe->select_tab(); +// wxGetApp().mainframe->select_tab(); } // BBS. Move logic from Plater::on_extruders_change() to Sidebar::on_filament_count_change(). @@ -5334,7 +5267,7 @@ void Sidebar::on_filament_count_change(size_t num_filaments) // their own (they are rendered by update_mixed_filament_list instead), so the physical // subset drives the combo list. auto& project_config = wxGetApp().preset_bundle->project_config; - auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); std::vector physical_indices; for (size_t i = 0; i < num_filaments; ++i) { @@ -5362,8 +5295,9 @@ void Sidebar::on_filament_count_change(size_t num_filaments) wxWindowUpdateLocker noUpdates_scrolled_panel(this); size_t i = choices.size(); - while (i < num_physical) { - PlaterPresetComboBox* choice /*{ nullptr }*/; + while (i < num_physical) + { + PlaterPresetComboBox* choice/*{ nullptr }*/; init_filament_combo(&choice, physical_indices[i]); int last_selection = choices.back()->GetSelection(); choices.push_back(choice); @@ -5379,7 +5313,7 @@ void Sidebar::on_filament_count_change(size_t num_filaments) show_SEMM_buttons(); // ORCA - update_filaments_area_height(); // ORCA + update_filaments_area_height(); // ORCA recalc_filament_scroll_sizes(); update_mixed_filament_list(); @@ -5391,7 +5325,7 @@ void Sidebar::on_filament_count_change(size_t num_filaments) void Sidebar::on_filaments_delete(size_t filament_id) { - auto& choices = combos_filament(); + auto &choices = combos_filament(); // A mixed (virtual) slot has no combo of its own, so there is no combo UI to remove — // but the shared refresh below must still run so the mixed filament panel drops its row. @@ -5403,8 +5337,8 @@ void Sidebar::on_filaments_delete(size_t filament_id) // delete UI item { - const int last = p->combos_filament.size() - 1; - auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); + const int last = p->combos_filament.size() - 1; + auto sizer_filaments = this->p->sizer_filaments->GetItem(last % 2)->GetSizer(); sizer_filaments->Remove(last / 2); PlaterPresetComboBox* to_delete_combox = p->combos_filament[filament_id]; @@ -5428,7 +5362,7 @@ void Sidebar::on_filaments_delete(size_t filament_id) show_SEMM_buttons(); // ORCA - for (size_t idx = filament_id; idx < p->combos_filament.size(); ++idx) { + for (size_t idx = filament_id ; idx < p->combos_filament.size(); ++idx) { p->combos_filament[idx]->update(); } } @@ -5443,28 +5377,22 @@ void Sidebar::on_filaments_delete(size_t filament_id) update_dynamic_filament_list(); } -void Sidebar::add_filament() -{ - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - wxColour new_col = Plater::get_next_color_for_filament(); +void Sidebar::add_filament() { + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + wxColour new_col = Plater::get_next_color_for_filament(); add_custom_filament(new_col); auto filament_list = p->m_panel_filament_content; - if (!filament_list->IsShown()) { + if(!filament_list->IsShown()){ filament_list->Show(); // ORCA show list if its folded m_scrolled_sizer->Layout(); } filament_list->Scroll(-1, INT_MAX); // ORCA scroll to end of list on changes to inform user about filament count } -void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) -{ - if (is_new_project_in_gcode3mf()) { - return; - } - if (p->combos_filament.size() <= 1) - return; +void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) { + if (is_new_project_in_gcode3mf()) { return; } + if (p->combos_filament.size() <= 1) return; size_t filament_count = p->combos_filament.size() - 1; if (filament_id == size_t(-2)) { @@ -5505,16 +5433,14 @@ void Sidebar::delete_filament(size_t filament_id, int replace_filament_id) // new number of slots; recompute from the shrunk preset list for the downstream updates. size_t total_after_delete = wxGetApp().preset_bundle->filament_presets.size(); wxGetApp().plater()->get_partplate_list().on_filament_deleted(total_after_delete, filament_id); - wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, - replace_filament_id > (int) filament_id ? (replace_filament_id - 1) : replace_filament_id, - is_mixed_snapshot); + wxGetApp().plater()->on_filaments_delete(total_after_delete, filament_id, replace_filament_id > (int)filament_id ? (replace_filament_id - 1) : replace_filament_id, is_mixed_snapshot); wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); wxGetApp().plater()->update(); auto filament_list = p->m_panel_filament_content; - if (!filament_list->IsShown()) { + if(!filament_list->IsShown()){ filament_list->Show(); // ORCA show list if its folded m_scrolled_sizer->Layout(); } @@ -5526,16 +5452,16 @@ void Sidebar::change_filament(size_t from_id, size_t to_id) { // Merging a physical filament into a mixed one that lists it as a component would delete // the very filament the mix depends on, leaving it broken. Warn before doing so. - auto& pb = *wxGetApp().preset_bundle; + auto& pb = *wxGetApp().preset_bundle; bool from_is_physical = !pb.is_mixed_filament(from_id); - bool to_is_mixed = pb.is_mixed_filament(to_id); + bool to_is_mixed = pb.is_mixed_filament(to_id); if (from_is_physical && to_is_mixed) { auto* comp_opt = pb.project_config.option("filament_mixed_components"); if (comp_opt && to_id < comp_opt->values.size()) { - auto comps = Slic3r::parse_mixed_components(comp_opt->values[to_id]); - unsigned int from_1based = (unsigned int) from_id + 1; - bool target_uses_source = false; + auto comps = Slic3r::parse_mixed_components(comp_opt->values[to_id]); + unsigned int from_1based = (unsigned int)from_id + 1; + bool target_uses_source = false; for (unsigned int c : comps) { if (c == from_1based) { target_uses_source = true; @@ -5543,9 +5469,11 @@ void Sidebar::change_filament(size_t from_id, size_t to_id) } } if (target_uses_source) { - int ret = wxMessageBox(_L("The target mixed filament uses this physical filament as a component. " - "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), - _L("Warning"), wxOK | wxCANCEL | wxICON_WARNING); + int ret = wxMessageBox( + _L("The target mixed filament uses this physical filament as a component. " + "Merging will remove this physical filament and may invalidate the mixed filament. Continue?"), + _L("Warning"), + wxOK | wxCANCEL | wxICON_WARNING); if (ret != wxOK) return; } @@ -5558,31 +5486,26 @@ void Sidebar::change_filament(size_t from_id, size_t to_id) void Sidebar::edit_filament() { p->editing_filament = -1; - if (p->m_menu_filament_id >= 0 && p->m_menu_filament_id < p->combos_filament.size() && - p->combos_filament[p->m_menu_filament_id]->switch_to_tab()) + if (p->m_menu_filament_id >= 0 && p->m_menu_filament_id < p->combos_filament.size() + && p->combos_filament[p->m_menu_filament_id]->switch_to_tab()) p->editing_filament = p->m_menu_filament_id; // sync with TabPresetComboxBox's m_filament_idx } -void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_name, bool /*skip_preset_validation*/) -{ - if (is_new_project_in_gcode3mf()) { - return; - } - if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; - if (wxGetApp().preset_bundle->filament_presets.size() >= MAXIMUM_EXTRUDER_NUMBER) - return; +void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_name, bool /*skip_preset_validation*/) { + if (is_new_project_in_gcode3mf()) { return; } + if (p->combos_filament.size() >= MAXIMUM_EXTRUDER_NUMBER) return; + if (wxGetApp().preset_bundle->filament_presets.size() >= MAXIMUM_EXTRUDER_NUMBER) return; // Mixed-color slots are kept at the tail of the filament arrays, so a new physical // filament has to be inserted just after the last physical one rather than appended. // Count off filament_is_mixed, not filament_presets or the combos: the extruder-count spinner // reaches this before the sidebar has rebuilt, and update_multi_material_filament_presets() // can have grown filament_presets alone. - auto* bundle = wxGetApp().preset_bundle; - size_t insert_pos = bundle->num_physical_filaments(); - size_t total = insert_pos + bundle->num_mixed_filaments(); - int filament_count = (int) (total + 1); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + auto *bundle = wxGetApp().preset_bundle; + size_t insert_pos = bundle->num_physical_filaments(); + size_t total = insert_pos + bundle->num_mixed_filaments(); + int filament_count = (int)(total + 1); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); bundle->set_num_filaments(filament_count, new_color); // Maintain physical-first ordering: rotate the new slot from end to insert_pos. @@ -5592,7 +5515,7 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na std::rotate(presets.begin() + insert_pos, presets.begin() + total, presets.end()); auto& project_config = wxGetApp().preset_bundle->project_config; - auto& ams_mc = wxGetApp().preset_bundle->ams_multi_color_filment; + auto& ams_mc = wxGetApp().preset_bundle->ams_multi_color_filment; auto rotate_strings = [&](const char* key) { if (auto* opt = project_config.option(key)) @@ -5628,8 +5551,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na std::rotate(ams_mc.begin() + insert_pos, ams_mc.begin() + total, ams_mc.end()); // Remap object/volume extruder IDs and paint data: anything >= insert_pos+1 (1-based) shifts up by 1 - int threshold_1based = (int) (insert_pos + 1); - auto ebt_threshold = EnforcerBlockerType(threshold_1based); + int threshold_1based = (int)(insert_pos + 1); + auto ebt_threshold = EnforcerBlockerType(threshold_1based); for (auto* obj : wxGetApp().plater()->model().objects) { if (obj->config.has("extruder")) { int ext = obj->config.extruder(); @@ -5647,7 +5570,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na } } - if (!preset_name.empty() && wxGetApp().preset_bundle->filaments.find_preset(preset_name, false) && + if (!preset_name.empty() && + wxGetApp().preset_bundle->filaments.find_preset(preset_name, false) && insert_pos < wxGetApp().preset_bundle->filament_presets.size()) { wxGetApp().preset_bundle->filament_presets[insert_pos] = preset_name; } @@ -5662,9 +5586,10 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na bool Sidebar::is_new_project_in_gcode3mf() { if (p->plater->using_exported_file()) { - auto filename = p->plater->get_preview_only_filename(); - auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); - MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), wxOK | wxICON_WARNING); + auto filename = p->plater->get_preview_only_filename(); + auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename); + MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"), + wxOK | wxICON_WARNING); dlg.ShowModal(); p->plater->new_project(); return true; @@ -5725,8 +5650,7 @@ void Sidebar::on_bed_type_change(BedType bed_type) std::map Sidebar::build_filament_ams_list(MachineObject* obj) { std::map filament_ams_list; - if (!obj) - return filament_ams_list; + if (!obj) return filament_ams_list; // For pull-mode agents (e.g., HTTP REST API), refresh DevFilaSystem first auto* agent = wxGetApp().getDeviceManager()->get_agent(); @@ -5736,18 +5660,17 @@ std::map Sidebar::build_filament_ams_list(MachineObject } } - auto build_tray_config = [](DevAmsTray const& tray, std::string const& name, std::string ams_id, std::string slot_id) { - BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") % name % - tray.setting_id % tray.m_fila_type % tray.color; + auto build_tray_config = [](DevAmsTray const &tray, std::string const &name, std::string ams_id, std::string slot_id) { + BOOST_LOG_TRIVIAL(info) << boost::format("build_filament_ams_list: name %1% setting_id %2% type %3% color %4%") + % name % tray.setting_id % tray.m_fila_type % tray.color; DynamicPrintConfig tray_config; tray_config.set_key_value("filament_id", new ConfigOptionStrings{tray.setting_id}); tray_config.set_key_value("tag_uid", new ConfigOptionStrings{tray.tag_uid}); tray_config.set_key_value("ams_id", new ConfigOptionStrings{ams_id}); tray_config.set_key_value("slot_id", new ConfigOptionStrings{slot_id}); tray_config.set_key_value("filament_type", new ConfigOptionStrings{tray.m_fila_type}); - tray_config.set_key_value("tray_name", new ConfigOptionStrings{name}); - tray_config.set_key_value("filament_colour", - new ConfigOptionStrings{into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX))}); + tray_config.set_key_value("tray_name", new ConfigOptionStrings{ name }); + tray_config.set_key_value("filament_colour", new ConfigOptionStrings{into_u8(wxColour("#" + tray.color).GetAsString(wxC2S_HTML_SYNTAX))}); tray_config.set_key_value("filament_multi_colour", new ConfigOptionStrings{}); tray_config.set_key_value("filament_colour_type", new ConfigOptionStrings{std::to_string(tray.ctype)}); tray_config.set_key_value("filament_exist", new ConfigOptionBools{tray.is_exists}); @@ -5756,23 +5679,22 @@ std::map Sidebar::build_filament_ams_list(MachineObject if (wxGetApp().preset_bundle) { info = wxGetApp().preset_bundle->get_filament_by_filament_id(tray.setting_id); } - tray_config.set_key_value("filament_is_support", new ConfigOptionBools{info.has_value() ? info->is_support : false}); + tray_config.set_key_value("filament_is_support", new ConfigOptionBools{ info.has_value() ? info->is_support : false}); for (int i = 0; i < tray.cols.size(); ++i) { - tray_config.opt("filament_multi_colour") - ->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); + tray_config.opt("filament_multi_colour")->values.push_back(into_u8(wxColour("#" + tray.cols[i]).GetAsString(wxC2S_HTML_SYNTAX))); } return tray_config; }; if (obj->ams_support_virtual_tray) { int extruder = 0x10000; // Main (first) extruder at right - for (auto& vt_tray : obj->vt_slot) { - filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext", vt_tray.id, "0")); // 254 or 255 + for (auto & vt_tray : obj->vt_slot) { + filament_ams_list.emplace(extruder + stoi(vt_tray.id), build_tray_config(vt_tray, "Ext",vt_tray.id, "0"));//254 or 255 extruder = 0; } } - auto get_ams_name = [](int ams_id, int slot_id) -> std::string { + auto get_ams_name = [](int ams_id, int slot_id)->std::string { if (ams_id >= 0 && ams_id < 26) { char slot_name = slot_id + '1'; return std::string(1, 'A' + ams_id) + std::string(1, slot_name); @@ -5789,9 +5711,9 @@ std::map Sidebar::build_filament_ams_list(MachineObject int ams_id = std::stoi(ams.first); int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right for (auto tray : ams.second->GetTrays()) { - int slot_id = std::stoi(tray.first); - filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), - std::to_string(ams_id), std::to_string(slot_id))); + int slot_id = std::stoi(tray.first); + filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id), + build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id))); } } return filament_ams_list; @@ -5803,18 +5725,24 @@ bool Sidebar::sync_extruder_list() return p->sync_extruder_list(only_external_material); } -bool Sidebar::is_fila_switch_ready() { return p->is_fila_switch_ready(); } - -void Sidebar::reset_fila_switch() { p->update_extruder_separator_icon(false, false); } - -bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject* obj) +bool Sidebar::is_fila_switch_ready() { - if (!obj) + return p->is_fila_switch_ready(); +} + +void Sidebar::reset_fila_switch() +{ + p->update_extruder_separator_icon(false, false); +} + +bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObject *obj) +{ + if(!obj) return false; - std::string machine_print_name = obj->get_show_printer_type(); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); + std::string machine_print_name = obj->get_show_printer_type(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + std::string target_model_id = preset_bundle->printers.get_selected_preset().get_printer_type(preset_bundle); if (machine_print_name != target_model_id) { return false; } @@ -5825,12 +5753,17 @@ bool Sidebar::need_auto_sync_extruder_list_after_connect_priner(const MachineObj return true; } -void Sidebar::update_sync_status(const MachineObject* obj) { p->update_sync_status(obj); } - -int Sidebar::get_sidebar_pos_right_x() { return this->GetScreenPosition().x + this->GetSize().x; } - -void Sidebar::on_size(SimpleEvent& e) +void Sidebar::update_sync_status(const MachineObject *obj) { + p->update_sync_status(obj); +} + +int Sidebar::get_sidebar_pos_right_x() +{ + return this->GetScreenPosition().x + this->GetSize().x; +} + +void Sidebar::on_size(SimpleEvent &e) { if (m_sna_dialog && m_sna_dialog->IsShown()) { pop_sync_nozzle_and_ams_dialog(); } @@ -5839,24 +5772,18 @@ void Sidebar::on_size(SimpleEvent& e) } } -void Sidebar::on_full_screen(IntEvent& e) -{ - if (m_sna_dialog) { - m_sna_dialog->on_full_screen(e); - } - if (m_fna_dialog) { - m_fna_dialog->on_full_screen(e); - } +void Sidebar::on_full_screen(IntEvent &e) { + if (m_sna_dialog) { m_sna_dialog->on_full_screen(e); } + if (m_fna_dialog) { m_fna_dialog->on_full_screen(e); } } -void Sidebar::get_big_btn_sync_pos_size(wxPoint& pt, wxSize& size) +void Sidebar::get_big_btn_sync_pos_size(wxPoint &pt, wxSize &size) { size = p->m_printer_bbl_sync->GetSize(); - pt = p->m_printer_bbl_sync->GetScreenPosition(); + pt = p->m_printer_bbl_sync->GetScreenPosition(); } -void Sidebar::get_small_btn_sync_pos_size(wxPoint& pt, wxSize& size) -{ +void Sidebar::get_small_btn_sync_pos_size(wxPoint &pt, wxSize &size) { size = ams_btn->GetSize(); pt = ams_btn->GetScreenPosition(); } @@ -5872,22 +5799,23 @@ void Sidebar::load_ams_list(MachineObject* obj) filament_ams_list = build_filament_ams_list(obj); } - bool device_change = false; + bool device_change = false; const std::string& device = obj ? obj->get_dev_id() : ""; if (p->ams_list_device != device) { p->ams_list_device = device; device_change = true; } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": %1% items") % filament_ams_list.size(); - if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) { + if (wxGetApp().preset_bundle->filament_ams_list == filament_ams_list && !device_change) + { return; } wxGetApp().preset_bundle->filament_ams_list = filament_ams_list; - for (auto c : p->combos_filament) { + for (auto c : p->combos_filament){ c->update(); if (device_change) { - c->ShowBadge(false); // change printer,then clear badge + c->ShowBadge(false);//change printer,then clear badge } } @@ -5903,15 +5831,14 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) return; GUI::wxGetApp().sidebar().load_ams_list(obj); - auto& list = wxGetApp().preset_bundle->filament_ams_list; + auto & list = wxGetApp().preset_bundle->filament_ams_list; if (list.empty()) { auto printer_name = p->plater->get_selected_printer_name_in_combox(); - p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, - _L("Sync printer information")); + p->plater->pop_warning_and_go_to_device_page(printer_name, Plater::PrinterWarningType::NOT_CONNECTED, _L("Sync printer information")); return; } - bool exist_at_list_one_filament = false; - for (auto& cur : list) { + bool exist_at_list_one_filament =false; + for (auto &cur : list) { auto temp_config = cur.second; auto filament_type = temp_config.opt_string("filament_type", 0u); auto filament_color = temp_config.opt_string("filament_colour", 0u); @@ -5936,9 +5863,9 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) if (!ams_filament_ids.empty()) { boost::algorithm::split(list2, ams_filament_ids, boost::algorithm::is_any_of(",")); } - wxGetApp().plater()->update_all_plate_thumbnails(true); // preview thumbnail for sync_dlg + wxGetApp().plater()->update_all_plate_thumbnails(true);//preview thumbnail for sync_dlg SyncAmsInfoDialog::SyncInfo temp_info; - temp_info.use_dialog_pos = false; + temp_info.use_dialog_pos = false; temp_info.cancel_text_to_later = is_from_big_sync_btn; if (m_sync_dlg == nullptr) { m_sync_dlg = new SyncAmsInfoDialog(this, temp_info); @@ -5956,7 +5883,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) m_sync_dlg->set_check_dirty_fialment(false); dlg_res = m_sync_dlg->ShowModal(); } else { - dlg_res = (int) wxID_YES; + dlg_res =(int) wxID_YES; } if (dlg_res == wxID_CANCEL) return; @@ -5968,7 +5895,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) list2.resize(list.size()); auto iter = list.begin(); for (int i = 0; i < list.size(); ++i, ++iter) { - auto& ams = iter->second; + auto & ams = iter->second; auto filament_id = ams.opt_string("filament_id", 0u); ams.set_key_value("filament_changed", new ConfigOptionBool{dlg_res == wxID_YES || list2[i] != filament_id}); list2[i] = filament_id; @@ -5976,49 +5903,47 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) // BBS:Record consumables information before synchronization std::vector color_before_sync; - std::vector is_support_before; + std::vector is_support_before; DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; - ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); for (int i = 0; i < p->combos_filament.size(); ++i) { is_support_before.push_back(is_support_filament(i)); color_before_sync.push_back(color_opt->values[i]); } MergeFilamentInfo merge_info; - std::vector> unknowns; - auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); + std::vector> unknowns; + auto enable_append = wxGetApp().app_config->get_bool("enable_append_color_by_sync_ams"); auto sync_color_only = wxGetApp().app_config->get("sync_ams_filament_mode") == "1"; - auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, - sync_color_only); + auto n = wxGetApp().preset_bundle->sync_ams_list(unknowns, !sync_result.direct_sync, sync_result.sync_maps, enable_append, merge_info, sync_color_only); wxString detail; - for (auto& uk : unknowns) { + for (auto & uk : unknowns) { auto tray_name = uk.first->opt_string("tray_name", 0u); auto filament_type = uk.first->opt_string("filament_type", 0u); detail += from_u8("\n- " + tray_name + "(" + filament_type + ") ") + _L(uk.second); } if (n == 0) { - MessageDialog dlg(this, _L("There are no compatible filaments, and sync is not performed.") + detail, _L("Sync filaments with AMS"), - wxOK); + MessageDialog dlg(this, + _L("There are no compatible filaments, and sync is not performed.") + detail, + _L("Sync filaments with AMS"), wxOK); dlg.ShowModal(); return; } // Replace unknown filament IDs with the resolved preset's filament_id - auto& filaments = wxGetApp().preset_bundle->filaments; - auto& filament_presets = wxGetApp().preset_bundle->filament_presets; + auto &filaments = wxGetApp().preset_bundle->filaments; + auto &filament_presets = wxGetApp().preset_bundle->filament_presets; for (size_t i = 0; i < list2.size() && i < filament_presets.size(); ++i) { if (list2[i] == UNKNOWN_FILAMENT_ID) { - const Preset* resolved = filaments.find_preset(filament_presets[i]); + const Preset *resolved = filaments.find_preset(filament_presets[i]); if (resolved) list2[i] = resolved->filament_id; } } ams_filament_ids = boost::algorithm::join(list2, ","); - wxGetApp().app_config->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); + wxGetApp().app_config ->set("ams_filament_ids", p->ams_list_device, ams_filament_ids); if (!unknowns.empty()) { MessageDialog dlg(this, - _L("There are some unknown or incompatible filaments mapped to generic preset.\nPlease update Orca Slicer or " - "restart Orca Slicer to check if there is an update to system presets.") + - detail, - _L("Sync filaments with AMS"), wxOK); + _L("There are some unknown or incompatible filaments mapped to generic preset.\nPlease update Orca Slicer or restart Orca Slicer to check if there is an update to system presets.") + detail, + _L("Sync filaments with AMS"), wxOK); dlg.ShowModal(); } if (!sync_color_only) { @@ -6036,9 +5961,11 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (int i = 0; i < p->combos_filament.size(); ++i) { if (i >= color_before_sync.size()) { auto_calc_flushing_volumes(i); - } else if (color_before_sync[i] != color_opt->values[i] && wxGetApp().app_config->get("auto_calculate_flush") != "disabled") { + } + else if(color_before_sync[i] != color_opt->values[i] && wxGetApp().app_config->get("auto_calculate_flush") != "disabled"){ auto_calc_flushing_volumes(i); - } else if (is_support_filament(i) != is_support_before[i] && wxGetApp().app_config->get("auto_calculate_flush") == "all") { + } + else if(is_support_filament(i) !=is_support_before[i] && wxGetApp().app_config->get("auto_calculate_flush") == "all"){ auto_calc_flushing_volumes(i); } } @@ -6052,16 +5979,15 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) update_dynamic_filament_list(); } else { wxGetApp().plater()->update_filament_colors_in_full_config(); - for (auto& c : p->combos_filament) + for (auto &c : p->combos_filament) c->update(); obj_list()->update_filament_colors(); update_dynamic_filament_list(); } - auto badge_combox_filament = [sync_color_only](PlaterPresetComboBox* c) { - auto tip = sync_color_only ? - _L("Only filament color information has been synchronized from printer.") : - _L("Filament type and color information have been synchronized, but slot information is not included."); + auto badge_combox_filament = [sync_color_only](PlaterPresetComboBox *c) { + auto tip = sync_color_only ? _L("Only filament color information has been synchronized from printer.") : + _L("Filament type and color information have been synchronized, but slot information is not included."); c->SetToolTip(tip); c->ShowBadge(true); }; @@ -6073,17 +5999,16 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) // empty trays are skipped), so every resulting combo is AMS-sourced and gets a badge. The // previous per-tray index walked the full filament_ams_list (including the skipped empties), // so an empty slot before a loaded one dropped the badge for the trailing filaments. - for (auto& c : p->combos_filament) { + for (auto &c : p->combos_filament) { badge_combox_filament(c); } } } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "prepare enable_merge_color_by_sync_ams"; - if (!merge_info.is_empty() && - wxGetApp().app_config->get_bool("enable_merge_color_by_sync_ams")) { // merge same color and preset filament//use same ams - auto reduce_index = [](MergeFilamentInfo& merge_info, int value) { + if (!merge_info.is_empty() && wxGetApp().app_config->get_bool("enable_merge_color_by_sync_ams")) { // merge same color and preset filament//use same ams + auto reduce_index = [](MergeFilamentInfo &merge_info,int value) { for (size_t i = 0; i < merge_info.merges.size(); i++) { - auto& cur = merge_info.merges[i]; + auto &cur = merge_info.merges[i]; for (size_t j = 0; j < cur.size(); j++) { if (value < cur[j]) { cur[j] = cur[j] - 1; @@ -6102,7 +6027,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (size_t i = 0; i < merge_info.merges.size(); i++) { auto& cur = merge_info.merges[i]; - for (int j = cur.size() - 1; j >= 1; j--) { + for (int j = cur.size() -1; j >= 1 ; j--) { auto last_index = cur[j]; change_filament(last_index, cur[0]); cur.erase(cur.begin() + j); @@ -6113,7 +6038,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) for (size_t i = 0; i < sync_ams_badges.size(); i++) { if (sync_ams_badges[i] == true) { if (i < p->combos_filament.size()) { - auto& c = p->combos_filament[i]; + auto &c = p->combos_filament[i]; badge_combox_filament(c); } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "check error: p->combos_filament array out of bound"; @@ -6127,7 +6052,7 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) } auto temp_index = iter.first; if (temp_index < p->combos_filament.size() && temp_index >= 0) { - auto& c = p->combos_filament[temp_index]; + auto &c = p->combos_filament[temp_index]; badge_combox_filament(c); } } @@ -6137,11 +6062,12 @@ void Sidebar::sync_ams_list(bool is_from_big_sync_btn) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "finish pop_finsish_sync_ams_dialog"; } + bool Sidebar::should_show_SEMM_buttons() { - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); - auto cfg = preset_bundle.printers.get_edited_preset().config; + PresetBundle &preset_bundle = *wxGetApp().preset_bundle; + bool is_bbl_vendor = preset_bundle.is_bbl_vendor(); + auto cfg = preset_bundle.printers.get_edited_preset().config; return cfg.opt_bool("single_extruder_multi_material") || is_bbl_vendor; } @@ -6151,22 +6077,23 @@ void Sidebar::show_SEMM_buttons() // ORCA if (!p || p->combos_filament.empty() || !p->m_bpButton_add_filament || !p->m_bpButton_del_filament || !p->m_flushing_volume_btn) return; - + bool is_multi_material = p->combos_filament.size() > 1; bool single_or_bbl = should_show_SEMM_buttons(); - bool is_single = single_or_bbl && !is_multi_material; // SINGLE EXTRUDER / BBL WITH 1 MATERIAL - bool is_multi = single_or_bbl && is_multi_material; // MULTI MATERIAL WITH SINGLE EXTRUDER - bool is_fixed = !is_single && !is_multi; // MULTI EXTRUDER / TOOLCHANGER / IDEX WITH FIXED MATERIAL + bool is_single = single_or_bbl && !is_multi_material; // SINGLE EXTRUDER / BBL WITH 1 MATERIAL + bool is_multi = single_or_bbl && is_multi_material; // MULTI MATERIAL WITH SINGLE EXTRUDER + bool is_fixed = !is_single && !is_multi; // MULTI EXTRUDER / TOOLCHANGER / IDEX WITH FIXED MATERIAL p->m_bpButton_add_filament->Show(single_or_bbl); p->m_bpButton_del_filament->Show(is_multi); - p->m_flushing_volume_btn->Show(is_multi); + p->m_flushing_volume_btn->Show( is_multi); if (is_multi) { - for (auto& c : p->combos_filament) + for (auto &c : p->combos_filament) c->edit_btn->SetBitmap_("menu_filament"); - } else if (is_single || is_fixed) { - for (auto& c : p->combos_filament) + } + else if (is_single || is_fixed) { + for (auto &c : p->combos_filament) c->edit_btn->SetBitmap_("edit"); } @@ -6210,24 +6137,42 @@ void Sidebar::update_dynamic_filament_list() dynamic_physical_filament_list.update(); } -PlaterPresetComboBox* Sidebar::printer_combox() { return p->combo_printer; } +PlaterPresetComboBox* Sidebar::printer_combox() +{ + return p->combo_printer; +} ObjectList* Sidebar::obj_list() { // BBS - // return obj_list(); + //return obj_list(); return p->m_object_list; } -ObjectSettings* Sidebar::obj_settings() { return p->object_settings; } +ObjectSettings* Sidebar::obj_settings() +{ + return p->object_settings; +} -ObjectLayers* Sidebar::obj_layers() { return p->object_layers; } +ObjectLayers* Sidebar::obj_layers() +{ + return p->object_layers; +} -wxPanel* Sidebar::scrolled_panel() { return p->scrolled; } +wxPanel* Sidebar::scrolled_panel() +{ + return p->scrolled; +} -wxPanel* Sidebar::print_panel() { return p->m_panel_print_content; } +wxPanel* Sidebar::print_panel() +{ + return p->m_panel_print_content; +} -wxPanel* Sidebar::filament_panel() { return p->m_panel_filament_content; } +wxPanel* Sidebar::filament_panel() +{ + return p->m_panel_filament_content; +} ConfigOptionsGroup* Sidebar::og_freq_chng_params(const bool is_fff) { @@ -6248,10 +6193,11 @@ wxButton* Sidebar::get_wiping_dialog_button() void Sidebar::set_flushing_volume_warning(const bool flushing_volume_modify) { - if (flushing_volume_modify) { + if(flushing_volume_modify){ p->m_flushing_volume_btn->SetStyle(ButtonStyle::Regular, ButtonType::Compact); p->m_flushing_volume_btn->SetBorderColor(wxColour("#FF6F00")); - } else + } + else p->m_flushing_volume_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Compact); } @@ -6266,17 +6212,19 @@ void Sidebar::enable_buttons(bool enable) #endif } -bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } -bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } -bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } +bool Sidebar::show_reslice(bool show) const { return p->btn_reslice->Show(show); } +bool Sidebar::show_export(bool show) const { return p->btn_export_gcode->Show(show); } +bool Sidebar::show_send(bool show) const { return p->btn_send_gcode->Show(show); } bool Sidebar::show_export_removable(bool show) const { return p->btn_export_gcode_removable->Show(show); } -// bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } -// bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } +//bool Sidebar::show_eject(bool show) const { return p->btn_eject_device->Show(show); } +//bool Sidebar::get_eject_shown() const { return p->btn_eject_device->IsShown(); } -bool Sidebar::is_multifilament() { return p->combos_filament.size() > 1; } - -void Sidebar::deal_btn_sync() +bool Sidebar::is_multifilament() { + return p->combos_filament.size() > 1; +} + +void Sidebar::deal_btn_sync() { m_begin_sync_printer_status = true; bool only_external_material; // Manual "sync machine" button: is_manual=true so an H2C pops the MultiNozzleSyncDialog to pick a nozzle option. @@ -6290,10 +6238,10 @@ void Sidebar::deal_btn_sync() template void setup_dialog_position(T& info) { - auto plater = wxGetApp().plater(); - auto& sidebar = plater->sidebar(); - auto docking = plater->get_sidebar_docking_state(); - bool on_right = true; + auto plater = wxGetApp().plater(); + auto& sidebar = plater->sidebar(); + auto docking = plater->get_sidebar_docking_state(); + bool on_right = true; if (docking == Sidebar::Left) { on_right = true; @@ -6318,27 +6266,23 @@ template void setup_dialog_position(T& info) } } -void Sidebar::pop_sync_nozzle_and_ams_dialog() -{ +void Sidebar::pop_sync_nozzle_and_ams_dialog() { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " begin pop_sync_nozzle_and_ams_dialog"; wxTheApp->CallAfter([this]() { SyncNozzleAndAmsDialog::InputInfo temp_na_info; - wxPoint big_btn_pt; - wxSize big_btn_size; + wxPoint big_btn_pt; + wxSize big_btn_size; wxGetApp().plater()->sidebar().get_big_btn_sync_pos_size(big_btn_pt, big_btn_size); - temp_na_info.dialog_pos = big_btn_pt + wxPoint(big_btn_size.x, big_btn_size.y) + - wxPoint(FromDIP(big_btn_size.x / 10.f - 5), FromDIP(big_btn_size.y / 10.f)); + temp_na_info.dialog_pos = big_btn_pt + wxPoint(big_btn_size.x, big_btn_size.y) + wxPoint(FromDIP(big_btn_size.x / 10.f - 5), FromDIP(big_btn_size.y / 10.f)); temp_na_info.dialog_pos.y += FromDIP(2); setup_dialog_position(temp_na_info); wxPoint small_btn_pt; - wxSize small_btn_size; + wxSize small_btn_size; get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); temp_na_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); - if (m_fna_dialog) { - m_fna_dialog->on_hide(); - } + if (m_fna_dialog) { m_fna_dialog->on_hide(); } if (m_sna_dialog) { m_sna_dialog->Destroy(); m_sna_dialog = nullptr; @@ -6352,16 +6296,14 @@ void Sidebar::pop_finsish_sync_ams_dialog() { wxTheApp->CallAfter([this]() { wxPoint small_btn_pt; - wxSize small_btn_size; + wxSize small_btn_size; get_small_btn_sync_pos_size(small_btn_pt, small_btn_size); FinishSyncAmsDialog::InputInfo temp_fsa_info; - temp_fsa_info.dialog_pos.y = small_btn_pt.y; + temp_fsa_info.dialog_pos.y = small_btn_pt.y; setup_dialog_position(temp_fsa_info); - temp_fsa_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); - if (m_sna_dialog) { - m_sna_dialog->on_hide(); - } + temp_fsa_info.ams_btn_pos = small_btn_pt + wxPoint(small_btn_size.x / 2, small_btn_size.y / 2); + if (m_sna_dialog) { m_sna_dialog->on_hide(); } if (m_fna_dialog) { m_fna_dialog->Destroy(); m_fna_dialog = nullptr; @@ -6369,38 +6311,42 @@ void Sidebar::pop_finsish_sync_ams_dialog() m_fna_dialog = new FinishSyncAmsDialog(temp_fsa_info); m_fna_dialog->on_show(); }); + } static std::vector get_search_inputs(ConfigOptionMode mode) { - std::vector ret{}; + std::vector ret {}; auto& tabs_list = wxGetApp().tabs_list; auto print_tech = wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology(); for (auto tab : tabs_list) if (tab->supports_printer_technology(print_tech)) - ret.emplace_back(Search::InputInfo{tab->get_config(), tab->type(), mode}); + ret.emplace_back(Search::InputInfo {tab->get_config(), tab->type(), mode}); return ret; } -void Sidebar::update_searcher() { p->searcher.init(get_search_inputs(m_mode)); } +void Sidebar::update_searcher() +{ + p->searcher.init(get_search_inputs(m_mode)); +} void Sidebar::update_mode() { m_mode = wxGetApp().get_mode(); - // BBS: remove print related combos + //BBS: remove print related combos update_searcher(); wxWindowUpdateLocker noUpdates(this); // BBS - // obj_list()->get_sizer()->Show(m_mode > comSimple); + //obj_list()->get_sizer()->Show(m_mode > comSimple); obj_list()->unselect_objects(); obj_list()->update_selections(); - // obj_list()->update_object_menu(); +// obj_list()->update_object_menu(); Layout(); } @@ -6412,14 +6358,14 @@ void Sidebar::collapse(bool collapse) { p->plater->collapse_sidebar(collapse); } #ifdef _MSW_DARK_MODE void Sidebar::show_mode_sizer(bool show) { - // p->mode_sizer->Show(show); + //p->mode_sizer->Show(show); } #endif void Sidebar::update_ui_from_settings() { // BBS - // p->object_manipulation->update_ui_from_settings(); + //p->object_manipulation->update_ui_from_settings(); // update Cut gizmo, if it's open p->plater->canvas3D()->update_gizmos_on_off_state(); p->plater->set_current_canvas_as_dirty(); @@ -6444,38 +6390,47 @@ bool Sidebar::show_object_list(bool show) const void Sidebar::finish_param_edit() { p->editing_filament = -1; } -std::vector& Sidebar::combos_filament() { return p->combos_filament; } +std::vector& Sidebar::combos_filament() +{ + return p->combos_filament; +} void Sidebar::clear_combos_filament_badge() { - auto& combos_filament = p->combos_filament; - for (auto& c : combos_filament) { // clear flag + auto &combos_filament = p->combos_filament; + for (auto &c : combos_filament) { // clear flag c->ShowBadge(false); } } -void Sidebar::udpate_combos_filament_badge() -{ - auto& combos_filament = p->combos_filament; - for (auto& c : combos_filament) { +void Sidebar::udpate_combos_filament_badge() { + auto &combos_filament = p->combos_filament; + for (auto &c : combos_filament) { auto selection = c->GetSelection(); auto select_flag = c->GetFlag(selection); auto ok = select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS; c->ShowBadge(ok); } + } -Search::OptionsSearcher& Sidebar::get_searcher() { return p->searcher; } +Search::OptionsSearcher& Sidebar::get_searcher() +{ + return p->searcher; +} -std::string& Sidebar::get_search_line() { return p->searcher.search_string(); } +std::string& Sidebar::get_search_line() +{ + return p->searcher.search_string(); +} static std::map printer_thumbnails = {}; void Sidebar::update_printer_thumbnail() { - auto& preset_bundle = wxGetApp().preset_bundle; - Preset& selected_preset = preset_bundle->printers.get_edited_preset(); - std::string printer_type = selected_preset.get_current_printer_type(preset_bundle); + auto& preset_bundle = wxGetApp().preset_bundle; + Preset & selected_preset = preset_bundle->printers.get_edited_preset(); + std::string printer_type = selected_preset.get_current_printer_type(preset_bundle); if (printer_thumbnails.find(printer_type) != printer_thumbnails.end()) // Use known cache first p->image_printer->SetBitmap(create_scaled_bitmap(printer_thumbnails[printer_type], this, PRINTER_THUMBNAIL_SIZE.GetHeight())); else { @@ -6491,7 +6446,7 @@ void Sidebar::update_printer_thumbnail() */ // Orca: try to use the printer model cover as the thumbnail - const auto model_name = selected_preset.config.opt_string("printer_model"); + const auto model_name = selected_preset.config.opt_string("printer_model"); std::string cover_file = model_name + "_cover.png"; for (auto vendor_profile : preset_bundle->vendors) { for (auto vendor_model : vendor_profile.second.models) { @@ -6515,27 +6470,29 @@ void Sidebar::update_printer_thumbnail() } } -void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extruder_id) -{ +void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extruder_id) { + std::vector filament_indices; std::vector extruder_indices; auto& preset_bundle = wxGetApp().preset_bundle; - auto filament_ptr = preset_bundle->project_config.option("filament_colour"); - int filament_count = filament_ptr ? filament_ptr->size() : 0; - int extruder_count = preset_bundle->get_printer_extruder_count(); + auto filament_ptr = preset_bundle->project_config.option("filament_colour"); + int filament_count = filament_ptr ? filament_ptr->size() : 0; + int extruder_count = preset_bundle->get_printer_extruder_count(); if (filament_idx < 0) { filament_indices.resize(filament_count); std::iota(filament_indices.begin(), filament_indices.end(), 0); - } else { + } + else { filament_indices.emplace_back(filament_idx); } if (extruder_id < 0) { extruder_indices.resize(extruder_count); std::iota(extruder_indices.begin(), extruder_indices.end(), 0); - } else { + } + else { extruder_indices.emplace_back(extruder_id); } @@ -6553,28 +6510,28 @@ void Sidebar::auto_calc_flushing_volumes(const int filament_idx, const int extru p->plater->update(); } + void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int extruder_id) { auto& preset_bundle = wxGetApp().preset_bundle; // A mixed-colour slot is virtual and is never flushed to or from: leave its row and column // alone (the flushing dialog hides them and only compares physical slots). - if (modify_id >= 0 && preset_bundle->is_mixed_filament((size_t) modify_id)) + if (modify_id >= 0 && preset_bundle->is_mixed_filament((size_t)modify_id)) return; - auto& project_config = preset_bundle->project_config; - const auto& full_config = wxGetApp().preset_bundle->full_config(); - auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; - size_t extruder_nums = preset_bundle->get_printer_extruder_count(); - int nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values[extruder_id]; - std::vector init_matrix = get_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, - extruder_id, extruder_nums); + auto& project_config = preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + auto& ams_multi_color_filament = preset_bundle->ams_multi_color_filment; + size_t extruder_nums = preset_bundle->get_printer_extruder_count(); + int nozzle_flush_dataset = full_config.option("nozzle_flush_dataset")->values[extruder_id]; + std::vector init_matrix = get_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, extruder_id, extruder_nums); const std::vector& min_flush_volumes = get_min_flush_volumes(full_config, extruder_id); - const auto* flush_multi_opt = project_config.option("flush_multiplier"); - float flush_multiplier = flush_multi_opt ? (float) flush_multi_opt->get_at(extruder_id) : 1.f; - std::vector matrix = init_matrix; - int m_max_flush_volume = Slic3r::g_max_flush_volume; - unsigned int m_number_of_extruders = (int) (sqrt(init_matrix.size()) + 0.001); + const auto* flush_multi_opt = project_config.option("flush_multiplier"); + float flush_multiplier = flush_multi_opt ? (float)flush_multi_opt->get_at(extruder_id) : 1.f; + std::vector matrix = init_matrix; + int m_max_flush_volume = Slic3r::g_max_flush_volume; + unsigned int m_number_of_extruders = (int)(sqrt(init_matrix.size()) + 0.001); const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); std::vector> multi_colours; @@ -6599,25 +6556,25 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int if (modify_id >= 0 && modify_id < multi_colours.size()) { for (int i = 0; i < multi_colours.size(); ++i) { - if (preset_bundle->is_mixed_filament((size_t) i)) + if (preset_bundle->is_mixed_filament((size_t)i)) continue; // from to modify int from_idx = i; if (from_idx != modify_id) { Slic3r::FlushVolCalculator calculator(min_flush_volumes[from_idx], m_max_flush_volume, nozzle_flush_dataset); - int flushing_volume = 0; + int flushing_volume = 0; bool is_from_support = is_support_filament(from_idx); - bool is_to_support = is_support_filament(modify_id); + bool is_to_support = is_support_filament(modify_id); if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; - } else { + } + else { for (int j = 0; j < multi_colours[from_idx].size(); ++j) { const wxColour& from = multi_colours[from_idx][j]; for (int k = 0; k < multi_colours[modify_id].size(); ++k) { const wxColour& to = multi_colours[modify_id][k]; - int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), - to.Red(), to.Green(), to.Blue()); - flushing_volume = std::max(flushing_volume, volume); + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); } } if (is_from_support) @@ -6631,18 +6588,18 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int if (to_idx != modify_id) { Slic3r::FlushVolCalculator calculator(min_flush_volumes[modify_id], m_max_flush_volume, nozzle_flush_dataset); bool is_from_support = is_support_filament(modify_id); - bool is_to_support = is_support_filament(to_idx); - int flushing_volume = 0; + bool is_to_support = is_support_filament(to_idx); + int flushing_volume = 0; if (is_to_support) { flushing_volume = Slic3r::g_flush_volume_to_support; - } else { + } + else { for (int j = 0; j < multi_colours[modify_id].size(); ++j) { const wxColour& from = multi_colours[modify_id][j]; for (int k = 0; k < multi_colours[to_idx].size(); ++k) { const wxColour& to = multi_colours[to_idx][k]; - int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), - to.Red(), to.Green(), to.Blue()); - flushing_volume = std::max(flushing_volume, volume); + int volume = calculator.calc_flush_vol(from.Alpha(), from.Red(), from.Green(), from.Blue(), to.Alpha(), to.Red(), to.Green(), to.Blue()); + flushing_volume = std::max(flushing_volume, volume); } } if (is_from_support) @@ -6653,21 +6610,27 @@ void Sidebar::auto_calc_flushing_volumes_internal(const int modify_id, const int } } } - set_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, matrix, extruder_id, - extruder_nums); + set_flush_volumes_matrix((project_config.option("flush_volumes_matrix"))->values, matrix, extruder_id, extruder_nums); } -void Sidebar::jump_to_object(ObjectDataViewModelNode* item) { p->jump_to_object(item); } +void Sidebar::jump_to_object(ObjectDataViewModelNode* item) +{ + p->jump_to_object(item); +} -void Sidebar::can_search() { p->can_search(); } +void Sidebar::can_search() +{ + p->can_search(); +} class PlaterDropTarget : public wxFileDropTarget { public: - PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) - { this->SetDefaultAction(wxDragCopy); } + PlaterDropTarget(MainFrame& mainframe, Plater& plater) : m_mainframe(mainframe), m_plater(plater) { + this->SetDefaultAction(wxDragCopy); + } - virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames); + virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); private: MainFrame& m_mainframe; @@ -6675,16 +6638,16 @@ private: }; namespace { -bool emboss_svg(Plater& plater, const wxString& svg_file, const Vec2d& mouse_drop_position) +bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) { std::string svg_file_str = into_u8(svg_file); - GLCanvas3D* canvas = plater.canvas3D(); + GLCanvas3D* canvas = plater.canvas3D(); if (canvas == nullptr) return false; auto base_svg = canvas->get_gizmos_manager().get_gizmo(GLGizmosManager::Svg); if (base_svg == nullptr) return false; - GLGizmoSVG* svg = dynamic_cast(base_svg); + GLGizmoSVG* svg = dynamic_cast(base_svg); if (svg == nullptr) return false; @@ -6695,42 +6658,51 @@ bool emboss_svg(Plater& plater, const wxString& svg_file, const Vec2d& mouse_dro return svg->create_volume(svg_file_str, mouse_drop_position, ModelVolumeType::MODEL_PART); } -} // namespace +} // State to manage showing after export notifications and device ejecting -enum ExportingStatus { NOT_EXPORTING, EXPORTING_TO_REMOVABLE, EXPORTING_TO_LOCAL }; +enum ExportingStatus{ + NOT_EXPORTING, + EXPORTING_TO_REMOVABLE, + EXPORTING_TO_LOCAL +}; + // TODO: listen on dark ui change class FloatFrame : public wxAuiFloatingFrame { public: FloatFrame(wxWindow* parent, wxAuiManager* ownerMgr, const wxAuiPaneInfo& pane) : wxAuiFloatingFrame(parent, ownerMgr, pane) - { wxGetApp().UpdateFrameDarkUI(this); } + { + wxGetApp().UpdateFrameDarkUI(this); + } }; class AuiMgr : public wxAuiManager { public: - AuiMgr() : wxAuiManager() {} + AuiMgr() : wxAuiManager(){} virtual wxAuiFloatingFrame* CreateFloatingFrame(wxWindow* parent, const wxAuiPaneInfo& p) override - { return new FloatFrame(parent, this, p); } + { + return new FloatFrame(parent, this, p); + } }; // Plater / private struct Plater::priv { // PIMPL back pointer ("Q-Pointer") - Plater* q; - Sidebar* sidebar; - MainFrame* main_frame; + Plater *q; + Sidebar * sidebar; + MainFrame *main_frame; MenuFactory menus; SelectMachineDialog* m_select_machine_dlg = nullptr; - SendMultiMachinePage* m_send_multi_dlg = nullptr; + SendMultiMachinePage* m_send_multi_dlg = nullptr; SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr; - PublishDialog* m_publish_dlg = nullptr; + PublishDialog *m_publish_dlg = nullptr; // Session-level stash of the last published selection. Written on publish and on // loading a published 3MF; read when the Publish dialog is opened. @@ -6739,33 +6711,33 @@ struct Plater::priv std::vector m_pending_material_keys; // Data - Slic3r::DynamicPrintConfig* config; // FIXME: leak? - Slic3r::Print fff_print; - Slic3r::SLAPrint sla_print; - Slic3r::Model model; - PrinterTechnology printer_technology = ptFFF; + Slic3r::DynamicPrintConfig *config; // FIXME: leak? + Slic3r::Print fff_print; + Slic3r::SLAPrint sla_print; + Slic3r::Model model; + PrinterTechnology printer_technology = ptFFF; Slic3r::GCodeProcessorResult gcode_result; // GUI elements AuiMgr m_aui_mgr; wxString m_default_window_layout; - wxPanel* current_panel{nullptr}; + wxPanel* current_panel{ nullptr }; std::vector panels; struct SidebarLayout { - bool is_enabled{false}; - bool is_collapsed{false}; - bool show{false}; + bool is_enabled{false}; + bool is_collapsed{false}; + bool show{false}; } sidebar_layout; Bed3D bed; Camera camera; - // BBS: partplate related structure + //BBS: partplate related structure PartPlateList partplate_list; - // BBS: add a flag to ignore cancel event + //BBS: add a flag to ignore cancel event bool m_ignore_event{false}; bool m_slice_all{false}; - bool m_is_slicing{false}; + bool m_is_slicing {false}; // Missing-plugin set signatures (sorted full refs joined by '\n'), one per notification. They // gate plugin-load re-validation and avoid needlessly recreating the notification when the set // is unchanged. Whether missing plugins block slicing is derived directly from PluginResolver @@ -6774,17 +6746,17 @@ struct Plater::priv std::string m_cloud_missing_shown_sig; std::string m_inactive_shown_sig; std::string m_broken_shown_sig; - bool auto_reslice_pending{false}; - bool auto_reslice_after_cancel{false}; - bool m_is_publishing{false}; + bool auto_reslice_pending {false}; + bool auto_reslice_after_cancel {false}; + bool m_is_publishing {false}; int m_is_RightClickInLeftUI{-1}; int m_cur_slice_plate; - // BBS: m_slice_all in .gcode.3mf file case, set true when slice all - bool m_slice_all_only_has_gcode{false}; + //BBS: m_slice_all in .gcode.3mf file case, set true when slice all + bool m_slice_all_only_has_gcode{ false }; bool m_need_update{false}; - // BBS: add popup object table logic - // ObjectTableDialog* m_popup_table{ nullptr }; + //BBS: add popup object table logic + //ObjectTableDialog* m_popup_table{ nullptr }; #if ENABLE_ENVIRONMENT_MAP GLTexture environment_texture; @@ -6792,17 +6764,17 @@ struct Plater::priv Mouse3DController mouse3d_controller; View3D* view3D; // BBS - // GLToolbar view_toolbar; + //GLToolbar view_toolbar; GLToolbar collapse_toolbar; - Preview* preview; - AssembleView* assemble_view{nullptr}; - bool first_enter_assemble{true}; + Preview *preview; + AssembleView* assemble_view { nullptr }; + bool first_enter_assemble{ true }; std::unique_ptr notification_manager; ProjectDirtyStateManager dirty_state; - BackgroundSlicingProcess background_process; - bool suppressed_backround_processing_update{false}; + BackgroundSlicingProcess background_process; + bool suppressed_backround_processing_update { false }; // TODO: A mechanism would be useful for blocking the plater interactions: // objects would be frozen for the user. In case of arrange, an animation @@ -6812,22 +6784,22 @@ struct Plater::priv // UIThreadWorker can be used as a replacement for BoostThreadWorker if // no additional worker threads are desired (useful for debugging or profiling) PlaterWorker m_worker; - SLAImportDialog* m_sla_import_dlg; + SLAImportDialog * m_sla_import_dlg; - int m_job_prepare_state; + int m_job_prepare_state; - bool delayed_scene_refresh; - std::string delayed_error_message; + bool delayed_scene_refresh; + std::string delayed_error_message; - wxTimer background_process_timer; - wxTimer auto_reslice_timer; + wxTimer background_process_timer; + wxTimer auto_reslice_timer; - std::string label_btn_export; - std::string label_btn_send; + std::string label_btn_export; + std::string label_btn_send; - bool show_render_statistic_dialog{false}; - bool show_wireframe{false}; - bool wireframe_enabled{true}; + bool show_render_statistic_dialog{ false }; + bool show_wireframe{ false }; + bool wireframe_enabled{ true }; static const std::regex pattern_bundle; static const std::regex pattern_3mf; @@ -6837,9 +6809,10 @@ struct Plater::priv bool m_is_dark = false; - priv(Plater* q, MainFrame* main_frame); + priv(Plater *q, MainFrame *main_frame); ~priv(); + bool need_update() const { return m_need_update; } void set_need_update(bool need_update) { m_need_update = need_update; } @@ -6852,8 +6825,7 @@ struct Plater::priv Slic3r::put_other_changes(); dirty_state.update_from_presets(); } - int save_project_if_dirty(const wxString& reason) - { + int save_project_if_dirty(const wxString& reason) { int res = wxID_NO; if (dirty_state.is_dirty()) { MainFrame* mainframe = wxGetApp().mainframe; @@ -6861,13 +6833,11 @@ struct Plater::priv wxString suggested_project_name; wxString project_name = suggested_project_name = get_project_filename(".3mf"); if (suggested_project_name.IsEmpty()) { - fs::path output_file = get_export_file_path(FT_3MF); + fs::path output_file = get_export_file_path(FT_3MF); suggested_project_name = output_file.empty() ? _L("Untitled") : from_u8(output_file.stem().string()); } - res = MessageDialog(mainframe, - reason + "\n" + format_wxstr(_L("Do you want to save changes to \"%1%\"?"), suggested_project_name), - wxString(SLIC3R_APP_FULL_NAME), wxYES_NO | wxCANCEL) - .ShowModal(); + res = MessageDialog(mainframe, reason + "\n" + format_wxstr(_L("Do you want to save changes to \"%1%\"?"), suggested_project_name), + wxString(SLIC3R_APP_FULL_NAME), wxYES_NO | wxCANCEL).ShowModal(); if (res == wxID_YES) if (!mainframe->save_project_as(project_name)) res = wxID_CANCEL; @@ -6875,11 +6845,7 @@ struct Plater::priv } return res; } - void reset_project_dirty_after_save() - { - m_undo_redo_stack_main.mark_current_as_saved(); - dirty_state.reset_after_save(); - } + void reset_project_dirty_after_save() { m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.reset_after_save(); } void reset_project_dirty_initial_presets() { dirty_state.reset_initial_presets(); } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW @@ -6893,7 +6859,7 @@ struct Plater::priv }; void update(unsigned int flags = 0); void select_view(const std::string& direction); - // BBS: add no_slice option + //BBS: add no_slice option void select_view_3D(const std::string& name, bool no_slice = true); void select_next_view_3D(); @@ -6905,7 +6871,7 @@ struct Plater::priv bool are_view3D_labels_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->are_labels_shown(); } void show_view3D_labels(bool show) { - if (current_panel == view3D) { + if (current_panel == view3D) { view3D->get_canvas3d()->show_labels(show); wxGetApp().app_config->set_bool("show_labels", show); } @@ -6914,7 +6880,7 @@ struct Plater::priv bool is_view3D_overhang_shown() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_overhang_shown(); } void show_view3D_overhang(bool show) { - if (current_panel == view3D) { + if (current_panel == view3D) { view3D->get_canvas3d()->show_overhang(show); wxGetApp().app_config->set_bool("show_overhang", show); } @@ -6926,15 +6892,14 @@ struct Plater::priv void reset_window_layout(); Sidebar::DockingState get_sidebar_docking_state(); - bool is_view3D_layers_editing_enabled() const - { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } + bool is_view3D_layers_editing_enabled() const { return (current_panel == view3D) && view3D->get_canvas3d()->is_layers_editing_enabled(); } void set_current_canvas_as_dirty(); GLCanvas3D* get_current_canvas3D(bool exclude_preview = false); void unbind_canvas_event_handlers(); void reset_canvas_volumes(); - bool check_ams_status_impl(bool is_slice_all); // Check whether the printer and ams status are consistent, for grouping algorithm - bool get_machine_sync_status(); // check whether the printer is linked and the printer type is same as selected profile + bool check_ams_status_impl(bool is_slice_all); // Check whether the printer and ams status are consistent, for grouping algorithm + bool get_machine_sync_status(); // check whether the printer is linked and the printer type is same as selected profile // BBS bool init_collapse_toolbar(); @@ -6963,7 +6928,7 @@ struct Plater::priv void update_ui_from_settings(); // BBS std::shared_ptr statusbar(); - std::string get_config(const std::string& key) const; + std::string get_config(const std::string &key) const; BoundingBoxf bed_shape_bb() const; BoundingBox scaled_bed_shape_bb() const; @@ -6973,37 +6938,31 @@ struct Plater::priv LoadStrategy strategy, bool ask_multi = false, bool* published_out = nullptr); - std::vector load_model_objects(const ModelObjectPtrs& model_objects, - bool allow_negative_z = false, - bool split_object = false, - bool auto_drop = true); + std::vector load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true); // Texture-to-color import: a mesh loaded with UVs + a texture map gets its faces clustered // into printable colours, which are then matched against (or added to) the filament list. - struct TextureImportResult - { - Slic3r::PaintedMesh painted; - std::vector matches; - std::vector> new_filament_colors; - std::vector new_filament_preset_names; + struct TextureImportResult { + Slic3r::PaintedMesh painted; + std::vector matches; + std::vector> new_filament_colors; + std::vector new_filament_preset_names; std::vector new_mixed_filaments; - std::vector filament_entries; - size_t existing_filament_count = 0; - bool skipped = false; - bool fallback_to_geometry_only = false; - wxString fallback_warning; + std::vector filament_entries; + size_t existing_filament_count = 0; + bool skipped = false; + bool fallback_to_geometry_only = false; + wxString fallback_warning; }; - bool run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, - TextureImportResult& result, - std::function cancel_callback = {}, + bool run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, TextureImportResult& result, + std::function cancel_callback = {}, std::function progress_callback = {}); - void apply_textured_mesh_import_result(Slic3r::Model& loaded_model, - const std::vector& obj_idxs, + void apply_textured_mesh_import_result(Slic3r::Model& loaded_model, const std::vector& obj_idxs, const TextureImportResult& result, - LoadProgressCallback progress_callback = {}, - bool update_scene = true); - void handle_textured_mesh_import(Slic3r::Model& model, const std::vector& obj_idxs, std::function cancel_callback = {}); + LoadProgressCallback progress_callback = {}, bool update_scene = true); + void handle_textured_mesh_import(Slic3r::Model& model, const std::vector& obj_idxs, + std::function cancel_callback = {}); fs::path get_export_file_path(GUI::FileType file_type); wxString get_export_file(GUI::FileType file_type, const wxString& title = {}, bool published = false); @@ -7028,7 +6987,7 @@ struct Plater::priv void deselect_all(); void exit_gizmo(); void remove(size_t obj_idx); - bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); // BBS + bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -7040,11 +6999,7 @@ struct Plater::priv void scale_selection_to_fit_print_volume(); // Return the active Undo/Redo stack. It may be either the main stack or the Gimzo stack. - Slic3r::UndoRedo::Stack& undo_redo_stack() - { - assert(m_undo_redo_stack_active != nullptr); - return *m_undo_redo_stack_active; - } + Slic3r::UndoRedo::Stack& undo_redo_stack() { assert(m_undo_redo_stack_active != nullptr); return *m_undo_redo_stack_active; } Slic3r::UndoRedo::Stack& undo_redo_stack_main() { return m_undo_redo_stack_main; } void enter_gizmos_stack(); bool leave_gizmos_stack(); @@ -7052,7 +7007,7 @@ struct Plater::priv void take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action); /*void take_snapshot(const wxString& snapshot_name, UndoRedo::SnapshotType snapshot_type = UndoRedo::SnapshotType::Action) { this->take_snapshot(std::string(snapshot_name.ToUTF8().data()), snapshot_type); }*/ - int get_active_snapshot_index(); + int get_active_snapshot_index(); void undo(); void redo(); @@ -7061,25 +7016,22 @@ struct Plater::priv // BBS: backup bool up_to_date(bool saved, bool backup); - void suppress_snapshots() { m_prevent_snapshots++; } - void allow_snapshots() { m_prevent_snapshots--; } + void suppress_snapshots() { m_prevent_snapshots++; } + void allow_snapshots() { m_prevent_snapshots--; } // BBS: single snapshot - void single_snapshots_enter(SingleSnapshot* single) + void single_snapshots_enter(SingleSnapshot *single) { - if (m_single == nullptr) - m_single = single; + if (m_single == nullptr) m_single = single; } - void single_snapshots_leave(SingleSnapshot* single) + void single_snapshots_leave(SingleSnapshot *single) { - if (m_single == single) - m_single = nullptr; + if (m_single == single) m_single = nullptr; } - void process_validation_warning(StringObjectException const& warning) const; - void process_validation_warnings(const std::vector& warnings) const; + void process_validation_warning(StringObjectException const &warning) const; + void process_validation_warnings(const std::vector &warnings) const; - bool background_processing_enabled() const - { + bool background_processing_enabled() const { #ifdef SUPPORT_BACKGROUND_PROCESSING return this->get_config("background_processing") == "1"; #else @@ -7091,7 +7043,7 @@ struct Plater::priv void schedule_background_process(); void schedule_auto_reslice_if_needed(); void trigger_auto_reslice_now(); - int auto_slice_delay_seconds() const; + int auto_slice_delay_seconds() const; // Update background processing thread from the current config and Model. enum UpdateBackgroundProcessReturnState { // update_background_process() reports, that the Print / SLAPrint was updated in a way, @@ -7114,8 +7066,7 @@ struct Plater::priv bool restart_background_process(unsigned int state); // returns bit mask of UpdateBackgroundProcessReturnState unsigned int update_restart_background_process(bool force_scene_update, bool force_preview_update); - void show_delayed_error_message() - { + void show_delayed_error_message() { if (!this->delayed_error_message.empty()) { std::string msg = std::move(this->delayed_error_message); this->delayed_error_message.clear(); @@ -7131,7 +7082,7 @@ struct Plater::priv void replace_all_with_stl(); void reload_all_from_disk(); - // BBS: add no_slice option + //BBS: add no_slice option void set_current_panel(wxPanel* panel, bool no_slice = true); void on_combobox_select(wxCommandEvent&); @@ -7145,9 +7096,9 @@ struct Plater::priv void on_slicing_began(); void clear_warnings(); - void add_warning(const Slic3r::PrintStateBase::Warning& warning, size_t oid); + void add_warning(const Slic3r::PrintStateBase::Warning &warning, size_t oid); // Update notification manager with the current state of warnings produced by the background process (slicing). - void actualize_slicing_warnings(const PrintBase& print); + void actualize_slicing_warnings(const PrintBase &print); void actualize_object_warnings(const PrintBase& print); // Displays dialog window with list of warnings. // Returns true if user clicks OK. @@ -7160,32 +7111,32 @@ struct Plater::priv void on_action_split_objects(SimpleEvent&); void on_action_split_volumes(SimpleEvent&); void on_action_layersediting(SimpleEvent&); - void on_create_filament(SimpleEvent&); - void on_modify_filament(SimpleEvent&); - void on_add_filament(SimpleEvent&); - void on_delete_filament(SimpleEvent&); - void on_add_custom_filament(ColorEvent&); + void on_create_filament(SimpleEvent &); + void on_modify_filament(SimpleEvent &); + void on_add_filament(SimpleEvent &); + void on_delete_filament(SimpleEvent &); + void on_add_custom_filament(ColorEvent &); void on_object_select(SimpleEvent&); - void show_right_click_menu(Vec2d mouse_position, wxMenu* menu); + void show_right_click_menu(Vec2d mouse_position, wxMenu *menu); void on_right_click(RBtnEvent&); - // BBS: add model repair - void on_repair_model(wxCommandEvent& event); - void on_filament_color_changed(wxCommandEvent& event); - void show_install_plugin_hint(wxCommandEvent& event); - void install_network_plugin(wxCommandEvent& event); - void show_preview_only_hint(wxCommandEvent& event); - // BBS: add part plate related logic + //BBS: add model repair + void on_repair_model(wxCommandEvent &event); + void on_filament_color_changed(wxCommandEvent &event); + void show_install_plugin_hint(wxCommandEvent &event); + void install_network_plugin(wxCommandEvent &event); + void show_preview_only_hint(wxCommandEvent &event); + //BBS: add part plate related logic void on_plate_right_click(RBtnPlateEvent&); void on_plate_selected(SimpleEvent&); void on_action_request_model_id(wxCommandEvent& evt); void on_action_download_project(wxCommandEvent& evt); void on_slice_button_status(bool enable); - // BBS: GUI refactor: GLToolbar + //BBS: GUI refactor: GLToolbar void on_action_open_project(SimpleEvent&); void on_action_slice_plate(SimpleEvent&); void on_action_slice_all(SimpleEvent&); - void on_action_publish(wxCommandEvent& evt); + void on_action_publish(wxCommandEvent &evt); void on_action_print_plate(SimpleEvent&); void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL); void on_action_print_all(SimpleEvent&); @@ -7194,7 +7145,7 @@ struct Plater::priv void on_action_export_sliced_file(SimpleEvent&); void on_action_export_all_sliced_file(SimpleEvent&); void on_action_select_sliced_plate(wxCommandEvent& evt); - // BBS: change dark/light mode + //BBS: change dark/light mode void on_change_color_mode(SimpleEvent& evt); void on_apple_change_color_mode(wxSysColourChangedEvent& evt); void apply_color_mode(); @@ -7202,9 +7153,9 @@ struct Plater::priv void on_3dcanvas_mouse_dragging_started(SimpleEvent&); void on_3dcanvas_mouse_dragging_finished(SimpleEvent&); - // void show_action_buttons(const bool is_ready_to_slice) const; + //void show_action_buttons(const bool is_ready_to_slice) const; bool show_publish_dlg(bool show = true); - void update_publish_dialog_status(wxString& msg, int percent = -1); + void update_publish_dialog_status(wxString &msg, int percent = -1); void on_action_print_plate_from_sdcard(SimpleEvent&); void on_tab_selection_changing(wxBookCtrlEvent&); @@ -7213,16 +7164,16 @@ struct Plater::priv // triangulate the bed and store the triangles into m_bed.m_triangles, // fills the m_bed.m_grid_lines and sets m_bed.m_origin. // Sets m_bed.m_polygon to limit the object placement. - // BBS: add bed exclude area - void set_bed_shape(const Pointfs& shape, - const Pointfs& exclude_areas, - const Pointfs& wrapping_exclude_areas, - const double printable_height, + //BBS: add bed exclude area + void set_bed_shape(const Pointfs &shape, + const Pointfs &exclude_areas, + const Pointfs &wrapping_exclude_areas, + const double printable_height, std::vector extruder_areas, - std::vector extruder_heights, - const std::string& custom_texture, - const std::string& custom_model, - bool force_as_custom = false); + std::vector extruder_heights, + const std::string &custom_texture, + const std::string &custom_model, + bool force_as_custom = false); bool can_delete() const; bool can_delete_all() const; @@ -7240,7 +7191,7 @@ struct Plater::priv bool can_set_instance_to_object() const; bool can_mirror() const; bool can_reload_from_disk() const; - // BBS: + //BBS: bool can_fillcolor() const; bool has_assemble_view() const; bool can_replace_with_stl() const; @@ -7250,15 +7201,12 @@ struct Plater::priv bool can_scale_to_print_volume() const; #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT - // BBS: add plate_id for thumbnail - void generate_thumbnail(ThumbnailData& data, - unsigned int w, - unsigned int h, - const ThumbnailsParams& thumbnail_params, - Camera::EType camera_type, - Camera::ViewAngleType camera_view_angle_type = Camera::ViewAngleType::Iso, - bool for_picking = false, - bool ban_light = false); + //BBS: add plate_id for thumbnail + void generate_thumbnail(ThumbnailData& data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type = Camera::ViewAngleType::Iso, + bool for_picking = false, + bool ban_light = false); ThumbnailsList generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type); PlateBBoxData generate_first_layer_bbox(); @@ -7270,7 +7218,7 @@ struct Plater::priv wxString get_export_gcode_filename(const wxString& extension = wxEmptyString, bool only_filename = false, bool export_all = false) const; void set_project_filename(const wxString& filename); - // BBS store bbs project name + //BBS store bbs project name wxString get_project_name(); void set_project_name(const wxString& project_name); void update_title_dirty_status(); @@ -7280,34 +7228,27 @@ struct Plater::priv void update_objects_position_when_select_preset(const std::function& select_prest); - // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will - // not override it. - // mutable bool ready_to_slice = { false }; - // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case - // when the background processing finishes. - ExportingStatus exporting_status{NOT_EXPORTING}; - std::string last_output_path; - std::string last_output_dir_path; - // BBS store machine_sn and 3mf_path for PrintJob - PrintPrepareData m_print_job_data; - bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } - int process_completed_with_error{-1}; //-1 means no error + // Caching last value of show_action_buttons parameter for show_action_buttons(), so that a callback which does not know this state will not override it. + //mutable bool ready_to_slice = { false }; + // Flag indicating that the G-code export targets a removable device, therefore the show_action_buttons() needs to be called at any case when the background processing finishes. + ExportingStatus exporting_status { NOT_EXPORTING }; + std::string last_output_path; + std::string last_output_dir_path; + //BBS store machine_sn and 3mf_path for PrintJob + PrintPrepareData m_print_job_data; + bool inside_snapshot_capture() { return m_prevent_snapshots != 0; } + int process_completed_with_error { -1 }; //-1 means no error - // BBS: project - BBLProject project; + //BBS: project + BBLProject project; - // BBS: add print project related logic + //BBS: add print project related logic void update_fff_scene_only_shells(bool only_shells = true); - // BBS: add popup object table logic + //BBS: add popup object table logic bool PopupObjectTable(int object_id, int volume_id, const wxPoint& position); void on_action_send_to_printer(bool isall = false); void on_action_send_to_multi_machine(SimpleEvent&); - int update_print_required_data(Slic3r::DynamicPrintConfig config, - Slic3r::Model model, - Slic3r::PlateDataPtrs plate_data_list, - std::string file_name, - std::string file_path); - + int update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path); private: bool layers_height_allowed() const; @@ -7320,30 +7261,30 @@ private: void on_action_export_to_sdcard_all(SimpleEvent&); void update_plugin_when_launch(wxCommandEvent& event); // path to project folder stored with no extension - boost::filesystem::path m_project_folder; + boost::filesystem::path m_project_folder; /* display project name */ - wxString m_project_name; + wxString m_project_name; - Slic3r::UndoRedo::Stack m_undo_redo_stack_main; - Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos; - Slic3r::UndoRedo::Stack* m_undo_redo_stack_active = &m_undo_redo_stack_main; - int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting". - * Like for "delete selected" or "set numbers of copies" - * we should call tack_snapshot just ones - * instead of calls for each action separately - * */ + Slic3r::UndoRedo::Stack m_undo_redo_stack_main; + Slic3r::UndoRedo::Stack m_undo_redo_stack_gizmos; + Slic3r::UndoRedo::Stack *m_undo_redo_stack_active = &m_undo_redo_stack_main; + int m_prevent_snapshots = 0; /* Used for avoid of excess "snapshoting". + * Like for "delete selected" or "set numbers of copies" + * we should call tack_snapshot just ones + * instead of calls for each action separately + * */ // BBS: single snapshot - Plater::SingleSnapshot* m_single = nullptr; + Plater::SingleSnapshot *m_single = nullptr; // BBS: backup - size_t m_saved_timestamp = 0; + size_t m_saved_timestamp = 0; size_t m_backup_timestamp = 0; - std::string m_last_fff_printer_profile_name; - std::string m_last_sla_printer_profile_name; + std::string m_last_fff_printer_profile_name; + std::string m_last_sla_printer_profile_name; // vector of all warnings generated by last slicing std::vector> current_warnings; - bool show_warning_dialog{false}; + bool show_warning_dialog { false }; }; Plater::~Plater() = default; @@ -7354,7 +7295,7 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase); -bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& filenames) +bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) { #ifdef WIN32 // hides the system icon @@ -7368,14 +7309,14 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& fi // When only one .svg file is dropped on scene if (filenames.size() == 1) { - const wxString& filename = filenames.Last(); - const wxString file_extension = filename.substr(filename.length() - 4); + const wxString &filename = filenames.Last(); + const wxString file_extension = filename.substr(filename.length() - 4); if (file_extension.CmpNoCase(".svg") == 0) { // BBS: GUI refactor: move sidebar to the left - const wxPoint offset = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); + const wxPoint offset = m_plater.GetPosition() + m_plater.p->current_panel->GetPosition(); Vec2d mouse_position(x - offset.x, y - offset.y); // Scale for retina displays - const GLCanvas3D* canvas = m_plater.canvas3D(); + const GLCanvas3D *canvas = m_plater.canvas3D(); canvas->apply_retina_scale(mouse_position); return emboss_svg(m_plater, filename, mouse_position); } @@ -7385,26 +7326,29 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString& fi return res; } -Plater::priv::priv(Plater* q, MainFrame* main_frame) +Plater::priv::priv(Plater *q, MainFrame *main_frame) : q(q) , main_frame(main_frame) - // BBS: add bed_exclude_area - , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys( - {"printable_area", "bed_exclude_area", "wrapping_exclude_area", "extruder_printable_area", "bed_custom_texture", - "bed_custom_model", "print_sequence", "extruder_clearance_radius", "extruder_clearance_height_to_lid", - "extruder_clearance_height_to_rod", "nozzle_height", "skirt_type", "skirt_loops", "skirt_speed", "min_skirt_length", - "skirt_distance", "skirt_start_angle", "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", - "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", "enable_prime_tower", - "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", - "prime_tower_enable_framework", "prime_tower_infill_gap", "prime_volume", "extruder_colour", "filament_colour", "filament_type", - "filament_is_support", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", - // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. - "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", "wall_loops", "outer_wall_filament_id", - "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers", "enable_support", - "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", - "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length", "wipe_tower_rib_width", - "wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos", "master_extruder_id"})) + //BBS: add bed_exclude_area + , config(Slic3r::DynamicPrintConfig::new_from_defaults_keys({ + "printable_area", "bed_exclude_area", "wrapping_exclude_area", "extruder_printable_area", "bed_custom_texture", "bed_custom_model", "print_sequence", + "extruder_clearance_radius", + "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", + "nozzle_height", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", + "brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", + "enable_prime_tower", "wipe_tower_x", "wipe_tower_y", "prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "prime_tower_enable_framework", + "prime_tower_infill_gap", "prime_volume", + "extruder_colour", "filament_colour", "filament_type", "filament_is_support", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", + // These values are necessary to construct SlicingParameters by the Canvas3D variable layer height editor. + "layer_height", "initial_layer_print_height", "min_layer_height", "max_layer_height", + "wall_loops", "outer_wall_filament_id", "inner_wall_filament_id", "sparse_infill_density", "sparse_infill_filament_id", "top_shell_layers", + "enable_support", "support_filament", "support_interface_filament", + "support_top_z_distance", "support_bottom_z_distance", "raft_layers", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", + "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", + "wipe_tower_filament", + "best_object_pos", "master_extruder_id" + })) , sidebar(new Sidebar(q)) , notification_manager(std::make_unique(q)) , m_worker{q, std::make_unique(notification_manager.get()), "ui_worker"} @@ -7412,7 +7356,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) , m_job_prepare_state(Job::JobPrepareState::PREPARE_STATE_DEFAULT) , delayed_scene_refresh(false) , collapse_toolbar(GLToolbar::Normal, "Collapse") - // BBS :partplatelist construction + //BBS :partplatelist construction , partplate_list(this->q, &model) { m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; @@ -7427,21 +7371,21 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) if (disable_wayland_floating) m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING); #endif - // m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0); - // m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2); + //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_PANE_BORDER_SIZE, 0); + //m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_SASH_SIZE, 2); m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18); m_aui_mgr.GetArtProvider()->SetMetric(wxAUI_DOCKART_GRADIENT_TYPE, wxAUI_GRADIENT_NONE); this->q->SetFont(Slic3r::GUI::wxGetApp().normal_font()); - // BBS: use the first partplate's print for background process + //BBS: use the first partplate's print for background process partplate_list.update_slice_context_to_current_plate(background_process); /* background_process.set_fff_print(&fff_print); background_process.set_sla_print(&sla_print); background_process.set_gcode_result(&gcode_result); - background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, - Camera::EType::Ortho); }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); + background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); + background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); background_process.set_export_began_event(EVT_EXPORT_BEGAN); // Default printer technology for default config. @@ -7455,8 +7399,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) sla_print.set_status_callback(statuscb); */ // BBS: to be checked. Not follow patch. - background_process.set_thumbnail_cb( - [this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); + background_process.set_thumbnail_cb([this](const ThumbnailsParams& params) { return this->generate_thumbnails(params, Camera::EType::Ortho); }); background_process.set_slicing_completed_event(EVT_SLICING_COMPLETED); background_process.set_finished_event(EVT_PROCESS_COMPLETED); background_process.set_export_began_event(EVT_EXPORT_BEGAN); @@ -7479,17 +7422,16 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) main_frame->m_tabpanel->Bind(wxEVT_NOTEBOOK_PAGE_CHANGING, &priv::on_tab_selection_changing, this); auto* panel_3d = new wxPanel(q); - view3D = new View3D(panel_3d, bed, &model, config, &background_process); - // BBS: use partplater's gcode - preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), - [this]() { schedule_background_process(); }); + view3D = new View3D(panel_3d, bed, &model, config, &background_process); + //BBS: use partplater's gcode + preview = new Preview(panel_3d, bed, &model, config, &background_process, partplate_list.get_current_slice_result(), [this]() { schedule_background_process(); }); assemble_view = new AssembleView(panel_3d, bed, &model, config, &background_process); #ifdef __APPLE__ // BBS // set default view_toolbar icons size equal to GLGizmosManager::Default_Icons_Size - // view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); + //view_toolbar.set_icons_size(GLGizmosManager::Default_Icons_Size); #endif // __APPLE__ panels.push_back(view3D); @@ -7498,7 +7440,8 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) this->background_process_timer.SetOwner(this->q, 0); this->auto_reslice_timer.SetOwner(this->q, 0); - this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent& evt) { + this->q->Bind(wxEVT_TIMER, [this](wxTimerEvent &evt) + { if (&evt.GetTimer() == &this->background_process_timer) { if (!this->suppressed_backround_processing_update) this->update_restart_background_process(false, false); @@ -7513,8 +7456,13 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) update(); // Orca: Make sidebar dockable - m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo().Name("sidebar").Left().CloseButton(false).TopDockable(false).BottomDockable(false).BestSize( - wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); + m_aui_mgr.AddPane(sidebar, wxAuiPaneInfo() + .Name("sidebar") + .Left() + .CloseButton(false) + .TopDockable(false) + .BottomDockable(false) + .BestSize(wxSize(39 * wxGetApp().em_unit(), 90 * wxGetApp().em_unit()))); auto* panel_sizer = new wxBoxSizer(wxHORIZONTAL); panel_sizer->Add(view3D, 1, wxEXPAND | wxALL, 0); @@ -7529,8 +7477,8 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) auto& sidebar = m_aui_mgr.GetPane(this->sidebar); // Load previous window layout - const auto cfg = wxGetApp().app_config; - wxString layout = wxString::FromUTF8(cfg->get("window_layout")); + const auto cfg = wxGetApp().app_config; + wxString layout = wxString::FromUTF8(cfg->get("window_layout")); if (!layout.empty()) { bool removed_floating_state = false; #ifdef __WXGTK__ @@ -7564,6 +7512,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) menus.init(main_frame); + // Events: if (wxGetApp().is_editor()) { @@ -7575,14 +7524,15 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) // jump to found option from SearchDialog q->Bind(wxCUSTOMEVT_JUMP_TO_OPTION, [this](wxCommandEvent& evt) { sidebar->jump_to_option(evt.GetInt()); }); q->Bind(wxCUSTOMEVT_JUMP_TO_OBJECT, [this](wxCommandEvent& evt) { - auto client_data = evt.GetClientData(); + auto client_data = evt.GetClientData(); ObjectDataViewModelNode* data = static_cast(client_data); sidebar->jump_to_object(data); - }); + } + ); } wxGLCanvas* view3D_canvas = view3D->get_wxglcanvas(); - // BBS: GUI refactor + //BBS: GUI refactor wxGLCanvas* preview_canvas = preview->get_wxglcanvas(); if (wxGetApp().is_editor()) { @@ -7590,34 +7540,30 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) view3D_canvas->Bind(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS, [this](SimpleEvent&) { delayed_error_message.clear(); this->background_process_timer.Start(500, wxTIMER_ONE_SHOT); - }); + }); view3D_canvas->Bind(EVT_GLCANVAS_OBJECT_SELECT, &priv::on_object_select, this); view3D_canvas->Bind(EVT_GLCANVAS_RIGHT_CLICK, &priv::on_right_click, this); - // BBS: add part plate related logic + //BBS: add part plate related logic view3D_canvas->Bind(EVT_GLCANVAS_PLATE_RIGHT_CLICK, &priv::on_plate_right_click, this); view3D_canvas->Bind(EVT_GLCANVAS_REMOVE_OBJECT, [q](SimpleEvent&) { q->remove_selected(); }); view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent& evt) { - // BBS arrange from EVT set default state. + //BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->arrange(); - }); + this->q->arrange(); }); view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE_PARTPLATE, [this](SimpleEvent& evt) { - // BBS arrange from EVT set default state. + //BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_MENU); - this->q->arrange(); - }); + this->q->arrange(); }); view3D_canvas->Bind(EVT_GLCANVAS_ORIENT, [this](SimpleEvent& evt) { - // BBS orient from EVT set default state. + //BBS orient from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->orient(); - }); + this->q->orient(); }); view3D_canvas->Bind(EVT_GLCANVAS_ORIENT_PARTPLATE, [this](SimpleEvent& evt) { - // BBS orient from EVT set default state. + //BBS orient from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_MENU); - this->q->orient(); - }); - // BBS - view3D_canvas->Bind(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, [this](SimpleEvent&) { this->q->select_curr_plate_all(); }); + this->q->orient(); }); + //BBS + view3D_canvas->Bind(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, [this](SimpleEvent&) {this->q->select_curr_plate_all(); }); view3D_canvas->Bind(EVT_GLCANVAS_PRINTABLE, [this](SimpleEvent& evt) { this->sidebar->obj_list()->toggle_printable_state(); }); view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); }); @@ -7626,18 +7572,14 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) if (this->q->is_view3D_shown()) wxGetApp().open_speed_dial(); }); - view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt) { - if (evt.data == 1) - this->q->increase_instances(); - else if (this->can_decrease_instances()) - this->q->decrease_instances(); - }); + view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event& evt) + { if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_FORCE_UPDATE, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_ROTATED, [this](SimpleEvent&) { update(); }); view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_SCALED, [this](SimpleEvent&) { update(); }); // BBS - // view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); + //view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { this->sidebar->enable_buttons(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, [this](Event& evt) { on_slice_button_status(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_GEOMETRY, &priv::on_update_geometry, this); view3D_canvas->Bind(EVT_GLCANVAS_MOUSE_DRAGGING_STARTED, &priv::on_3dcanvas_mouse_dragging_started, this); @@ -7646,89 +7588,83 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) view3D_canvas->Bind(EVT_GLCANVAS_RESETGIZMOS, [this](SimpleEvent&) { reset_all_gizmos(); }); view3D_canvas->Bind(EVT_GLCANVAS_UNDO, [this](SimpleEvent&) { this->undo(); }); view3D_canvas->Bind(EVT_GLCANVAS_REDO, [this](SimpleEvent&) { this->redo(); }); - view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, - [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); - view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, - [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); - view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, - [this](Event& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); - view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, - [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + view3D_canvas->Bind(EVT_GLCANVAS_RESET_LAYER_HEIGHT_PROFILE, [this](SimpleEvent&) { this->view3D->get_canvas3d()->reset_layer_height_profile(); }); + view3D_canvas->Bind(EVT_GLCANVAS_ADAPTIVE_LAYER_HEIGHT_PROFILE, [this](Event& evt) { this->view3D->get_canvas3d()->adaptive_layer_height_profile(evt.data); }); + view3D_canvas->Bind(EVT_GLCANVAS_SMOOTH_LAYER_HEIGHT_PROFILE, [this](HeightProfileSmoothEvent& evt) { this->view3D->get_canvas3d()->smooth_layer_height_profile(evt.data); }); view3D_canvas->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->reload_all_from_disk(); }); // 3DScene/Toolbar: view3D_canvas->Bind(EVT_GLTOOLBAR_ADD, &priv::on_action_add, this); view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE, [q](SimpleEvent&) { q->remove_selected(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [this](SimpleEvent&) { delete_all_objects_from_model(); }); - // view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); +// view3D_canvas->Bind(EVT_GLTOOLBAR_DELETE_ALL, [q](SimpleEvent&) { q->reset_with_confirm(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_ADD_PLATE, &priv::on_action_add_plate, this); view3D_canvas->Bind(EVT_GLTOOLBAR_DEL_PLATE, &priv::on_action_del_plate, this); view3D_canvas->Bind(EVT_GLTOOLBAR_ORIENT, [this](SimpleEvent&) { - // BBS arrange from EVT set default state. + //BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); - this->q->orient(); - }); + this->q->orient(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_ARRANGE, [this](SimpleEvent&) { - // BBS arrange from EVT set default state. + //BBS arrange from EVT set default state. this->q->set_prepare_state(Job::PREPARE_STATE_DEFAULT); this->q->arrange(); - }); + }); view3D_canvas->Bind(EVT_GLTOOLBAR_CUT, [q](SimpleEvent&) { q->cut_selection_to_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_COPY, [q](SimpleEvent&) { q->copy_selection_to_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_PASTE, [q](SimpleEvent&) { q->paste_from_clipboard(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_LAYERSEDITING, &priv::on_action_layersediting, this); - // BBS: add clone + //BBS: add clone view3D_canvas->Bind(EVT_GLTOOLBAR_CLONE, [q](SimpleEvent&) { q->clone_selection(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_MORE, [q](SimpleEvent&) { q->increase_instances(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_FEWER, [q](SimpleEvent&) { q->decrease_instances(); }); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_OBJECTS, &priv::on_action_split_objects, this); view3D_canvas->Bind(EVT_GLTOOLBAR_SPLIT_VOLUMES, &priv::on_action_split_volumes, this); - // BBS: GUI refactor: GLToolbar + //BBS: GUI refactor: GLToolbar view3D_canvas->Bind(EVT_GLTOOLBAR_OPEN_PROJECT, &priv::on_action_open_project, this); - // view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); - // view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); - // view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); - // view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); - // view3D_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + //view3D_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); view3D_canvas->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); - // preview also send these events - // preview_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); - // preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); - // preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); - // review_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); + //preview also send these events + //preview_canvas->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); + //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_PLATE, &priv::on_action_print_plate, this); + //preview_canvas->Bind(EVT_GLTOOLBAR_PRINT_ALL, &priv::on_action_print_all, this); + //review_canvas->Bind(EVT_GLTOOLBAR_EXPORT_GCODE, &priv::on_action_export_gcode, this); view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_OBJECT, [main_frame](SimpleEvent&) { - if (main_frame->m_param_panel) { - main_frame->m_param_panel->switch_to_object(false); - } - }); + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_object(false); + } + }); view3D_canvas->Bind(EVT_GLCANVAS_SWITCH_TO_GLOBAL, [main_frame](SimpleEvent&) { - if (main_frame->m_param_panel) { - main_frame->m_param_panel->switch_to_global(); - } - }); + if (main_frame->m_param_panel) { + main_frame->m_param_panel->switch_to_global(); + } + }); } view3D_canvas->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); // Preview events: preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); }); preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE_BED_SHAPE, [q](SimpleEvent&) { q->set_bed_shape(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent&) { preview->get_canvas3d()->set_as_dirty(); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_UPDATE, [this](SimpleEvent &) { + preview->get_canvas3d()->set_as_dirty(); + }); if (wxGetApp().is_editor()) { preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_TAB, [this](SimpleEvent&) { select_next_view_3D(); }); - preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, - [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); + preview->get_wxglcanvas()->Bind(EVT_GLCANVAS_COLLAPSE_SIDEBAR, [this](SimpleEvent&) { this->q->collapse_sidebar(!this->q->is_sidebar_collapsed()); }); preview->get_wxglcanvas()->Bind(EVT_CUSTOMEVT_TICKSCHANGED, [this](wxCommandEvent& event) { - Type tick_event_type = (Type) event.GetInt(); - Model& model = wxGetApp().plater()->model(); - // BBS: replace model custom gcode with current plate custom gcode - model.plates_custom_gcodes[model.curr_plate_index] = - preview->get_canvas3d()->get_gcode_viewer().get_layers_slider()->GetTicksValues(); + Type tick_event_type = (Type)event.GetInt(); + Model& model = wxGetApp().plater()->model(); + //BBS: replace model custom gcode with current plate custom gcode + model.plates_custom_gcodes[model.curr_plate_index] = preview->get_canvas3d()->get_gcode_viewer().get_layers_slider()->GetTicksValues(); // BBS set to invalid state only - if (tick_event_type == Type::ToolChange || tick_event_type == Type::Custom || tick_event_type == Type::Template || - tick_event_type == Type::PausePrint) { - PartPlate* plate = this->q->get_partplate_list().get_curr_plate(); + if (tick_event_type == Type::ToolChange || tick_event_type == Type::Custom || tick_event_type == Type::Template || tick_event_type == Type::PausePrint) { + PartPlate *plate = this->q->get_partplate_list().get_curr_plate(); if (plate) { plate->update_slice_result_valid_state(false); } @@ -7745,7 +7681,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) if (wxGetApp().is_gcode_viewer()) preview->Bind(EVT_GLCANVAS_RELOAD_FROM_DISK, [this](SimpleEvent&) { this->q->reload_gcode_from_disk(); }); - // BBS + //BBS wxGLCanvas* assemble_canvas = assemble_view->get_wxglcanvas(); if (wxGetApp().is_editor()) { assemble_canvas->Bind(EVT_GLTOOLBAR_FILLCOLOR, [q](IntEvent& evt) { q->fill_color(evt.get_data()); }); @@ -7763,7 +7699,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) q->Bind(EVT_EXPORT_BEGAN, &priv::on_export_began, this); q->Bind(EVT_EXPORT_FINISHED, &priv::on_export_finished, this); q->Bind(EVT_GLVIEWTOOLBAR_3D, [q](SimpleEvent&) { q->select_view_3D("3D"); }); - // BBS: set on_slice to false + //BBS: set on_slice to false q->Bind(EVT_GLVIEWTOOLBAR_PREVIEW, [q](SimpleEvent&) { q->select_view_3D("Preview", false); }); q->Bind(EVT_GLTOOLBAR_SLICE_PLATE, &priv::on_action_slice_plate, this); q->Bind(EVT_GLTOOLBAR_SLICE_ALL, &priv::on_action_slice_all, this); @@ -7784,14 +7720,14 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) q->Bind(EVT_PRINT_FINISHED, [q](wxCommandEvent& evt) { q->print_job_finished(evt); }); q->Bind(EVT_SEND_CALIBRATION_FINISHED, [q](wxCommandEvent& evt) { q->send_calibration_job_finished(evt); }); q->Bind(EVT_SEND_FINISHED, [q](wxCommandEvent& evt) { q->send_job_finished(evt); }); - q->Bind(EVT_PUBLISH_FINISHED, [q](wxCommandEvent& evt) { q->publish_job_finished(evt); }); - q->Bind(EVT_OPEN_PLATESETTINGSDIALOG, [q](wxCommandEvent& evt) { q->open_platesettings_dialog(evt); }); - q->Bind(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, [q](wxCommandEvent& evt) { q->open_filament_map_setting_dialog(evt); }); - // q->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); + q->Bind(EVT_PUBLISH_FINISHED, [q](wxCommandEvent& evt) { q->publish_job_finished(evt);}); + q->Bind(EVT_OPEN_PLATESETTINGSDIALOG, [q](wxCommandEvent& evt) { q->open_platesettings_dialog(evt);}); + q->Bind(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG, [q](wxCommandEvent &evt) { q->open_filament_map_setting_dialog(evt); }); + //q->Bind(EVT_GLVIEWTOOLBAR_ASSEMBLE, [q](SimpleEvent&) { q->select_view_3D("Assemble"); }); } // Drop target: - q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership + q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); // if my understanding is right, wxWindow takes the owenership q->Layout(); apply_color_mode(); @@ -7817,41 +7753,43 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) // Register an USB HID (Human Interface Device) attach event. evt contains Win32 path to the USB device containing VID, PID and other info. // This event wakes up the Mouse3DController's background thread to enumerate HID devices, if the VID of the callback event // is one of the 3D Mouse vendors (3DConnexion or Logitech). - this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent& evt) { mouse3d_controller.device_attached(evt.data); }); - this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) { mouse3d_controller.device_detached(evt.data); }); + this->q->Bind(EVT_HID_DEVICE_ATTACHED, [this](HIDDeviceAttachedEvent &evt) { + mouse3d_controller.device_attached(evt.data); + }); + this->q->Bind(EVT_HID_DEVICE_DETACHED, [this](HIDDeviceAttachedEvent& evt) { + mouse3d_controller.device_detached(evt.data); + }); #endif /* _WIN32 */ - // notification_manager = new NotificationManager(this->q); + //notification_manager = new NotificationManager(this->q); if (wxGetApp().is_editor()) { this->q->Bind(EVT_EJECT_DRIVE_NOTIFICAION_CLICKED, [this](EjectDriveNotificationClickedEvent&) { this->q->eject_drive(); }); this->q->Bind(EVT_EXPORT_GCODE_NOTIFICAION_CLICKED, [this](ExportGcodeNotificationClickedEvent&) { this->q->export_gcode(true); }); - this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, - [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); + this->q->Bind(EVT_PRESET_UPDATE_AVAILABLE_CLICKED, [](PresetUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->on_update_notification_confirm(); }); this->q->Bind(EVT_PRINTER_CONFIG_UPDATE_AVAILABLE_CLICKED, [](PrinterConfigUpdateAvailableClickedEvent&) { wxGetApp().get_preset_updater()->do_printer_config_update(); - wxGetApp().getDeviceManager()->reload_printer_settings(); - }); + wxGetApp().getDeviceManager()->reload_printer_settings(); }); /* BBS do not handle removeable driver event */ - this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent& evt) { + this->q->Bind(EVT_REMOVABLE_DRIVE_EJECTED, [this](RemovableDriveEjectEvent &evt) { if (evt.data.second) { // BBS - // this->show_action_buttons(this->ready_to_slice); + //this->show_action_buttons(this->ready_to_slice); notification_manager->close_notification_of_type(NotificationType::ExportFinished); - notification_manager->push_notification( - NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, - format(_L("Successfully unmounted. The device %s (%s) can now be safely removed from the computer."), - evt.data.first.name, evt.data.first.path)); + notification_manager->push_notification(NotificationType::CustomNotification, + NotificationManager::NotificationLevel::RegularNotificationLevel, + format(_L("Successfully unmounted. The device %s (%s) can now be safely removed from the computer."), evt.data.first.name, evt.data.first.path) + ); } else { notification_manager->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, - format(_L("Ejecting of device %s (%s) has failed."), evt.data.first.name, - evt.data.first.path)); + format(_L("Ejecting of device %s (%s) has failed."), evt.data.first.name, evt.data.first.path) + ); } }); - this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent&) { + this->q->Bind(EVT_REMOVABLE_DRIVES_CHANGED, [this](RemovableDrivesChangedEvent &) { // BBS - // this->show_action_buttons(this->ready_to_slice); + //this->show_action_buttons(this->ready_to_slice); // Close notification ExportingFinished but only if last export was to removable notification_manager->device_ejected(); }); @@ -7865,12 +7803,12 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) } // Initialize the Undo / Redo stack with a first snapshot. - // this->take_snapshot("New Project", UndoRedo::SnapshotType::ProjectSeparator); + //this->take_snapshot("New Project", UndoRedo::SnapshotType::ProjectSeparator); // Reset the "dirty project" flag. m_undo_redo_stack_main.mark_current_as_saved(); dirty_state.update_from_undo_redo_stack(false); - // this->take_snapshot("New Project"); - // BBS: save project confirm + //this->take_snapshot("New Project"); + // BBS: save project confirm up_to_date(true, false); up_to_date(true, true); model.set_need_backup(); @@ -7882,9 +7820,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) std::string last_backup = last; std::string originfile; if (Slic3r::has_restore_data(last_backup, originfile)) { - auto result = MessageDialog(this->q, _L("Previously unsaved items have been detected. Do you want to restore them\?"), - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Restore"), wxYES_NO | wxYES_DEFAULT | wxCENTRE) - .ShowModal(); + auto result = MessageDialog(this->q, _L("Previously unsaved items have been detected. Do you want to restore them\?"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Restore"), wxYES_NO | wxYES_DEFAULT | wxCENTRE).ShowModal(); if (result == wxID_YES) { this->q->load_project(from_path(last_backup), from_path(originfile)); Slic3r::backup_soon(); @@ -7898,13 +7834,13 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) } catch (...) {} - + if (this->q->get_project_filename().IsEmpty() && this->q->is_empty_project()) { int skip_confirm = e.GetInt(); this->q->new_project(skip_confirm, true); } }); - // wxPostEvent(this->q, wxCommandEvent{EVT_RESTORE_PROJECT}); + //wxPostEvent(this->q, wxCommandEvent{EVT_RESTORE_PROJECT}); } this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent& evt) { @@ -7917,7 +7853,7 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) wxGetApp().mainframe->Raise(); this->q->load_files(input_files); }); - + this->q->Bind(EVT_START_DOWNLOAD_OTHER_INSTANCE, [](StartDownloadOtherInstanceEvent& evt) { BOOST_LOG_TRIVIAL(trace) << "Received url from other instance event."; wxGetApp().mainframe->Show(); @@ -7925,12 +7861,15 @@ Plater::priv::priv(Plater* q, MainFrame* main_frame) for (size_t i = 0; i < evt.data.size(); ++i) { wxGetApp().start_download(evt.data[i]); } + + }); + this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { + bring_instance_forward(); }); - this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent&) { bring_instance_forward(); }); wxGetApp().other_instance_message_handler()->init(this->q); // collapse sidebar according to saved value - // if (wxGetApp().is_editor()) { + //if (wxGetApp().is_editor()) { // bool is_collapsed = wxGetApp().app_config->get("collapsed_sidebar") == "1"; // sidebar->collapse(is_collapsed); //} @@ -7955,18 +7894,16 @@ void Plater::priv::update(unsigned int flags) model.center_instances_around_point(this->bed.build_volume().bed_center()); #endif - unsigned int update_status = 0; - const bool force_background_processing_restart = this->printer_technology == ptSLA || - (flags & (unsigned int) UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); + unsigned int update_status = 0; + const bool force_background_processing_restart = this->printer_technology == ptSLA || (flags & (unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE); if (force_background_processing_restart) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data. - update_status = this->update_background_process(false, flags & (unsigned int) UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); - // BBS TODO reload_scene - this->view3D->reload_scene(false, flags & (unsigned int) UpdateParams::FORCE_FULL_SCREEN_REFRESH); - if (is_preview_shown()) - this->preview->reload_print(); - // BBS assemble view + update_status = this->update_background_process(false, flags & (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + //BBS TODO reload_scene + this->view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH); + if (is_preview_shown()) this->preview->reload_print(); + //BBS assemble view this->assemble_view->reload_scene(false, flags); // todo: better to mark thumbnail dirty here @@ -7992,23 +7929,25 @@ void Plater::priv::select_view(const std::string& direction) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << "select view3D"; view3D->select_view(direction); wxGetApp().update_ui_from_settings(); - } else if (current_panel == preview) { + } + else if (current_panel == preview) { BOOST_LOG_TRIVIAL(info) << "select preview"; preview->select_view(direction); wxGetApp().update_ui_from_settings(); - } else if (current_panel == assemble_view) { + } + else if (current_panel == assemble_view) { BOOST_LOG_TRIVIAL(info) << "select assemble view"; assemble_view->select_view(direction); } } -const VendorProfile::PrinterModel* Plater::get_curr_printer_model() +const VendorProfile::PrinterModel *Plater::get_curr_printer_model() { auto bundle = wxGetApp().preset_bundle; if (bundle) { - const Preset* curr = &bundle->printers.get_selected_preset(); + const Preset *curr = &bundle->printers.get_selected_preset(); if (curr) { - const VendorProfile::PrinterModel* pm = PresetUtils::system_printer_model(*curr); + const VendorProfile::PrinterModel *pm = PresetUtils::system_printer_model(*curr); if (!pm) { auto curr_parent = bundle->printers.get_selected_preset_parent(); if (curr_parent) { @@ -8048,9 +7987,9 @@ std::map Plater::get_bed_texture_maps() bool Plater::get_enable_wrapping_detection() { - const DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const ConfigOptionBool* wrapping_detection = print_config.option("enable_wrapping_detection"); - return (wrapping_detection != nullptr) && wrapping_detection->value; + const DynamicPrintConfig & print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const ConfigOptionBool * wrapping_detection = print_config.option("enable_wrapping_detection"); + return (wrapping_detection != nullptr) && wrapping_detection->value; } wxColour Plater::get_next_color_for_filament() @@ -8059,9 +7998,22 @@ wxColour Plater::get_next_color_for_filament() // refs to https://www.ebaomonthly.com/window/photo/lesson/colorList.htm wxColour colors[FILAMENT_SYSTEM_COLORS_NUM] = { // ORCA updated all color palette - wxColour("#00C1AE"), wxColour("#F4E2C1"), wxColour("#ED1C24"), wxColour("#00FF7F"), wxColour("#F26722"), wxColour("#FFEB31"), - wxColour("#7841CE"), wxColour("#115877"), wxColour("#ED1E79"), wxColour("#2EBDEF"), wxColour("#345B2F"), wxColour("#800080"), - wxColour("#FA8173"), wxColour("#800000"), wxColour("#F7B763"), wxColour("#A4C41E"), + wxColour("#00C1AE"), + wxColour("#F4E2C1"), + wxColour("#ED1C24"), + wxColour("#00FF7F"), + wxColour("#F26722"), + wxColour("#FFEB31"), + wxColour("#7841CE"), + wxColour("#115877"), + wxColour("#ED1E79"), + wxColour("#2EBDEF"), + wxColour("#345B2F"), + wxColour("#800080"), + wxColour("#FA8173"), + wxColour("#800000"), + wxColour("#F7B763"), + wxColour("#A4C41E"), }; return colors[curr_color_filamenet++ % FILAMENT_SYSTEM_COLORS_NUM]; } @@ -8069,25 +8021,22 @@ wxColour Plater::get_next_color_for_filament() wxString Plater::get_slice_warning_string(GCodeProcessorResult::SliceWarning& warning) { if (warning.msg == BED_TEMP_TOO_HIGH_THAN_FILAMENT) { - return _L("The current heatbed temperature is relatively high. The nozzle may clog when printing this filament in a closed " - "environment. Please open the front door and/or remove the upper glass."); + return _L("The current heatbed temperature is relatively high. The nozzle may clog when printing this filament in a closed environment. Please open the front door and/or remove the upper glass."); } else if (warning.msg == NOZZLE_HRC_CHECKER) { - return _L("The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace " - "the hardened nozzle or filament, otherwise, the nozzle will be worn down or damaged."); + return _L("The nozzle hardness required by the filament is higher than the default nozzle hardness of the printer. Please replace the hardened nozzle or filament, otherwise, the nozzle will be worn down or damaged."); } else if (warning.msg == NOT_SUPPORT_TRADITIONAL_TIMELAPSE) { - return _L( - "Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode."); + return _L("Enabling traditional timelapse photography may cause surface imperfections. It is recommended to change to smooth mode."); } else if (warning.msg == NOT_GENERATE_TIMELAPSE) { return wxString(); } else if (warning.msg == SMOOTH_TIMELAPSE_WITHOUT_PRIME_TOWER) { - return _L("Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the " - "prime tower, re-slice and print again."); - } else { + return _L("Smooth mode for timelapse is enabled, but the prime tower is off, which may cause print defects. Please enable the prime tower, re-slice and print again."); + } + else { return wxString(warning.msg); } } -void Plater::priv::apply_free_camera_correction(bool apply /* = true*/) +void Plater::priv::apply_free_camera_correction(bool apply/* = true*/) { bool use_perspective_camera = get_config("use_perspective_camera").compare("true") == 0; if (use_perspective_camera) @@ -8098,7 +8047,7 @@ void Plater::priv::apply_free_camera_correction(bool apply /* = true*/) camera.recover_from_free_camera(); } -// BBS: add no slice option +//BBS: add no slice option void Plater::priv::select_view_3D(const std::string& name, bool no_slice) { if (name == "3D") { @@ -8107,23 +8056,25 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("goto preview page when loading gcode/exported_3mf"); } set_current_panel(view3D, no_slice); - } else if (name == "Preview") { + } + else if (name == "Preview") { BOOST_LOG_TRIVIAL(info) << "select preview"; - // BBS update extruder params and speed table before slicing + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); set_current_panel(preview, no_slice); - } else if (name == "Assemble") { + } + else if (name == "Assemble") { BOOST_LOG_TRIVIAL(info) << "select assemble view"; set_current_panel(assemble_view, no_slice); } - // BBS update selection + //BBS update selection wxGetApp().obj_list()->update_selections(); selection_changed(); @@ -8132,12 +8083,13 @@ void Plater::priv::select_view_3D(const std::string& name, bool no_slice) void Plater::priv::select_next_view_3D() { + if (current_panel == view3D) wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); else if (current_panel == preview) wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - // else if (current_panel == assemble_view) - // set_current_panel(view3D); +// else if (current_panel == assemble_view) +// set_current_panel(view3D); } void Plater::priv::enable_sidebar(bool enabled) @@ -8157,7 +8109,9 @@ void Plater::priv::collapse_sidebar(bool collapse) sidebar_layout.is_collapsed = collapse; // Now update the tooltip in the toolbar. - std::string new_tooltip = collapse ? _u8L("Expand sidebar") : _u8L("Collapse sidebar"); + std::string new_tooltip = collapse + ? _u8L("Expand sidebar") + : _u8L("Collapse sidebar"); new_tooltip += " [" + _u8L("Shift+") + _u8L("Tab") + "]"; int id = collapse_toolbar.get_item_id("collapse_sidebar"); collapse_toolbar.set_tooltip(id, new_tooltip); @@ -8165,13 +8119,12 @@ void Plater::priv::collapse_sidebar(bool collapse) update_sidebar(); } -void Plater::priv::update_sidebar(bool force_update) -{ +void Plater::priv::update_sidebar(bool force_update) { auto& sidebar = m_aui_mgr.GetPane(this->sidebar); if (!sidebar.IsOk() || this->current_panel == nullptr) { return; } - bool needs_update = force_update; + bool needs_update = force_update; if (!sidebar_layout.is_enabled) { if (sidebar.IsShown()) { @@ -8201,21 +8154,23 @@ void Plater::priv::reset_window_layout() update_sidebar(true); } -Sidebar::DockingState Plater::priv::get_sidebar_docking_state() -{ +Sidebar::DockingState Plater::priv::get_sidebar_docking_state() { if (!sidebar_layout.is_enabled) { return Sidebar::None; } const auto& sidebar = m_aui_mgr.GetPane(this->sidebar); - if (sidebar.IsFloating()) { + if(sidebar.IsFloating()) { return Sidebar::None; } return sidebar.dock_direction == wxAUI_DOCK_RIGHT ? Sidebar::Right : Sidebar::Left; } -void Plater::priv::reset_all_gizmos() { view3D->get_canvas3d()->reset_all_gizmos(); } +void Plater::priv::reset_all_gizmos() +{ + view3D->get_canvas3d()->reset_all_gizmos(); +} // Called after the Preferences dialog is closed and the program settings are saved. // Update the UI based on the current preferences. @@ -8230,9 +8185,15 @@ void Plater::priv::update_ui_from_settings() } // BBS -std::shared_ptr Plater::priv::statusbar() { return nullptr; } +std::shared_ptr Plater::priv::statusbar() +{ + return nullptr; +} -std::string Plater::priv::get_config(const std::string& key) const { return wxGetApp().app_config->get(key); } +std::string Plater::priv::get_config(const std::string &key) const +{ + return wxGetApp().app_config->get(key); +} BoundingBoxf Plater::priv::bed_shape_bb() const { @@ -8242,14 +8203,14 @@ BoundingBoxf Plater::priv::bed_shape_bb() const BoundingBox Plater::priv::scaled_bed_shape_bb() const { - const auto* bed_shape_opt = config->opt("printable_area"); + const auto *bed_shape_opt = config->opt("printable_area"); const auto printable_area = Slic3r::Polygon::new_scale(bed_shape_opt->values); return printable_area.bounding_box(); } -void read_binary_stl(const std::string& filename, std::string& model_id, std::string& code) -{ - std::ifstream file(encode_path(filename.c_str()), std::ios::binary); + +void read_binary_stl(const std::string& filename, std::string& model_id, std::string& code) { + std::ifstream file( encode_path(filename.c_str()), std::ios::binary); if (!file) { return; } @@ -8268,7 +8229,7 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st return; } - char magic[2] = {data[0], data[1]}; + char magic[2] = { data[0], data[1] }; if (magic[0] != 'M' || magic[1] != 'W') { file.close(); return; @@ -8279,9 +8240,9 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st return; } - char protocol_version[3] = {data[3], data[4], data[5]}; + char protocol_version[3] = { data[3], data[4], data[5] }; - // version + //version if (protocol_version[0] != '1' || protocol_version[1] != '.' || protocol_version[2] != '0') { file.close(); return; @@ -8296,16 +8257,18 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st tokens.push_back(tokenPtr); } - // model id + //model id if (tokens.size() < 4) { file.close(); return; } model_id = tokens[2]; - code = tokens[3]; + code = tokens[3]; file.close(); - } catch (...) {} + } + catch (...) { + } return; } @@ -8316,25 +8279,26 @@ std::vector Plater::priv::load_files(const std::vector& input_ bool* published_out) { std::vector empty_result; - bool dlg_cont = true; + bool dlg_cont = true; bool is_user_cancel = false; - bool translate_old = false; + bool translate_old = false; int current_width = 0, current_depth = 0, current_height = 0, project_filament_count = 1; if (input_files.empty()) return std::vector(); if (!input_files.empty()) - q->m_3mf_path = input_files[0].string(); - + q->m_3mf_path = input_files[0].string(); + // SoftFever: ugly fix so we can exist pa calib mode background_process.fff_print()->calib_mode() = CalibMode::Calib_None; + // BBS int filaments_cnt = config->opt("filament_colour")->values.size(); - bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA /* || filaments_cnt <= 1*/; - if (!one_by_one) { - for (const auto& path : input_files) { + bool one_by_one = input_files.size() == 1 || printer_technology == ptSLA/* || filaments_cnt <= 1*/; + if (! one_by_one) { + for (const auto &path : input_files) { if (std::regex_match(path.string(), pattern_bundle)) { one_by_one = true; break; @@ -8342,74 +8306,75 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - bool load_model = strategy & LoadStrategy::LoadModel; - bool load_config = strategy & LoadStrategy::LoadConfig; + bool load_model = strategy & LoadStrategy::LoadModel; + bool load_config = strategy & LoadStrategy::LoadConfig; bool imperial_units = strategy & LoadStrategy::ImperialUnits; - bool silence = strategy & LoadStrategy::Silence; + bool silence = strategy & LoadStrategy::Silence; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": load_model %1%, load_config %2%, input_files size %3%") % load_model % load_config % - input_files.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": load_model %1%, load_config %2%, input_files size %3%")%load_model %load_config %input_files.size(); const auto loading = _L("Loading") + dots; ProgressDialog dlg(loading, "", 100, find_toplevel_parent(q), wxPD_AUTO_HIDE | wxPD_CAN_ABORT | wxPD_APP_MODAL); wxBusyCursor busy; - auto* new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); + auto *new_model = (!load_model || one_by_one) ? nullptr : new Slic3r::Model(); std::vector obj_idxs; - std::string designer_model_id; - std::string designer_country_code; + std::string designer_model_id; + std::string designer_country_code; - int answer_convert_from_meters = wxOK_DEFAULT; - int answer_convert_from_imperial_units = wxOK_DEFAULT; - int tolal_model_count = 0; + int answer_convert_from_meters = wxOK_DEFAULT; + int answer_convert_from_imperial_units = wxOK_DEFAULT; + int tolal_model_count = 0; - int progress_percent = 0; - int total_files = input_files.size(); - const int stage_percent[IMPORT_STAGE_MAX + 1] = {5, // IMPORT_STAGE_RESTORE - 10, // IMPORT_STAGE_OPEN - 30, // IMPORT_STAGE_READ_FILES - 50, // IMPORT_STAGE_EXTRACT - 60, // IMPORT_STAGE_LOADING_OBJECTS - 70, // IMPORT_STAGE_LOADING_PLATES - 80, // IMPORT_STAGE_FINISH - 85, // IMPORT_STAGE_ADD_INSTANCE - 90, // IMPORT_STAGE_UPDATE_GCODE - 92, // IMPORT_STAGE_CHECK_MODE_GCODE - 95, // UPDATE_GCODE_RESULT - 98, // IMPORT_LOAD_CONFIG - 99, // IMPORT_LOAD_MODEL_OBJECTS - 100}; - const int step_percent[LOAD_STEP_STAGE_NUM + 1] = {5, // LOAD_STEP_STAGE_READ_FILE - 30, // LOAD_STEP_STAGE_GET_SOLID - 60, // LOAD_STEP_STAGE_GET_MESH - 100}; + int progress_percent = 0; + int total_files = input_files.size(); + const int stage_percent[IMPORT_STAGE_MAX+1] = { + 5, // IMPORT_STAGE_RESTORE + 10, // IMPORT_STAGE_OPEN + 30, // IMPORT_STAGE_READ_FILES + 50, // IMPORT_STAGE_EXTRACT + 60, // IMPORT_STAGE_LOADING_OBJECTS + 70, // IMPORT_STAGE_LOADING_PLATES + 80, // IMPORT_STAGE_FINISH + 85, // IMPORT_STAGE_ADD_INSTANCE + 90, // IMPORT_STAGE_UPDATE_GCODE + 92, // IMPORT_STAGE_CHECK_MODE_GCODE + 95, // UPDATE_GCODE_RESULT + 98, // IMPORT_LOAD_CONFIG + 99, // IMPORT_LOAD_MODEL_OBJECTS + 100 + }; + const int step_percent[LOAD_STEP_STAGE_NUM+1] = { + 5, // LOAD_STEP_STAGE_READ_FILE + 30, // LOAD_STEP_STAGE_GET_SOLID + 60, // LOAD_STEP_STAGE_GET_MESH + 100 + }; - const float INPUT_FILES_RATIO = 0.7; - const float INIT_MODEL_RATIO = 0.75; - const float CENTER_AROUND_ORIGIN_RATIO = 0.8; - const float LOAD_MODEL_RATIO = 0.9; + const float INPUT_FILES_RATIO = 0.7; + const float INIT_MODEL_RATIO = 0.75; + const float CENTER_AROUND_ORIGIN_RATIO = 0.8; + + const float LOAD_MODEL_RATIO = 0.9; for (size_t i = 0; i < input_files.size(); ++i) { #ifdef _WIN32 auto path = input_files[i]; - // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it - // assignes full path to names of loaded objects. + // On Windows, we swap slashes to back slashes, see GH #6803 as read_from_file() does not understand slashes on Windows thus it assignes full path to names of loaded objects. path.make_preferred(); #else // _WIN32 // Don't make a copy on Posix. Slash is a path separator, back slashes are not accepted as a substitute. - const auto& path = input_files[i]; + const auto &path = input_files[i]; #endif // _WIN32 - const auto filename = path.filename(); - int progress_percent = static_cast(100.0f * static_cast(i) / static_cast(input_files.size())); - const auto real_filename = (strategy & LoadStrategy::Restore) ? input_files[++i].filename() : filename; - const auto dlg_info = _L("Loading file") + ": " + from_path(real_filename); + const auto filename = path.filename(); + int progress_percent = static_cast(100.0f * static_cast(i) / static_cast(input_files.size())); + const auto real_filename = (strategy & LoadStrategy::Restore) ? input_files[++i].filename() : filename; + const auto dlg_info = _L("Loading file") + ": " + from_path(real_filename); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": load file %1%") % filename; dlg_cont = dlg.Update(progress_percent, dlg_info); - if (!dlg_cont) - return empty_result; + if (!dlg_cont) return empty_result; const bool type_3mf = std::regex_match(path.string(), pattern_3mf); // const bool type_zip_amf = !type_3mf && std::regex_match(path.string(), pattern_zip_amf); @@ -8423,15 +8388,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ load_aux = true; strategy = strategy | LoadStrategy::LoadAuxiliary; } - if (load_config) - strategy = strategy | LoadStrategy::CheckVersion; + if (load_config) strategy = strategy | LoadStrategy::CheckVersion; bool is_project_file = false; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": is_project_file %1%, type_3mf %2%") % is_project_file % type_3mf; try { if (type_3mf) { DynamicPrintConfig config; - Semver file_version; - En3mfType en_3mf_file_type = En3mfType::From_BBS; + Semver file_version; + En3mfType en_3mf_file_type = En3mfType::From_BBS; // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; // on load keep the user's current presets and overlay only those keys. Declared // here (outside the config block below) so it stays alive for the embedded-preset @@ -8441,9 +8405,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicPrintConfig config_loaded; // BBS: add part plate related logic - PlateDataPtrs plate_data; + PlateDataPtrs plate_data; ConfigSubstitutionContext config_substitutions{ForwardCompatibilitySubstitutionRule::Enable}; - std::vector project_presets; + std::vector project_presets; // BBS: backup & restore q->skip_thumbnail_invalid = true; model = Slic3r::Model::read_from_archive(path.string(), &config_loaded, &config_substitutions, en_3mf_file_type, strategy, &plate_data, &project_presets, @@ -8460,12 +8424,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (cancel) is_user_cancel = cancel; }); - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ":" << __LINE__ - << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % - plate_data.size() % project_presets.size() % - (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % - file_version.to_string(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() % + project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string(); // BBS: a "published" 3MF carries a flag plus the author-selected setting keys; // on load keep the user's current presets and overlay only those keys. Parsed @@ -8572,24 +8533,20 @@ std::vector Plater::priv::load_files(const std::vector& input_ // 2. add extruder for BBS or Other model if only import geometry if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) { std::set extruderIds; - for (ModelObject* o : model.objects) { - if (o->config.option("extruder")) - extruderIds.insert(o->config.extruder()); + for (ModelObject *o : model.objects) { + if (o->config.option("extruder")) extruderIds.insert(o->config.extruder()); for (auto volume : o->volumes) { - if (volume->config.option("extruder")) - extruderIds.insert(volume->config.extruder()); - for (int extruder : volume->get_extruders()) { - extruderIds.insert(extruder); - } + if (volume->config.option("extruder")) extruderIds.insert(volume->config.extruder()); + for (int extruder : volume->get_extruders()) { extruderIds.insert(extruder); } } } int size = extruderIds.size() == 0 ? 0 : *(extruderIds.rbegin()); int filament_size = sidebar->combos_filament().size(); while (filament_size < MAXIMUM_EXTRUDER_NUMBER && filament_size < size) { - int filament_count = filament_size + 1; - wxColour new_col = Plater::get_next_color_for_filament(); - std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + int filament_count = filament_size + 1; + wxColour new_col = Plater::get_next_color_for_filament(); + std::string new_color = new_col.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); wxGetApp().preset_bundle->set_num_filaments(filament_count, new_color); wxGetApp().plater()->on_filament_count_change(filament_count); ++filament_size; @@ -8602,22 +8559,21 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (import_project_action.empty()) load_type = LoadType::Unknown; else - load_type = static_cast(std::stoi(import_project_action)); + load_type = static_cast(std::stoi(import_project_action)); // BBS: version check - Semver app_version = *(Semver::parse(SoftFever_VERSION)); - const wxString load_3mf_title = _L("Load 3MF"); - const wxString newer_3mf_title = _L("Newer 3MF version"); - const wxString bambu_project_title = _L("BambuStudio Project"); - const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); - const wxString msg_old_orca_geometry = _L( - "The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); - const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); - const wxString msg_bambu_geometry = _L("The 3MF file was generated by BambuStudio, loading geometry data only."); - auto log_and_show_3mf_info = [&](const wxString& text, const wxString& title) { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ":" << __LINE__ << " " - << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); + Semver app_version = *(Semver::parse(SoftFever_VERSION)); + const wxString load_3mf_title = _L("Load 3MF"); + const wxString newer_3mf_title = _L("Newer 3MF version"); + const wxString bambu_project_title = _L("BambuStudio Project"); + const wxString msg_unsupported_geometry = _L("The 3MF is not supported by OrcaSlicer, loading geometry data only."); + const wxString msg_old_orca_geometry = _L("The 3MF file was generated by an old OrcaSlicer version, loading geometry data only."); + const wxString msg_older_geometry = _L("The 3MF file was generated by an older version, loading geometry data only."); + const wxString msg_bambu_geometry = _L("The 3MF file was generated by BambuStudio, loading geometry data only."); + auto log_and_show_3mf_info = [&](const wxString& text, const wxString& title) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << " " + << boost::format("3MF import message [%1%]: %2% | file: %3%") % into_u8(title) % into_u8(text) % path.string(); show_info(q, text, title); }; if (en_3mf_file_type == En3mfType::From_Prusa) { @@ -8625,7 +8581,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ load_config = false; if (load_type != LoadType::LoadGeometry) log_and_show_3mf_info(msg_unsupported_geometry, load_3mf_title); - } else if (en_3mf_file_type == En3mfType::From_Orca) { + } + else if (en_3mf_file_type == En3mfType::From_Orca) { // OrcaSlicer file (has OrcaSlicer tag) - compare file_version with SoftFever_VERSION // Migration fix for OrcaSlicer 2.3.1-alpha sparse infill rotation template if (load_config && (file_version < app_version) && file_version == Semver("2.3.1-alpha")) { @@ -8640,8 +8597,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " "infill rotation template settings that may not work properly with your current infill pattern. " "This could result in weak support or print quality issues.")); - msg_text += "\n\n" + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation " - "template settings?")); + msg_text += "\n\n" + + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); dialog.SetButtonLabel(wxID_YES, _L("Yes")); dialog.SetButtonLabel(wxID_NO, _L("No")); @@ -8652,30 +8609,31 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else if (load_config && (file_version > app_version)) { if (config_substitutions.unrecogized_keys.size() > 0) { - wxString text = wxString::Format( - _L("The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:"), - file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, found the following unrecognized keys:"), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); text += "\n"; wxString context = text; - wxString append = _L("You should update your software.\n"); + wxString append = _L("You should update your software.\n"); context += "\n\n"; context += append; log_and_show_3mf_info(context, newer_3mf_title); - } else { - // if the minor version is not matched + } + else { + //if the minor version is not matched if (file_version.min() != app_version.min()) { - wxString text = wxString::Format( - _L("The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software."), - file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); + wxString text = wxString::Format(_L("The 3MF file version %s is newer than %s's version %s, we suggest to upgrade your software."), + file_version.to_string_sf(), std::string(SLIC3R_APP_FULL_NAME), app_version.to_string_sf()); text += "\n"; log_and_show_3mf_info(text, newer_3mf_title); } } - } else if (load_config && config_loaded.empty()) { + } + else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); } - } else if (en_3mf_file_type == En3mfType::From_BBS) { + } + else if (en_3mf_file_type == En3mfType::From_BBS) { // No OrcaSlicer tag - check Bambu/Application version Semver orca_tag_start_version(2, 3, 2); if (file_version <= orca_tag_start_version) { @@ -8690,12 +8648,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ _sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag || _sparse_infill_pattern == ipLockedZag; if (!is_safe_to_rotate) { - wxString msg_text = _(L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " - "infill rotation template settings that may not work properly with your " - "current infill pattern. " - "This could result in weak support or print quality issues.")); - msg_text += "\n\n" + _(L("Would you like OrcaSlicer to automatically fix this by clearing the " - "rotation template settings?")); + wxString msg_text = _( + L("This project was created with an OrcaSlicer 2.3.1-alpha and uses " + "infill rotation template settings that may not work properly with your current infill pattern. " + "This could result in weak support or print quality issues.")); + msg_text += "\n\n" + + _(L("Would you like OrcaSlicer to automatically fix this by clearing the rotation template settings?")); MessageDialog dialog(wxGetApp().plater(), msg_text, "", wxICON_WARNING | wxYES | wxNO); dialog.SetButtonLabel(wxID_YES, _L("Yes")); dialog.SetButtonLabel(wxID_NO, _L("No")); @@ -8704,7 +8662,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } } - } else if (load_config && config_loaded.empty()) { + } + else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_older_geometry, load_3mf_title); } @@ -8715,23 +8674,21 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_bambu_geometry, load_3mf_title); - } else if (load_config && (file_version > slic3r_version)) { + } + else if (load_config && (file_version > slic3r_version)) { // BambuStudio file version is newer than our compatible SLIC3R_VERSION if (config_substitutions.unrecogized_keys.size() > 0) { - wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer " - "than the compatible version %s. Found unrecognized settings:"), + wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Found unrecognized settings:"), file_version.to_string(), slic3r_version.to_string()); text += "\n"; wxString context = text; - wxString append = _L("You should update your software.\n"); + wxString append = _L("You should update your software.\n"); context += "\n\n"; context += append; log_and_show_3mf_info(context, bambu_project_title); } else { - wxString text = - wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the " - "compatible version %s. Some settings may not be fully compatible."), - file_version.to_string(), slic3r_version.to_string()); + wxString text = wxString::Format(_L("The 3MF was created by BambuStudio (version %s), which is newer than the compatible version %s. Some settings may not be fully compatible."), + file_version.to_string(), slic3r_version.to_string()); text += "\n"; log_and_show_3mf_info(text, bambu_project_title); } @@ -8741,46 +8698,41 @@ std::vector Plater::priv::load_files(const std::vector& input_ log_and_show_3mf_info(text, bambu_project_title); } } - } else if (en_3mf_file_type == En3mfType::From_Other) { + } + else if (en_3mf_file_type == En3mfType::From_Other) { // Generic CAD/other 3MF without slicer metadata: import geometry silently. if (load_config && config_loaded.empty()) { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ":" << __LINE__ << " " - << boost::format("3MF has no slicer metadata/project config, importing geometry only: %1%") % path.string(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ + << " " + << boost::format("3MF has no slicer metadata/project config, importing geometry only: %1%") % path.string(); load_config = false; } - } else if (load_config && config_loaded.empty()) { + } + else if (load_config && config_loaded.empty()) { load_config = false; log_and_show_3mf_info(msg_old_orca_geometry, load_3mf_title); - } else if (!load_config) { + } + else if (!load_config) { // reset config except color - for (ModelObject* model_object : model.objects) { + for (ModelObject *model_object : model.objects) { bool has_extruder = model_object->config.has("extruder"); - int extruder_id = -1; + int extruder_id = -1; // save the extruder information before reset - if (has_extruder) { - extruder_id = model_object->config.extruder(); - } + if (has_extruder) { extruder_id = model_object->config.extruder(); } model_object->config.reset(); // restore the extruder after reset - if (has_extruder) { - model_object->config.set("extruder", extruder_id); - } + if (has_extruder) { model_object->config.set("extruder", extruder_id); } // Is there any modifier or advanced config data? - for (ModelVolume* model_volume : model_object->volumes) { + for (ModelVolume *model_volume : model_object->volumes) { has_extruder = model_volume->config.has("extruder"); - if (has_extruder) { - extruder_id = model_volume->config.extruder(); - } + if (has_extruder) { extruder_id = model_volume->config.extruder(); } model_volume->config.reset(); - if (has_extruder) { - model_volume->config.set("extruder", extruder_id); - } + if (has_extruder) { model_volume->config.set("extruder", extruder_id); } } } } @@ -8800,17 +8752,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Semver old_version(1, 5, 9); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && - (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { translate_old = true; partplate_list.get_plate_size(current_width, current_depth, current_height); } if (load_config) { if (translate_old) { - // set the size back - partplate_list.reset_size(current_width + Bed3D::Axes::DefaultTipRadius, - current_depth + Bed3D::Axes::DefaultTipRadius, current_height, false); + //set the size back + partplate_list.reset_size(current_width + Bed3D::Axes::DefaultTipRadius, current_depth + Bed3D::Axes::DefaultTipRadius, current_height, false); } project_filament_count = config_loaded.option("filament_colour")->size(); partplate_list.load_from_3mf_structure(plate_data, project_filament_count); @@ -8830,16 +8780,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ if ((project_presets.size() > 0) && load_config && !published_config.published) { // load project embedded presets PresetsConfigSubstitutions preset_substitutions; - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, - ForwardCompatibilitySubstitutionRule::Enable); - if (!preset_substitutions.empty()) - show_substitutions_info(preset_substitutions); + PresetBundle & preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); } if (project_presets.size() > 0) { - for (unsigned int i = 0; i < project_presets.size(); i++) { - delete project_presets[i]; - } + for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } project_presets.clear(); } @@ -8855,37 +8801,35 @@ std::vector Plater::priv::load_files(const std::vector& input_ // Based on the printer technology field found in the loaded config, select the base for the config, PrinterTechnology printer_technology = Preset::printer_technology(config_loaded); - config.apply(static_cast(FullPrintConfig::defaults())); + config.apply(static_cast(FullPrintConfig::defaults())); // and place the loaded config over the base. config += std::move(config_loaded); std::map validity = config.validate(); if (!validity.empty()) { - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("Param values in 3mf error: "); - for (std::map::iterator it = validity.begin(); it != validity.end(); ++it) - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("%1%: %2%") % it->first % it->second; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("Param values in 3mf error: "); + for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << " " << boost::format("%1%: %2%")%it->first %it->second; // - NotificationManager* notify_manager = q->get_notification_manager(); - std::string error_message = _u8L("Invalid values found in the 3MF:"); + NotificationManager *notify_manager = q->get_notification_manager(); + std::string error_message = _u8L("Invalid values found in the 3MF:"); error_message += "\n"; - for (std::map::iterator it = validity.begin(); it != validity.end(); ++it) + for (std::map::iterator it=validity.begin(); it!=validity.end(); ++it) error_message += "-" + it->first + ": " + it->second + "\n"; error_message += "\n"; error_message += _u8L("Please correct them in the Param tabs"); notify_manager->bbl_show_3mf_warn_notification(error_message); } } - if (!config_substitutions.empty()) - show_substitutions_info(config_substitutions.substitutions, filename.string()); + if (!config_substitutions.empty()) show_substitutions_info(config_substitutions.substitutions, filename.string()); // BBS if (load_model && !load_config) { ; - } else { + } + else { this->model.plates_custom_gcodes = model.plates_custom_gcodes; - this->model.design_info = model.design_info; - this->model.model_info = model.model_info; + this->model.design_info = model.design_info; + this->model.model_info = model.model_info; } } @@ -8905,60 +8849,52 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_config) { if (!config.empty()) { Preset::normalize(config); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; { // BBS: modify the prime tower params for old version file Semver old_version3(2, 0, 0); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && - file_version < old_version3) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) { double old_filament_prime_volume = 0.; - int filament_count = 0; + int filament_count = 0; { - ConfigOptionFloats* filament_prime_volume_option = config.option( - "filament_prime_volume"); - ConfigOptionStrings* filament_colors_option = config.option("filament_colour", - true); - filament_count = filament_colors_option->values.size(); + ConfigOptionFloats *filament_prime_volume_option = config.option("filament_prime_volume"); + ConfigOptionStrings *filament_colors_option = config.option("filament_colour", true); + filament_count = filament_colors_option->values.size(); if (filament_prime_volume_option) { - std::vector& filament_prime_volume_values = filament_prime_volume_option->values; + std::vector &filament_prime_volume_values = filament_prime_volume_option->values; if (!filament_prime_volume_values.empty()) { old_filament_prime_volume = filament_prime_volume_values[0]; - if (filament_count > 1) - filament_prime_volume_values.resize(filament_count, old_filament_prime_volume); + if (filament_count > 1) filament_prime_volume_values.resize(filament_count, old_filament_prime_volume); } } } - ConfigOptionEnum* prime_tower_rib_wall_option = - config.option>("wipe_tower_wall_type", true); - prime_tower_rib_wall_option->value = WipeTowerWallType::wtwRectangle; + ConfigOptionEnum *prime_tower_rib_wall_option = config.option>("wipe_tower_wall_type", true); + prime_tower_rib_wall_option->value = WipeTowerWallType::wtwRectangle; - ConfigOptionPercent* prime_tower_infill_gap_option = - config.option("prime_tower_infill_gap", true); - prime_tower_infill_gap_option->value = 100; + ConfigOptionPercent *prime_tower_infill_gap_option = config.option("prime_tower_infill_gap", true); + prime_tower_infill_gap_option->value = 100; - ConfigOptionInts* filament_adhesiveness_category_option = - config.option("filament_adhesiveness_category", true); - std::vector& filament_adhesiveness_category_values = filament_adhesiveness_category_option->values; + ConfigOptionInts *filament_adhesiveness_category_option = config.option("filament_adhesiveness_category", true); + std::vector &filament_adhesiveness_category_values = filament_adhesiveness_category_option->values; filament_adhesiveness_category_values.resize(filament_count); for (int index = 0; index < filament_count; index++) filament_adhesiveness_category_values[index] = 100; - std::vector& diff_settings = - config.option("different_settings_to_system", true)->values; + std::vector &diff_settings = config.option("different_settings_to_system", true)->values; diff_settings.resize(filament_count + 2); std::vector diff_process_keys; - std::string diff_process_settings = diff_settings[0]; + std::string diff_process_settings = diff_settings[0]; Slic3r::unescape_strings_cstyle(diff_process_settings, diff_process_keys); diff_process_keys.emplace_back("wipe_tower_wall_type"); diff_process_keys.emplace_back("prime_tower_infill_gap"); diff_process_settings = Slic3r::escape_strings_cstyle(diff_process_keys); - diff_settings[0] = diff_process_settings; + diff_settings[0] = diff_process_settings; for (int index = 0; index < filament_count; index++) { std::vector diff_filament_keys; - std::string diff_filament_settings = diff_settings[index + 1]; + std::string diff_filament_settings = diff_settings[index + 1]; Slic3r::unescape_strings_cstyle(diff_filament_settings, diff_filament_keys); diff_filament_keys.emplace_back("filament_adhesiveness_category"); diff_filament_settings = Slic3r::escape_strings_cstyle(diff_filament_keys); @@ -8976,51 +8912,38 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (validated == VALIDATE_PRESETS_MODIFIED_GCODES) { std::string warning_message; warning_message += "\n"; - for (std::set::iterator it = modified_gcodes.begin(); it != modified_gcodes.end(); ++it) + for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) warning_message += "-" + *it + "\n"; warning_message += "\n"; - // show_info(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + - // warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the - // machine!"), _L("Modified G-code")); - MessageDialog - dlg(q, - _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + - _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), - _L("Modified G-code")); + //show_info(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); + MessageDialog dlg(q, _L("The 3MF has the following modified G-code in filament or printer presets:") + warning_message + _L("Please confirm that all modified G-code is safe to prevent any damage to the machine!"), _L("Modified G-code")); dlg.show_dsa_button(); - auto res = dlg.ShowModal(); + auto res = dlg.ShowModal(); if (dlg.get_checkbox_state()) wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); - } else if ((validated == VALIDATE_PRESETS_PRINTER_NOT_FOUND) || - (validated == VALIDATE_PRESETS_FILAMENTS_NOT_FOUND)) { + } + else if ((validated == VALIDATE_PRESETS_PRINTER_NOT_FOUND) || (validated == VALIDATE_PRESETS_FILAMENTS_NOT_FOUND)) { std::string warning_message; warning_message += "\n"; - for (std::set::iterator it = modified_gcodes.begin(); it != modified_gcodes.end(); ++it) + for (std::set::iterator it=modified_gcodes.begin(); it!=modified_gcodes.end(); ++it) warning_message += "-" + *it + "\n"; warning_message += "\n"; - // show_info(q, _L("The 3MF has the following customized filament or printer presets:") + warning_message + - // _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), - // _L("Customized Preset")); - MessageDialog dlg(q, - _L("The 3MF has the following customized filament or printer presets:") + - from_u8(warning_message) + - _L("Please confirm that the G-code within these presets is safe to prevent any " - "damage to the machine!"), - _L("Customized Preset")); + //show_info(q, _L("The 3MF has the following customized filament or printer presets:") + warning_message + _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); + MessageDialog dlg(q, _L("The 3MF has the following customized filament or printer presets:") + from_u8(warning_message)+ _L("Please confirm that the G-code within these presets is safe to prevent any damage to the machine!"), _L("Customized Preset")); dlg.show_dsa_button(); - auto res = dlg.ShowModal(); + auto res = dlg.ShowModal(); if (dlg.get_checkbox_state()) wxGetApp().app_config->set("no_warn_when_modified_gcodes", "true"); } } - // always load config + //always load config { // BBS: save the wipe tower pos in file here, will be used later ConfigOptionFloats* wipe_tower_x_opt = config.opt("wipe_tower_x"); ConfigOptionFloats* wipe_tower_y_opt = config.opt("wipe_tower_y"); - std::optional file_wipe_tower_x; - std::optional file_wipe_tower_y; + std::optionalfile_wipe_tower_x; + std::optionalfile_wipe_tower_y; if (wipe_tower_x_opt) file_wipe_tower_x = *wipe_tower_x_opt; if (wipe_tower_y_opt) @@ -9073,7 +8996,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ 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(); + BedType bed_type = (BedType)bed_type_opt->getInt(); // update app config for bed type bool is_bbl_preset = preset_bundle->is_bbl_vendor(); if (is_bbl_preset) { @@ -9121,10 +9044,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ // // show notification about temporarily installed presets // if (!names.empty()) { - // std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active - // instance of PrusaSlicer", - // "The presets below were temporarily installed on the active - // instance of PrusaSlicer", names.size())) + ":"; + // std::string notif_text = into_u8(_L_PLURAL("The preset below was temporarily installed on the active instance of PrusaSlicer", + // "The presets below were temporarily installed on the active instance of PrusaSlicer", + // names.size())) + ":"; // for (std::string& name : names) // notif_text += "\n - " + name; // notification_manager->push_notification(NotificationType::CustomNotification, @@ -9134,11 +9056,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ // BBS // if (printer_technology == ptFFF) - // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, - // &preset_bundle->project_config); + // CustomGCode::update_custom_gcode_per_print_z_from_config(model.custom_gcode_per_print_z, &preset_bundle->project_config); - // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print - // Host upload" BBS: add preset combo box re-active logic currently found only needs re-active here + // For exporting from the amf/3mf we shouldn't check printer_presets for the containing information about "Print Host upload" + // BBS: add preset combo box re-active logic + // currently found only needs re-active here wxGetApp().load_current_presets(false, false); // Update filament colors for the MM-printer profile in the full config // to avoid black (default) colors for Extruders in the ObjectList, @@ -9149,8 +9071,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ DynamicConfig& proj_cfg = preset_bundle->project_config; // do some post process after loading config { - // BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered - ConfigOptionFloats* wipe_tower_x = proj_cfg.opt("wipe_tower_x"); + //BBS: rewrite wipe tower pos stored in 3mf file , the code above should be seriously reconsidered + ConfigOptionFloats* wipe_tower_x = proj_cfg.opt("wipe_tower_x"); ConfigOptionFloats* wipe_tower_y = proj_cfg.opt("wipe_tower_y"); if (file_wipe_tower_x) *wipe_tower_x = *file_wipe_tower_x; @@ -9178,22 +9100,19 @@ std::vector Plater::priv::load_files(const std::vector& input_ } // Sync filament multi colour - ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", - true); + ConfigOptionStrings* filament_multi_color = proj_cfg.opt("filament_multi_colour", true); if (filament_multi_color->size() != filament_count) { filament_multi_color->values.resize(filament_count); } // If there is no multi-color data or color is not match, use single color as default value for (size_t i = 0; i < filament_count; i++) { std::vector colors = Slic3r::split_string(filament_multi_color->values[i], ' '); - if (i >= filament_multi_color->values.size() || colors.empty() || - colors[0] != filament_color->values[i]) { + if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) { filament_multi_color->values[i] = filament_color->values[i]; } } // Sync filament colour type - ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", - true); + ConfigOptionStrings* filament_color_type = proj_cfg.opt("filament_colour_type", true); if (filament_color_type && filament_color_type->size() != filament_count) { filament_color_type->values.resize(filament_count); @@ -9223,33 +9142,29 @@ std::vector Plater::priv::load_files(const std::vector& input_ } // 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( - "nozzle_volume_type")) { + if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")) { const int extruder_count = wxGetApp().preset_bundle->get_printer_extruder_count(); - for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volumes->values.size(); - ++extruder_id) - updateNozzleCountDisplay(wxGetApp().preset_bundle, extruder_id, - NozzleVolumeType(nozzle_volumes->values[extruder_id])); + for (int extruder_id = 0; extruder_id < extruder_count && extruder_id < (int) nozzle_volumes->values.size(); ++extruder_id) + updateNozzleCountDisplay(wxGetApp().preset_bundle, extruder_id, NozzleVolumeType(nozzle_volumes->values[extruder_id])); } } } - if (!silence) - wxGetApp().app_config->update_config_dir(path.parent_path().string()); + if (!silence) wxGetApp().app_config->update_config_dir(path.parent_path().string()); } } else { // BBS: add plate data related logic PlateDataPtrs plate_data; // BBS: project embedded settings - std::vector project_presets; - bool is_xxx; - Semver file_version; + std::vector project_presets; + bool is_xxx; + Semver file_version; //ObjImportColorFn obj_color_fun=nullptr; auto obj_color_fun = [&path](ObjDialogInOut &in_out) { if (!boost::iends_with(path.string(), ".obj")) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } @@ -9295,10 +9210,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ is_split = mesh_dlg.get_split_compound_value(); return 1; } - } else { + }else { linear_value = linear; - angle_value = angle; - is_split = split_compound; + angle_value = angle; + is_split = split_compound; return 1; } is_user_cancel = true; @@ -9312,25 +9227,22 @@ std::vector Plater::priv::load_files(const std::vector& input_ designer_model_id = mode_id; designer_country_code = code; - bool cont = true; - float percent_float = (100.0f * (float) i / (float) total_files) + - INPUT_FILES_RATIO * 100.0f * ((float) current / (float) total) / (float) total_files; - BOOST_LOG_TRIVIAL(trace) - << "load_stl_file: percent(float)=" << percent_float << ", curr = " << current << ", total = " << total; - progress_percent = (int) percent_float; - wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); - cont = dlg.Update(progress_percent, msg); - cancel = !cont; - }, - nullptr, 0, obj_color_fun); + bool cont = true; + float percent_float = (100.0f * (float)i / (float)total_files) + INPUT_FILES_RATIO * 100.0f * ((float)current / (float)total) / (float)total_files; + BOOST_LOG_TRIVIAL(trace) << "load_stl_file: percent(float)=" << percent_float << ", curr = " << current << ", total = " << total; + progress_percent = (int)percent_float; + wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); + cont = dlg.Update(progress_percent, msg); + cancel = !cont; + }, + nullptr, 0, obj_color_fun); } if (designer_model_id.empty() && boost::algorithm::iends_with(path.string(), ".stl")) { read_binary_stl(path.string(), designer_model_id, designer_country_code); } - if (type_any_amf && is_xxx) - imperial_units = true; + if (type_any_amf && is_xxx) imperial_units = true; for (auto obj : model.objects) { if (obj->name.empty()) { @@ -9351,31 +9263,26 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (project_presets.size() > 0) { // load project embedded presets PresetsConfigSubstitutions preset_substitutions; - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, - ForwardCompatibilitySubstitutionRule::Enable); - if (!preset_substitutions.empty()) - show_substitutions_info(preset_substitutions); + PresetBundle & preset_bundle = *wxGetApp().preset_bundle; + preset_substitutions = preset_bundle.load_project_embedded_presets(project_presets, ForwardCompatibilitySubstitutionRule::Enable); + if (!preset_substitutions.empty()) show_substitutions_info(preset_substitutions); - for (unsigned int i = 0; i < project_presets.size(); i++) { - delete project_presets[i]; - } + for (unsigned int i = 0; i < project_presets.size(); i++) { delete project_presets[i]; } project_presets.clear(); } } - } catch (const ConfigurationError& e) { - std::string message = GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), filename.string()) + - "\n\n" + e.what(); + } catch (const ConfigurationError &e) { + std::string message = GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), filename.string()) + "\n\n" + e.what(); GUI::show_error(q, message); continue; - } catch (const std::exception& e) { + } catch (const std::exception &e) { if (!is_user_cancel) GUI::show_error(q, e.what()); continue; } - progress_percent = 100.0f * (float) i / (float) total_files + INIT_MODEL_RATIO * 100.0f / (float) total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float)i / (float)total_files + INIT_MODEL_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; @@ -9383,17 +9290,13 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (load_model) { // The model should now be initialized - auto convert_from_imperial_units = [](Model& model, bool only_small_volumes) { - model.convert_from_imperial_units(only_small_volumes); - }; + auto convert_from_imperial_units = [](Model &model, bool only_small_volumes) { model.convert_from_imperial_units(only_small_volumes); }; // BBS: add load_old_project logic if ((!is_project_file) && (!load_old_project)) { // if (!is_project_file) { if (int deleted_objects = model.removed_objects_with_zero_volume(); deleted_objects > 0) { - MessageDialog(q, _L("Objects with zero volume removed"), _L("The volume of the object is zero"), - wxICON_INFORMATION | wxOK) - .ShowModal(); + MessageDialog(q, _L("Objects with zero volume removed"), _L("The volume of the object is zero"), wxICON_INFORMATION | wxOK).ShowModal(); } if (imperial_units) // Convert even if the object is big. @@ -9404,18 +9307,16 @@ std::vector Plater::priv::load_files(const std::vector& input_ format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\nDo you want to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); - if (answer == wxID_YES) - model.convert_from_meters(true); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) model.convert_from_meters(true); } else if (model.looks_like_imperial_units()) { // BBS do not handle look like in meters MessageDialog dlg(q, format_wxstr(_L("The object from file %s is too small, and may be in meters or inches.\nDo you want to scale to millimeters\?"), from_path(filename)), _L("Object too small"), wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); - if (answer == wxID_YES) - convert_from_imperial_units(model, true); + int answer = dlg.ShowModal(); + if (answer == wxID_YES) convert_from_imperial_units(model, true); } // else if (model.looks_like_imperial_units()) { // BBS do not handle look like in imperial @@ -9429,8 +9330,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ // "The dimensions of the object from file %s seem to be defined in inches.\n" // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of the object?", // "The dimensions of some objects from file %s seem to be defined in inches.\n" - // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", - // model.objects.size()), from_path(filename)) + // "The internal unit of PrusaSlicer is a millimeter. Do you want to recalculate the dimensions of these objects?", model.objects.size()), from_path(filename)) // + "\n", _L("The object is too small"), wxICON_QUESTION | wxYES_NO); // dlg.ShowCheckBox(_L("Apply to all the remaining small objects being loaded.")); // int answer = dlg.ShowModal(); @@ -9442,12 +9342,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ // convert_model_if(model, answer_convert_from_imperial_units == wxID_YES); } - if (!is_project_file && model.looks_like_multipart_object()) { - MessageDialog msg_dlg(q, - _L("This file contains several objects positioned at multiple heights.\nInstead of considering them " - "as multiple objects, should \nthe file be loaded as a single object with multiple parts\?") + - "\n", - _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); + if (!is_project_file && model.looks_like_multipart_object()) { + MessageDialog msg_dlg(q, _L("This file contains several objects positioned at multiple heights.\nInstead of considering them as multiple objects, should \nthe file be loaded as a single object with multiple parts\?") + "\n", + _L("Multi-part object detected"), wxICON_WARNING | wxYES | wxNO); if (msg_dlg.ShowModal() == wxID_YES) { model.convert_multipart_object(filaments_cnt); } @@ -9464,15 +9361,15 @@ std::vector Plater::priv::load_files(const std::vector& input_ // return obj_idxs; //} - progress_percent = 100.0f * (float) i / (float) total_files + CENTER_AROUND_ORIGIN_RATIO * 100.0f / (float) total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float)i / (float)total_files + CENTER_AROUND_ORIGIN_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; } int model_idx = 0; - for (ModelObject* model_object : model.objects) { + for (ModelObject *model_object : model.objects) { if (!type_3mf && !type_any_amf) model_object->center_around_origin(false); @@ -9492,8 +9389,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ tolal_model_count += model_idx; - progress_percent = 100.0f * (float) i / (float) total_files + LOAD_MODEL_RATIO * 100.0f / (float) total_files; - dlg_cont = dlg.Update(progress_percent); + progress_percent = 100.0f * (float)i / (float)total_files + LOAD_MODEL_RATIO * 100.0f / (float)total_files; + dlg_cont = dlg.Update(progress_percent); if (!dlg_cont) { q->skip_thumbnail_invalid = false; return empty_result; @@ -9518,7 +9415,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ auto cancel_cb = [&dlg, &dlg_cont]() { return !dlg_cont || dlg.WasCancelled(); }; auto progress_cb = [&dlg, &dlg_cont, &progress_percent](int percent) { progress_percent = std::clamp(percent, 0, 100); - dlg_cont = dlg.Update(progress_percent, _L("Matching textures to filaments")); + dlg_cont = dlg.Update(progress_percent, _L("Matching textures to filaments")); return dlg_cont; }; if (!run_textured_mesh_import_dialog(model, texture_import_result, cancel_cb, progress_cb)) { @@ -9526,7 +9423,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ return empty_result; } if (texture_import_result.fallback_to_geometry_only && !texture_import_result.fallback_warning.empty()) { - MessageDialog(q, texture_import_result.fallback_warning, _L("Texture Import Warning"), wxOK | wxICON_WARNING).ShowModal(); + MessageDialog(q, texture_import_result.fallback_warning, + _L("Texture Import Warning"), + wxOK | wxICON_WARNING).ShowModal(); } if (!texture_import_result.painted.face_colors.empty()) { std::vector texture_object_idxs(model.objects.size()); @@ -9535,12 +9434,12 @@ std::vector Plater::priv::load_files(const std::vector& input_ dlg.Update(std::clamp(percent, 0, 100), msg); return true; }; - apply_textured_mesh_import_result(model, texture_object_idxs, texture_import_result, apply_progress_cb, false); + apply_textured_mesh_import_result(model, texture_object_idxs, texture_import_result, + apply_progress_cb, false); } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ - << boost::format(", before load_model_objects, count %1%") % model.objects.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", before load_model_objects, count %1%")%model.objects.size(); auto loaded_idxs = load_model_objects(model.objects, is_project_file); obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end()); @@ -9553,10 +9452,10 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } else { // This must be an .stl or .obj file, which may contain a maximum of one volume. - for (const ModelObject* model_object : model.objects) { + for (const ModelObject *model_object : model.objects) { new_model->add_object(*model_object); - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":" << __LINE__ << boost::format(", added object %1%") % model_object->name; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":" << __LINE__ << boost::format(", added object %1%")%model_object->name; wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename)); dlg_cont = dlg.Update(progress_percent, msg); if (!dlg_cont) { @@ -9568,18 +9467,18 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (new_model != nullptr && new_model->objects.size() > 1) { - // BBS do not popup this dialog + //BBS do not popup this dialog bool new_model_auto_drop = true; int single_object_answer = false; if (ask_multi) { RichMessageDialog dlg(q, _L("Load these files as a single object with multiple parts?\n"), - _L("An object with multiple parts was detected"), wxICON_QUESTION | wxYES_NO); + _L("An object with multiple parts was detected"), wxICON_QUESTION | wxYES_NO); dlg.ShowCheckBox(_L("Auto-Drop"), true); single_object_answer = dlg.ShowModal(); - if (dlg.IsCheckBoxChecked() == false) + if (dlg.IsCheckBoxChecked() == false) new_model_auto_drop = false; // convert to multipart and split after load_model_objects @@ -9591,9 +9490,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ // TODO // DONE always convert to multipart, split afterwards to retain relative position // DONE if !auto_drop move all objects over the z-position 0, so that none are clipped by the bed. - // DONE retain auto_drop (and printable) state when assembling or splitting objects. - // DONE when manually split to object ask users if looks_like_multipart and none have auto_drob disabled if they want to disable - // auto_drop for all resulting objects. + // DONE retain auto_drop (and printable) state when assembling or splitting objects. + // DONE when manually split to object ask users if looks_like_multipart and none have auto_drob disabled if they want to disable auto_drop for all resulting objects. // - add icon in object list, similar to fuzzy painting, etc. auto loaded_idxs = load_model_objects(new_model->objects, false, false, new_model_auto_drop); @@ -9605,48 +9503,41 @@ std::vector Plater::priv::load_files(const std::vector& input_ } if (load_config) { - DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (dev) { - MachineObject* obj = dev->get_selected_machine(); + MachineObject *obj = dev->get_selected_machine(); if (obj && obj->is_info_ready()) { if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) { - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - Preset& printer_preset = preset_bundle->printers.get_selected_preset(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + Preset &printer_preset = preset_bundle->printers.get_selected_preset(); - double preset_nozzle_diameter = 0.4; - const ConfigOption* opt = printer_preset.config.option("nozzle_diameter"); - if (opt) - preset_nozzle_diameter = static_cast(opt)->values[0]; + double preset_nozzle_diameter = 0.4; + const ConfigOption *opt = printer_preset.config.option("nozzle_diameter"); + if (opt) preset_nozzle_diameter = static_cast(opt)->values[0]; std::string machine_type = obj->printer_type; - if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) - machine_type = "C12"; + if (obj->is_support_upgrade_kit && obj->installed_upgrade_kit) machine_type = "C12"; bool nozzle_mismatch = !obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, (float) preset_nozzle_diameter); if (printer_preset.get_current_printer_type(preset_bundle) != machine_type || nozzle_mismatch) { - Preset* machine_preset = get_printer_preset(obj); + Preset *machine_preset = get_printer_preset(obj); if (machine_preset != nullptr) { std::string printer_model = machine_preset->config.option("printer_model")->value; - bool sync_printer_info = false; + bool sync_printer_info = false; if (!wxGetApp().app_config->has("sync_after_load_file_show_flag")) { - wxString tips = from_u8( - (boost::format(_u8L("Connected printer is %s. It must match the project preset for printing.\n")) % - printer_model) - .str()); + wxString tips = from_u8((boost::format(_u8L("Connected printer is %s. It must match the project preset for printing.\n")) % printer_model).str()); tips += _L("Do you want to sync the printer information and automatically switch the preset?"); TipsDialog dlg(wxGetApp().mainframe, _L("Tips"), tips, "sync_after_load_file_show_flag", wxYES_NO); - if (dlg.ShowModal() == wxID_YES) { - sync_printer_info = true; - } - } else { + if (dlg.ShowModal() == wxID_YES) { sync_printer_info = true; } + } + else { sync_printer_info = wxGetApp().app_config->get("sync_after_load_file_show_flag") == "true"; } if (sync_printer_info) { - Tab* printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); + Tab *printer_tab = GUI::wxGetApp().get_tab(Preset::Type::TYPE_PRINTER); printer_tab->select_preset(machine_preset->name); - if (obj->is_multi_extruders()) - GUI::wxGetApp().sidebar().sync_extruder_list(); + if (obj->is_multi_extruders()) GUI::wxGetApp().sidebar().sync_extruder_list(); } } } @@ -9655,15 +9546,14 @@ std::vector Plater::priv::load_files(const std::vector& input_ } } - if (new_model) - delete new_model; + if (new_model) delete new_model; - // BBS: translate old 3mf to correct positions + //BBS: translate old 3mf to correct positions if (translate_old) { - // translate the objects + //translate the objects int plate_count = partplate_list.get_plate_count(); - for (int index = 1; index < plate_count; index++) { - PartPlate* cur_plate = (PartPlate*) partplate_list.get_plate(index); + for (int index = 1; index < plate_count; index ++) { + PartPlate* cur_plate = (PartPlate *)partplate_list.get_plate(index); Vec3d cur_origin = cur_plate->get_origin(); Vec3d new_origin = partplate_list.compute_origin_using_new_size(index, current_width, current_depth); @@ -9675,46 +9565,50 @@ std::vector Plater::priv::load_files(const std::vector& input_ partplate_list.register_raycasters_for_picking(*view3D->get_canvas3d()); } - // BBS: add gcode loading logic in the end - q->m_exported_file = false; + //BBS: add gcode loading logic in the end + q->m_exported_file = false; q->skip_thumbnail_invalid = false; if (load_model && load_config) { if (model.objects.empty()) { partplate_list.load_gcode_files(); - PartPlate *first_plate = nullptr, *cur_plate = nullptr; + PartPlate * first_plate = nullptr, *cur_plate = nullptr; int plate_cnt = partplate_list.get_plate_count(); int index = 0, first_plate_index = 0; q->m_valid_plates_count = 0; - for (index = 0; index < plate_cnt; index++) { + for (index = 0; index < plate_cnt; index ++) + { cur_plate = partplate_list.get_plate(index); if (!first_plate && cur_plate->is_slice_result_valid()) { - first_plate = cur_plate; + first_plate = cur_plate; first_plate_index = index; } if (cur_plate->is_slice_result_valid()) - q->m_valid_plates_count++; + q->m_valid_plates_count ++; } - if (first_plate && first_plate->is_slice_result_valid()) { + if (first_plate&&first_plate->is_slice_result_valid()) { q->m_exported_file = true; - // select plate 0 as default + //select plate 0 as default q->select_plate(first_plate_index); - // set to 3d tab + //set to 3d tab q->select_view_3D("Preview"); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); - } else { - // set to 3d tab + } + else { + //set to 3d tab q->select_view_3D("3D"); - // select plate 0 as default + //select plate 0 as default q->select_plate(0); } - } else { - // set to 3d tab + } + else { + //set to 3d tab q->select_view_3D("3D"); - // select plate 0 as default + //select plate 0 as default q->select_plate(0); } - } else { - // always set to 3D after loading files + } + else { + //always set to 3D after loading files q->select_view_3D("3D"); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); } @@ -9726,8 +9620,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->reload_scene(true); view3D->set_as_dirty(); } - if (!silence) - wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); + if (!silence) wxGetApp().app_config->update_skein_dir(input_files[input_files.size() - 1].parent_path().make_preferred().string()); // XXX: Plater.pm had @loaded_files, but didn't seem to fill them with the filenames... } @@ -9740,7 +9633,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ Selection& selection = view3D->get_canvas3d()->get_selection(); selection.clear(); for (size_t idx : obj_idxs) { - selection.add_object((unsigned int) idx, false); + selection.add_object((unsigned int)idx, false); } } // BBS: update object list selection @@ -9751,25 +9644,25 @@ std::vector Plater::priv::load_files(const std::vector& input_ view3D->get_canvas3d()->update_gizmos_on_off_state(); } - GLGizmoSimplify::add_simplify_suggestion_notification(obj_idxs, model.objects, *notification_manager); + GLGizmoSimplify::add_simplify_suggestion_notification( + obj_idxs, model.objects, *notification_manager); - // set designer_model_id - q->model().stl_design_id = designer_model_id; + //set designer_model_id + q->model().stl_design_id = designer_model_id; q->model().stl_design_country = designer_country_code; - // if (!designer_model_id.empty() && q->model().stl_design_id.empty() && !designer_country_code.empty()) { - // q->model().stl_design_id = designer_model_id; - // q->model().stl_design_country = designer_country_code; - // } - // else { - // q->model().stl_design_id = ""; - // q->model().stl_design_country = ""; - // } + //if (!designer_model_id.empty() && q->model().stl_design_id.empty() && !designer_country_code.empty()) { + // q->model().stl_design_id = designer_model_id; + // q->model().stl_design_country = designer_country_code; + //} + //else { + // q->model().stl_design_id = ""; + // q->model().stl_design_country = ""; + //} if (tolal_model_count <= 0 && !q->m_exported_file) { dlg.Hide(); if (!is_user_cancel) { - MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), - wxYES | wxICON_WARNING); + MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING); if (msg.ShowModal() == wxID_YES) {} } } @@ -9778,12 +9671,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ return obj_idxs; } -#define AUTOPLACEMENT_ON_LOAD + #define AUTOPLACEMENT_ON_LOAD -std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, - bool allow_negative_z, - bool split_object, - bool auto_drop) +std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z, bool split_object, bool auto_drop) { const Vec3d bed_size = Slic3r::to_3d(this->bed.build_volume().bounding_volume2d().size(), 1.0) - 2.0 * Vec3d::Ones(); @@ -9797,8 +9687,8 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #ifdef AUTOPLACEMENT_ON_LOAD ModelInstancePtrs new_instances; #endif /* AUTOPLACEMENT_ON_LOAD */ - for (ModelObject* model_object : model_objects) { - auto* object = model.add_object(*model_object); + for (ModelObject *model_object : model_objects) { + auto *object = model.add_object(*model_object); object->sort_volumes(true); std::string object_name = object->name.empty() ? fs::path(object->input_file).filename().string() : object->name; obj_idxs.push_back(obj_count++); @@ -9807,30 +9697,29 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #ifdef AUTOPLACEMENT_ON_LOAD object->center_around_origin(); new_instances.emplace_back(object->add_instance()); -#else /* AUTOPLACEMENT_ON_LOAD */ +#else /* AUTOPLACEMENT_ON_LOAD */ // if object has no defined position(s) we need to rearrange everything after loading // need_arrange = true; - // add a default instance and center object around origin - object->center_around_origin(); // also aligns object to Z = 0 + // add a default instance and center object around origin + object->center_around_origin(); // also aligns object to Z = 0 ModelInstance* instance = object->add_instance(); - // BBS calc transformation + //BBS calc transformation Geometry::Transformation t = instance->get_transformation(); instance->set_offset(Slic3r::to_3d(this->bed.build_volume().bed_center(), -object->origin_translation(2))); #endif /* AUTOPLACEMENT_ON_LOAD */ } - - // BBS: when the object is too large, let the user choose whether to scale it down + + //BBS: when the object is too large, let the user choose whether to scale it down for (size_t i = 0; i < object->instances.size(); ++i) { ModelInstance* instance = object->instances[i]; - const Vec3d size = object->instance_bounding_box(i).size(); - const Vec3d ratio = size.cwiseQuotient(bed_size); - const double max_ratio = std::max(ratio(0), ratio(1)); + const Vec3d size = object->instance_bounding_box(i).size(); + const Vec3d ratio = size.cwiseQuotient(bed_size); + const double max_ratio = std::max(ratio(0), ratio(1)); if (max_ratio > 10000) { - MessageDialog - dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), - _L("Object too large"), wxICON_QUESTION | wxYES); - int answer = dlg.ShowModal(); + MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), + wxICON_QUESTION | wxYES); + int answer = dlg.ShowModal(); // the size of the object is too big -> this could lead to overflow when moving to clipper coordinates, // so scale down the mesh object->scale_mesh_after_creation(1. / max_ratio); @@ -9838,11 +9727,11 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode object->center_around_origin(); scaled_down = true; break; - } else if (max_ratio > 10) { - MessageDialog - dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), - _L("Object too large"), wxICON_QUESTION | wxYES_NO); - int answer = dlg.ShowModal(); + } + else if (max_ratio > 10) { + MessageDialog dlg(q, _L("Your object appears to be too large, do you want to scale it down to fit the print bed automatically?"), _L("Object too large"), + wxICON_QUESTION | wxYES_NO); + int answer = dlg.ShowModal(); if (answer == wxID_YES) { instance->set_scaling_factor(instance->get_scaling_factor() / max_ratio); scaled_down = true; @@ -9852,27 +9741,28 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode if (!auto_drop) { for (size_t i = 0; i < object->instances.size(); ++i) { - ModelInstance* instance = object->instances[i]; - instance->auto_drop = auto_drop; + ModelInstance* instance = object->instances[i]; + instance->auto_drop = auto_drop; } // if under the bed, move over the bed double dist_to_bed = std::min(object->min_z(), double(0)); object->translate_instances(Vec3d(0, 0, -dist_to_bed)); - } else { + } + else { object->ensure_on_bed(allow_negative_z); } if (!split_object) { - // BBS initial assemble transformation + //BBS initial assemble transformation for (ModelObject* model_object : model.objects) { - // BBS initialize assemble transformation + //BBS initialize assemble transformation for (int i = 0; i < model_object->instances.size(); i++) { if (!model_object->instances[i]->is_assemble_initialized()) { model_object->instances[i]->set_assemble_transformation(model_object->instances[i]->get_transformation()); } } - } + } } } @@ -9898,7 +9788,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #else // BBS: find an empty cell to put the copied object for (auto& instance : new_instances) { - auto offset = instance->get_offset(); + auto offset = instance->get_offset(); auto start_point = this->bed.build_volume().bounding_volume2d().center(); bool plate_empty = partplate_list.get_curr_plate()->empty(); Vec3d displacement; @@ -9914,22 +9804,23 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #endif /* AUTOPLACEMENT_ON_LOAD */ - // BBS: remove the auto scaled_down logic when load models - // if (scaled_down) { - // GUI::show_info(q, - // _L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."), - // _L("Object too large?")); - // } + //BBS: remove the auto scaled_down logic when load models + //if (scaled_down) { + // GUI::show_info(q, + // _L("Your object appears to be too large, so it was automatically scaled down to fit your print bed."), + // _L("Object too large?")); + //} BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished auto placement, before add_objects_to_list"); notification_manager->close_notification_of_type(NotificationType::UpdatedItemsInfo); if (obj_idxs.size() > 1) { - std::vector obj_idxs_1(obj_idxs.begin(), obj_idxs.end() - 1); + std::vector obj_idxs_1 (obj_idxs.begin(), obj_idxs.end() - 1); wxGetApp().obj_list()->add_objects_to_list(obj_idxs_1, false); wxGetApp().obj_list()->add_object_to_list(obj_idxs[obj_idxs.size() - 1]); - } else + } + else wxGetApp().obj_list()->add_objects_to_list(obj_idxs); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", after add_objects_to_list"); @@ -9938,7 +9829,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode // which is updated after a view3D->reload_scene(false, flags & (unsigned int)UpdateParams::FORCE_FULL_SCREEN_REFRESH) call for (const size_t idx : obj_idxs) wxGetApp().obj_list()->update_info_items(idx); - + object_list_changed(); this->schedule_background_process(); @@ -9950,7 +9841,7 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode void Plater::priv::load_auxiliary_files() { std::string auxiliary_path = encode_path(q->model().get_auxiliary_file_temp_path().c_str()); - // wxGetApp().mainframe->m_project->Reload(auxiliary_path); + //wxGetApp().mainframe->m_project->Reload(auxiliary_path); } fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) @@ -9959,7 +9850,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) this->update_print_volume_state(); const Selection& selection = get_selection(); - int obj_idx = selection.get_object_idx(); + int obj_idx = selection.get_object_idx(); fs::path output_file; if (file_type == FT_3MF) @@ -9968,20 +9859,22 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type) else if (file_type == FT_STL) { if (obj_idx > 0 && obj_idx < this->model.objects.size() && selection.is_single_full_object()) { output_file = this->model.objects[obj_idx]->get_export_filename(); - } else { + } + else { output_file = into_path(get_project_name()); } } - // bbs name the project using the part name + //bbs name the project using the part name if (output_file.empty()) { if (get_project_name() != _L("Untitled")) { output_file = into_path(get_project_name() + ".3mf"); } } - if (output_file.empty()) { + if (output_file.empty()) + { // first try to get the file name from the current selection - if ((0 <= obj_idx) && (obj_idx < (int) this->model.objects.size())) + if ((0 <= obj_idx) && (obj_idx < (int)this->model.objects.size())) output_file = this->model.objects[obj_idx]->get_export_filename(); if (output_file.empty()) @@ -10003,56 +9896,65 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& { wxString wildcard; switch (file_type) { - case FT_STL: - case FT_DRC: - case FT_AMF: - case FT_3MF: - case FT_GCODE: - case FT_OBJ: wildcard = file_wildcards(file_type); break; - default: wildcard = file_wildcards(FT_MODEL); break; + case FT_STL: + case FT_DRC: + case FT_AMF: + case FT_3MF: + case FT_GCODE: + case FT_OBJ: + wildcard = file_wildcards(file_type); + break; + default: + wildcard = file_wildcards(FT_MODEL); + break; } fs::path output_file = get_export_file_path(file_type); wxString dlg_title; switch (file_type) { - case FT_STL: { - output_file.replace_extension("stl"); - dlg_title = _L("Export STL file:"); - break; - } - case FT_DRC: { - output_file.replace_extension("drc"); - dlg_title = _L("Export Draco file:"); - break; - } - case FT_AMF: { - // XXX: Problem on OS X with double extension? - output_file.replace_extension("zip.amf"); - dlg_title = _L("Export AMF file:"); - break; - } - case FT_3MF: { + case FT_STL: + { + output_file.replace_extension("stl"); + dlg_title = _L("Export STL file:"); + break; + } + case FT_DRC: + { + output_file.replace_extension("drc"); + dlg_title = _L("Export Draco file:"); + break; + } + case FT_AMF: + { + // XXX: Problem on OS X with double extension? + output_file.replace_extension("zip.amf"); + dlg_title = _L("Export AMF file:"); + break; + } + case FT_3MF: + { // A published export is suggested as ".published.3mf" so the role is visible in the // dialog and in the recent-files list. This is only a pre-filled suggestion; the user's // typed filename wins, keeping a plain ".3mf" output fully valid. output_file.replace_extension(published ? "published.3mf" : "3mf"); dlg_title = title.empty() ? _L("Save file as") : title; - break; - } - case FT_OBJ: { - output_file.replace_extension("obj"); - dlg_title = _L("Export OBJ file:"); - break; - } - default: break; + break; + } + case FT_OBJ: + { + output_file.replace_extension("obj"); + dlg_title = _L("Export OBJ file:"); + break; + } + default: break; } std::string out_dir = (boost::filesystem::path(output_file).parent_path()).string(); wxFileDialog dlg(q, dlg_title, - is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), - from_path(output_file.filename()), wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxPD_APP_MODAL); + is_shapes_dir(out_dir) ? from_u8(wxGetApp().app_config->get_last_dir()) : from_path(output_file.parent_path()), from_path(output_file.filename()), + wildcard, wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxPD_APP_MODAL); int result = dlg.ShowModal(); if (result == wxID_CANCEL) @@ -10068,8 +9970,9 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& boost::system::error_code ec; if (boost::filesystem::exists(into_u8(out_path), ec)) { auto result = MessageBox(q->GetHandle(), - wxString::Format(_L("The file %s already exists.\nDo you want to replace it\?"), out_path), - _L("Confirm Save As"), MB_YESNO | MB_ICONWARNING); + wxString::Format(_L("The file %s already exists.\nDo you want to replace it\?"), out_path), + _L("Confirm Save As"), + MB_YESNO | MB_ICONWARNING); if (result != IDYES) return wxEmptyString; } @@ -10080,11 +9983,20 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& return out_path; } -const Selection& Plater::priv::get_selection() const { return view3D->get_canvas3d()->get_selection(); } +const Selection& Plater::priv::get_selection() const +{ + return view3D->get_canvas3d()->get_selection(); +} -Selection& Plater::priv::get_selection() { return view3D->get_canvas3d()->get_selection(); } +Selection& Plater::priv::get_selection() +{ + return view3D->get_canvas3d()->get_selection(); +} -Selection& Plater::priv::get_curr_selection() { return get_current_canvas3D()->get_selection(); } +Selection& Plater::priv::get_curr_selection() +{ + return get_current_canvas3D()->get_selection(); +} int Plater::priv::get_selected_object_idx() const { @@ -10095,9 +10007,9 @@ int Plater::priv::get_selected_object_idx() const int Plater::priv::get_selected_volume_idx() const { auto& selection = get_selection(); - int idx = selection.get_object_idx(); + int idx = selection.get_object_idx(); if ((0 > idx) || (idx > 1000)) - return -1; + return-1; const GLVolume* v = selection.get_first_volume(); if (model.objects[idx]->volumes.size() > 1) return v->volume_idx(); @@ -10124,22 +10036,21 @@ void Plater::priv::object_list_changed() { const bool export_in_progress = this->background_process.is_export_scheduled(); // || ! send_gcode_file.empty()); // XXX: is this right? - // const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; + //const bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state() == ModelInstancePVS_Inside; ObjectFilamentResults object_results; bool model_fits = view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; - model_fits = model_fits && object_results.filaments.empty(); + model_fits = model_fits && object_results.filaments.empty(); PartPlate* part_plate = partplate_list.get_curr_plate(); // BBS - // sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); + //sidebar->enable_buttons(!model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances()); // A mixed filament with deleted or type-mismatched components cannot be resolved at slicing // time, so block the slice buttons the same way MainFrame::get_enable_slice_status() does. bool mixed_broken = sidebar->has_broken_mixed_filament(); - bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances() && !mixed_broken; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ") % - can_slice % model_fits % export_in_progress % part_plate->has_printable_instances(); + bool can_slice = !model.objects.empty() && !export_in_progress && model_fits && part_plate->has_printable_instances() + && !mixed_broken; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": can_slice %1%, model_fits= %2%, export_in_progress %3%, has_printable_instances %4% ")%can_slice %model_fits %export_in_progress %part_plate->has_printable_instances(); main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, can_slice); wxGetApp().params_panel()->notify_object_config_changed(); @@ -10166,9 +10077,15 @@ void Plater::priv::select_all() this->sidebar->obj_list()->update_selections(); } -void Plater::priv::deselect_all() { view3D->deselect_all(); } +void Plater::priv::deselect_all() +{ + view3D->deselect_all(); +} -void Plater::priv::exit_gizmo() { view3D->exit_gizmo(); } +void Plater::priv::exit_gizmo() +{ + view3D->exit_gizmo(); +} void Plater::priv::remove(size_t obj_idx) { @@ -10177,7 +10094,7 @@ void Plater::priv::remove(size_t obj_idx) m_worker.cancel_all(); model.delete_object(obj_idx); - // BBS: notify partplate the instance removed + //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. @@ -10185,15 +10102,15 @@ void Plater::priv::remove(size_t obj_idx) object_list_changed(); } + bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immediately) { // check if object isn't cut // show warning message that "cut consistancy" will not be supported any more - ModelObject* obj = model.objects[obj_idx]; + ModelObject *obj = model.objects[obj_idx]; if (obj->is_cut()) { InfoDialog dialog(q, _L("Delete object which is a part of cut object"), - _L("You are trying to delete an object which is a part of a cut object.\nThis action will break a cut " - "correspondence.\nAfter that, model consistency can\'t be guaranteed."), + _L("You are trying to delete an object which is a part of a cut object.\nThis action will break a cut correspondence.\nAfter that, model consistency can\'t be guaranteed."), false, wxYES | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING); dialog.SetButtonLabel(wxID_YES, _L("Delete")); if (dialog.ShowModal() == wxID_CANCEL) @@ -10210,10 +10127,10 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); model.delete_object(obj_idx); - // BBS: notify partplate the instance removed + //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); - // BBS + //BBS if (refresh_immediately) { update(); object_list_changed(); @@ -10239,7 +10156,7 @@ void Plater::priv::delete_all_objects_from_model() // Stop and reset the Print content. background_process.reset(); - // BBS: update partplate + //BBS: update partplate partplate_list.clear(); model.clear_objects(); @@ -10248,7 +10165,7 @@ void Plater::priv::delete_all_objects_from_model() sidebar->obj_list()->delete_all_objects_from_list(); object_list_changed(); - // BBS + //BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); } @@ -10272,16 +10189,16 @@ void Plater::priv::reset(bool apply_presets_change) view3D->get_canvas3d()->reset_all_gizmos(); reset_gcode_toolpaths(); - // BBS: update gcode to current partplate's - // GCodeProcessorResult* current_result = this->background_process.get_current_plate()->get_slice_result(); - // current_result->reset(); - // gcode_result.reset(); + //BBS: update gcode to current partplate's + //GCodeProcessorResult* current_result = this->background_process.get_current_plate()->get_slice_result(); + //current_result->reset(); + //gcode_result.reset(); view3D->get_canvas3d()->reset_sequential_print_clearance(); m_worker.cancel_all(); - // BBS: clear the partplate list's object before object cleared + //BBS: clear the partplate list's object before object cleared partplate_list.reinit(); partplate_list.update_slice_context_to_current_plate(background_process); preview->update_gcode_result(partplate_list.get_current_slice_result()); @@ -10292,7 +10209,7 @@ void Plater::priv::reset(bool apply_presets_change) assemble_view->get_canvas3d()->reset_explosion_ratio(); update(); - // BBS + //BBS if (wxGetApp().is_editor()) { // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar->obj_list()->delete_all_objects_from_list(); @@ -10302,14 +10219,14 @@ void Plater::priv::reset(bool apply_presets_change) project.reset(); wxGetApp().sidebar().printer_combox()->clear_selected_dev_id(); - // BBS: reset all project embedded presets + //BBS: reset all project embedded presets wxGetApp().preset_bundle->reset_project_embedded_presets(); if (apply_presets_change) wxGetApp().apply_keeped_preset_modifications(); else wxGetApp().load_current_presets(false, false); - // BBS + //BBS model.calib_pa_pattern.reset(); model.plates_custom_gcodes.clear(); @@ -10328,19 +10245,28 @@ void Plater::priv::reset(bool apply_presets_change) } } -void Plater::priv::center_selection() { view3D->center_selected(); } +void Plater::priv::center_selection() +{ + view3D->center_selected(); +} -void Plater::priv::drop_selection() { view3D->drop_selected(); } +void Plater::priv::drop_selection() +{ + view3D->drop_selected(); +} -void Plater::priv::mirror(Axis axis) { view3D->mirror_selection(axis); } +void Plater::priv::mirror(Axis axis) +{ + view3D->mirror_selection(axis); +} -void Plater::find_new_position(const ModelInstancePtrs& instances) +void Plater::find_new_position(const ModelInstancePtrs &instances) { arrangement::ArrangePolygons movable, fixed; arrangement::ArrangeParams arr_params = init_arrange_params(this); - for (const ModelObject* mo : p->model.objects) - for (ModelInstance* inst : mo->instances) { + for (const ModelObject *mo : p->model.objects) + for (ModelInstance *inst : mo->instances) { auto it = std::find(instances.begin(), instances.end(), inst); arrangement::ArrangePolygon arrpoly; inst->get_arrange_polygon(&arrpoly); @@ -10348,7 +10274,7 @@ void Plater::find_new_position(const ModelInstancePtrs& instances) if (it == instances.end()) fixed.emplace_back(std::move(arrpoly)); else { - arrpoly.setter = [it](const arrangement::ArrangePolygon& p) { + arrpoly.setter = [it](const arrangement::ArrangePolygon &p) { if (p.is_arranged() && p.bed_idx == 0) { Vec2d t = p.translation.cast(); (*it)->apply_arrange_result(t, p.rotation); @@ -10363,13 +10289,13 @@ void Plater::find_new_position(const ModelInstancePtrs& instances) arrangement::arrange(movable, fixed, this->build_volume().polygon(), arr_params); - for (auto& m : movable) + for (auto & m : movable) m.apply(); } // split selected object into multiple objects by its volumes void Plater::priv::split_object(bool auto_drop /* = true */) -{ +{ int obj_idx = get_selected_object_idx(); priv::split_object(obj_idx, auto_drop); } @@ -10382,7 +10308,7 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) // we clone model object because split_object() adds the split volumes // into the same model object, thus causing duplicates when we call load_model_objects() - Model new_model = model; + Model new_model = model; ModelObject* current_model_object = new_model.objects[obj_idx]; wxBusyCursor wait; @@ -10391,10 +10317,11 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) if (new_objects.size() == 1) // #ysFIXME use notification Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split.")); - else { + else + { // BBS no solid parts removed // If we splited object which is contain some parts/modifiers then all non-solid parts (modifiers) were deleted - // if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) + //if (current_model_object->volumes.size() > 1 && current_model_object->volumes.size() != new_objects.size()) // notification_manager->push_notification(NotificationType::CustomNotification, // NotificationManager::NotificationLevel::PrintInfoNotificationLevel, // _u8L("All non-solid parts (modifiers) were deleted")); @@ -10403,15 +10330,15 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) auto is_atleast_one_floating = [new_objects]() { for (ModelObject* new_object : new_objects) { - if (new_object->get_instance_min_z(0) >= SINKING_MIN_Z_THRESHOLD) + if (new_object->get_instance_min_z(0) >= SINKING_MIN_Z_THRESHOLD) return true; } return false; }; bool split_auto_drop = auto_drop; if (current_model_object->instances[0]->auto_drop && is_atleast_one_floating()) { - MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"), _L("Object with floating parts was detected"), - wxICON_QUESTION | wxYES_NO); + MessageDialog dlg(q, _L("Disable Auto-Drop to preserve Z positioning?\n"), + _L("Object with floating parts was detected"), wxICON_QUESTION | wxYES_NO); if (dlg.ShowModal() == wxID_YES) split_auto_drop = false; @@ -10421,19 +10348,23 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */) // load all model objects at once, otherwise the plate would be rearranged after each one // causing original positions not to be kept - // BBS: set split_object to true to avoid re-compute assemble matrix + //BBS: set split_object to true to avoid re-compute assemble matrix std::vector idxs = load_model_objects(new_objects, false, true, split_auto_drop); wxGetApp().plater()->get_view3D_canvas3D()->update_instance_printable_state_for_objects(idxs); // select newly added objects - for (size_t idx : idxs) { - get_selection().add_object((unsigned int) idx, false); + for (size_t idx : idxs) + { + get_selection().add_object((unsigned int)idx, false); } } } -void Plater::priv::split_volume() { wxGetApp().obj_list()->split(); } +void Plater::priv::split_volume() +{ + wxGetApp().obj_list()->split(); +} void Plater::priv::scale_selection_to_fit_print_volume() { @@ -10546,11 +10477,11 @@ int Plater::priv::auto_slice_delay_seconds() const std::vector> Plater::priv::get_extruder_filament_info() { std::vector> filament_infos; - DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return filament_infos; - MachineObject* obj_ = dev->get_selected_machine(); + MachineObject *obj_ = dev->get_selected_machine(); if (obj_ == nullptr) return filament_infos; @@ -10563,97 +10494,90 @@ std::vector> Plater::priv::get_extruder_filament void Plater::priv::update_print_volume_state() { - // BBS: use the plate's bounding box instead of the bed's + //BBS: use the plate's bounding box instead of the bed's PartPlate* pp = partplate_list.get_curr_plate(); - BuildVolume build_volume(pp->get_shape(), this->bed.build_volume().printable_height(), this->bed.build_volume().extruder_areas(), - this->bed.build_volume().extruder_heights()); + BuildVolume build_volume(pp->get_shape(), this->bed.build_volume().printable_height(), this->bed.build_volume().extruder_areas(), this->bed.build_volume().extruder_heights()); this->model.update_print_volume_state(build_volume); } -void Plater::priv::process_validation_warning(StringObjectException const& warning) const +void Plater::priv::process_validation_warning(StringObjectException const &warning) const { if (warning.string.empty()) notification_manager->close_notification_of_type(NotificationType::ValidateWarning); else { std::string text = warning.string; - auto po = dynamic_cast(warning.object); - auto mo = po ? po->model_object() : dynamic_cast(warning.object); - // ORCA: Update process_validation_warning to handle ModelInstance selection and include fallback - auto mi = dynamic_cast(warning.object); + auto po = dynamic_cast(warning.object); + auto mo = po ? po->model_object() : dynamic_cast(warning.object); + //ORCA: Update process_validation_warning to handle ModelInstance selection and include fallback + auto mi = dynamic_cast(warning.object); - auto action_fn = (mo || mi || !warning.opt_key.empty()) ? - [id = mo ? mo->id() : (mi ? mi->id() : 0), parent_id = mi ? mi->get_object()->id() : 0, - is_inst = (mi != nullptr), opt = warning.opt_key](wxEvtHandler*) { - auto& objects = wxGetApp().model().objects; + auto action_fn = (mo || mi || !warning.opt_key.empty()) ? [id = mo ? mo->id() : (mi ? mi->id() : 0), + parent_id = mi ? mi->get_object()->id() : 0, + is_inst = (mi != nullptr), + opt = warning.opt_key](wxEvtHandler *) { + auto & objects = wxGetApp().model().objects; - if (is_inst) { - bool selected = false; - auto iter = std::find_if(objects.begin(), objects.end(), - [parent_id](auto o) { return o->id() == parent_id; }); - if (iter != objects.end()) { - ModelObject* obj = *iter; - int inst_idx = -1; - for (size_t i = 0; i < obj->instances.size(); ++i) { - if (obj->instances[i]->id() == id) { - inst_idx = i; - break; - } - } + if (is_inst) { + bool selected = false; + auto iter = std::find_if(objects.begin(), objects.end(), [parent_id](auto o) { return o->id() == parent_id; }); + if (iter != objects.end()) { + ModelObject* obj = *iter; + int inst_idx = -1; + for(size_t i=0; iinstances.size(); ++i) { + if (obj->instances[i]->id() == id) { + inst_idx = i; + break; + } + } - wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - if (inst_idx != -1) { - auto* model = wxGetApp().obj_list()->GetModel(); - wxDataViewItem item; - wxDataViewItem objItem = model->GetObjectItem(obj); - if (objItem.IsOk()) { - int vm_obj_idx = model->GetIdByItem(objItem); - if (vm_obj_idx != -1) { - item = model->GetItemByInstanceId(vm_obj_idx, inst_idx); - } - } - if (item.IsOk()) { - wxDataViewItemArray sel_items; - sel_items.Add(item); - wxGetApp().obj_list()->select_items(sel_items); - wxGetApp().obj_list()->update_selections_on_canvas(); - selected = true; - } - } + if (inst_idx != -1) { + auto* model = wxGetApp().obj_list()->GetModel(); + wxDataViewItem item; + wxDataViewItem objItem = model->GetObjectItem(obj); + if (objItem.IsOk()) { + int vm_obj_idx = model->GetIdByItem(objItem); + if (vm_obj_idx != -1) { + item = model->GetItemByInstanceId(vm_obj_idx, inst_idx); + } + } + if (item.IsOk()) { + wxDataViewItemArray sel_items; + sel_items.Add(item); + wxGetApp().obj_list()->select_items(sel_items); + wxGetApp().obj_list()->update_selections_on_canvas(); + selected = true; + } + } - if (!selected) { - wxGetApp().obj_list()->select_items({{obj, nullptr}}); - wxGetApp().obj_list()->update_selections_on_canvas(); - } - } - } else { - auto iter = id.id ? - std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : - objects.end(); - if (iter != objects.end()) { - wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - wxGetApp().obj_list()->select_items({{*iter, nullptr}}); - wxGetApp().obj_list()->update_selections_on_canvas(); - } - } - if (!opt.empty()) { - if ((!is_inst && id.id) || (is_inst && parent_id.id)) - wxGetApp().params_panel()->switch_to_object(); - wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); - } - return false; - } : - std::function(); + if (!selected) { + wxGetApp().obj_list()->select_items({ {obj, nullptr} }); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + } else { + auto iter = id.id ? std::find_if(objects.begin(), objects.end(), [id](auto o) { return o->id() == id; }) : objects.end(); + if (iter != objects.end()) { + wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); + wxGetApp().obj_list()->select_items({{*iter, nullptr}}); + wxGetApp().obj_list()->update_selections_on_canvas(); + } + } + if (!opt.empty()) { + if ((!is_inst && id.id) || (is_inst && parent_id.id)) + wxGetApp().params_panel()->switch_to_object(); + wxGetApp().sidebar().jump_to_option(opt, Preset::TYPE_PRINT, L""); + } + return false; + } : std::function(); auto hypertext = (mo || mi || !warning.opt_key.empty()) ? _u8L("Jump to") : ""; - if (mo) - hypertext += std::string(" [") + mo->name + "]"; - if (mi) - hypertext += std::string(" [") + mi->get_object()->name + "]"; - if (!warning.opt_key.empty()) - hypertext += std::string(" (") + warning.opt_key + ")"; + if (mo) hypertext += std::string(" [") + mo->name + "]"; + if (mi) hypertext += std::string(" [") + mi->get_object()->name + "]"; + if (!warning.opt_key.empty()) hypertext += std::string(" (") + warning.opt_key + ")"; // BBS disable support enforcer - // if (text == "_SUPPORTS_OFF") { + //if (text == "_SUPPORTS_OFF") { // text = _u8L("An object has custom support enforcers which will not be used " // "because supports are disabled.")+"\n"; // hypertext = _u8L("Enable supports for enforcers only"); @@ -10670,21 +10594,24 @@ void Plater::priv::process_validation_warning(StringObjectException const& warni // }; //} - notification_manager->push_notification(NotificationType::ValidateWarning, - NotificationManager::NotificationLevel::WarningNotificationLevel, - _u8L("WARNING:") + "\n" + text, hypertext, action_fn); + notification_manager->push_notification( + NotificationType::ValidateWarning, + NotificationManager::NotificationLevel::WarningNotificationLevel, + _u8L("WARNING:") + "\n" + text, hypertext, action_fn + ); } } -void Plater::priv::process_validation_warnings(const std::vector& warnings) const +void Plater::priv::process_validation_warnings(const std::vector &warnings) const { // ValidateWarning stacks by text (m_multiple_types), so clear the stale set before re-adding. notification_manager->close_notification_of_type(NotificationType::ValidateWarning); - for (const StringObjectException& warning : warnings) + for (const StringObjectException &warning : warnings) if (!warning.string.empty()) process_validation_warning(warning); } + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) @@ -10698,52 +10625,50 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); // Apply new config to the possibly running background task. - bool was_running = background_process.running(); - // BBS: add the switch print logic before Print::Apply - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format( - ": enter, force_validation=%1% postpone_error_messages=%2%, switch_print=%3%, was_running=%4%") % - force_validation % postpone_error_messages % switch_print % was_running; - if (switch_print) { - // BBS: update the current print to the current plate + bool was_running = background_process.running(); + //BBS: add the switch print logic before Print::Apply + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, force_validation=%1% postpone_error_messages=%2%, switch_print=%3%, was_running=%4%")%force_validation %postpone_error_messages %switch_print %was_running; + if (switch_print) + { + //BBS: update the current print to the current plate this->partplate_list.update_slice_context_to_current_plate(background_process); this->preview->update_gcode_result(partplate_list.get_current_slice_result()); } - background_process.fff_print()->set_check_multi_filaments_compatibility( - wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); Print::ApplyStatus invalidated; const auto& preset_bundle = wxGetApp().preset_bundle; if (preset_bundle->get_printer_extruder_count() > 1) { - PartPlate* cur_plate = background_process.get_current_plate(); - std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); + PartPlate* cur_plate = background_process.get_current_plate(); + std::vector f_maps = cur_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = cur_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = background_process.apply(this->model, preset_bundle->full_config(false, f_maps, f_volume_maps)); background_process.fff_print()->set_extruder_filament_info(get_extruder_filament_info()); - } else + } + else invalidated = background_process.apply(this->model, preset_bundle->full_config(false)); if ((invalidated == Print::APPLY_STATUS_CHANGED) || (invalidated == Print::APPLY_STATUS_INVALIDATED)) // BBS: add only gcode mode q->set_only_gcode(false); - // BBS: add slicing related logs - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process apply result=%1%") % invalidated; + //BBS: add slicing related logs + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process apply result=%1%")%invalidated; if (background_process.empty()) view3D->get_canvas3d()->reset_sequential_print_clearance(); if (invalidated == Print::APPLY_STATUS_INVALIDATED) { - // BBS: update current plater's slicer result to invalid + //BBS: update current plater's slicer result to invalid this->background_process.get_current_plate()->update_slice_result_valid_state(false); - // no need, should be done in background_process.apply - // this->background_process.get_current_gcode_result()->reset(); - // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. - // Otherwise they will be just refreshed. + //no need, should be done in background_process.apply + //this->background_process.get_current_gcode_result()->reset(); + // Reset preview canvases. If the print has been invalidated, the preview canvases will be cleared. + // Otherwise they will be just refreshed. if (preview != nullptr) { // If the preview is not visible, the following line just invalidates the preview, // but the G-code paths or SLA preview are calculated first once the preview is made visible. @@ -10756,27 +10681,28 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; notification_manager->set_slicing_progress_hidden(); - } else { + } + else { if (preview && preview->get_reload_paint_after_background_process_apply()) { preview->set_reload_paint_after_background_process_apply(false); preview->reload_print(); } } - if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && !background_process.empty()) { + if ((invalidated != Print::APPLY_STATUS_UNCHANGED || force_validation) && ! background_process.empty()) { // The delayed error message is no more valid. delayed_error_message.clear(); // The state of the Print changed, and it is non-zero. Let's validate it and give the user feedback on errors. - // BBS: add is_warning logic + //BBS: add is_warning logic std::vector warnings; - // BBS: refine seq-print logic + //BBS: refine seq-print logic Polygons polygons; std::vector> height_polygons; StringObjectException err = background_process.validate(&warnings, &polygons, &height_polygons); // update string by type q->post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%") % err.string % warnings.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%")%err.string%warnings.size(); if (err.string.empty()) { this->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -10793,88 +10719,92 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool view3D->get_canvas3d()->set_as_dirty(); view3D->get_canvas3d()->request_extra_frame(); } - } else { + } + else { this->partplate_list.get_curr_plate()->update_apply_result_invalid(true); // The print is not valid. // Show error as notification. notification_manager->push_validate_error_notification(err); - // also update the warnings + //also update the warnings process_validation_warnings(warnings); return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; if (printer_technology == ptFFF) { const Print* print = background_process.fff_print(); - // Polygons polygons; - // if (print->config().print_sequence == PrintSequence::ByObject) - // Print::sequential_print_clearance_valid(*print, &polygons); + //Polygons polygons; + //if (print->config().print_sequence == PrintSequence::ByObject) + // Print::sequential_print_clearance_valid(*print, &polygons); view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); view3D->get_canvas3d()->set_sequential_print_clearance_polygons(polygons, height_polygons); } } - } else if (!this->delayed_error_message.empty()) { + } + else if (! this->delayed_error_message.empty()) { // Reusing the old state. return_state |= UPDATE_BACKGROUND_PROCESS_INVALID; } - // actualizate warnings + //actualizate warnings if (invalidated != Print::APPLY_STATUS_UNCHANGED || background_process.empty()) { if (background_process.empty()) process_validation_warning({}); actualize_slicing_warnings(*this->background_process.current_print()); actualize_object_warnings(*this->background_process.current_print()); - show_warning_dialog = false; + show_warning_dialog = false; process_completed_with_error = -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format( - ", Line %1%: was_running = %2%, running %3%, invalidated=%4%, return_state=%5%, internal_cancel=%6%") % - __LINE__ % was_running % this->background_process.running() % invalidated % return_state % - this->background_process.is_internal_cancelled(); - if (was_running && !this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { - if (invalidated != Print::APPLY_STATUS_UNCHANGED || this->background_process.is_internal_cancelled()) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: was_running = %2%, running %3%, invalidated=%4%, return_state=%5%, internal_cancel=%6%") + % __LINE__ % was_running % this->background_process.running() % invalidated % return_state % this->background_process.is_internal_cancelled(); + if (was_running && ! this->background_process.running() && (return_state & UPDATE_BACKGROUND_PROCESS_RESTART) == 0) { + if (invalidated != Print::APPLY_STATUS_UNCHANGED || this->background_process.is_internal_cancelled()) + { // The background processing was killed and it will not be restarted. // Post the "canceled" callback message, so that it will be processed after any possible pending status bar update messages. - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Cancelled, nullptr); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" %1%, post an EVT_PROCESS_COMPLETED to main, status %2%") % __LINE__ % evt.status(); + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Cancelled, nullptr); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%, post an EVT_PROCESS_COMPLETED to main, status %2%")%__LINE__ %evt.status(); wxQueueEvent(q, evt.Clone()); } } - if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0) { + if ((return_state & UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + { // Validation of the background data failed. - // BBS: add slice&&print status update logic + //BBS: add slice&&print status update logic this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); process_completed_with_error = partplate_list.get_curr_plate_index(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(", Line %1%: set to process_completed_with_error, return_state=%2%") % __LINE__ % - return_state; - } else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: set to process_completed_with_error, return_state=%2%")%__LINE__%return_state; + } + else + { // Background data is valid. - if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0) + if ((return_state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 || + (return_state & UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) != 0 ) notification_manager->set_slicing_progress_hidden(); - // BBS: add slice&&print status update logic - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(", Line %1%: background data valid, return_state=%2%") % __LINE__ % return_state; + //BBS: add slice&&print status update logic + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: background data valid, return_state=%2%")%__LINE__%return_state; PartPlate* cur_plate = background_process.get_current_plate(); - if (background_process.finished() && cur_plate && cur_plate->is_slice_result_valid()) { - // ready_to_slice = false; + if (background_process.finished() && cur_plate && cur_plate->is_slice_result_valid()) + { + //ready_to_slice = false; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); - } else if (!background_process.empty() && - !background_process.running()) /* Do not update buttons if background process is running - * This condition is important for SLA mode especially, - * when this function is called several times during calculations - * */ + } + else if (!background_process.empty() && + !background_process.running()) /* Do not update buttons if background process is running + * This condition is important for SLA mode especially, + * when this function is called several times during calculations + * */ { if (cur_plate->can_slice()) { - // ready_to_slice = true; + //ready_to_slice = true; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); process_completed_with_error = -1; - } else { - // ready_to_slice = false; + } + else { + //ready_to_slice = false; this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); process_completed_with_error = partplate_list.get_curr_plate_index(); } @@ -10898,7 +10828,7 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool #endif } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: exit, return_state=%2%") % __LINE__ % return_state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: exit, return_state=%2%")%__LINE__%return_state; return return_state; } @@ -10907,33 +10837,36 @@ bool Plater::priv::restart_background_process(unsigned int state) { if (!m_worker.is_idle()) { // Avoid a race condition - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false") % __LINE__; + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: ui jobs running, return false")%__LINE__; return false; } - if (!this->background_process.empty() && (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && - (((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && !this->background_process.finished()) || - (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0)) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: print is valid, try to start it now") % __LINE__; + if ( ! this->background_process.empty() && + (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) == 0 && + ( ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0 && ! this->background_process.finished()) || + (state & UPDATE_BACKGROUND_PROCESS_FORCE_EXPORT) != 0 || + (state & UPDATE_BACKGROUND_PROCESS_RESTART) != 0 ) ) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: print is valid, try to start it now")%__LINE__; // The print is valid and it can be started. if (this->background_process.start()) { if (!show_warning_dialog) on_slicing_began(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully") % __LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; return true; } - } else if (this->background_process.empty()) { + } + else if (this->background_process.empty()) { PartPlate* cur_plate = background_process.get_current_plate(); if (cur_plate->is_slice_result_valid() && ((state & UPDATE_BACKGROUND_PROCESS_FORCE_RESTART) != 0)) { if (this->background_process.start()) { if (!show_warning_dialog) on_slicing_began(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully") % __LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: start successfully")%__LINE__; return true; } } } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: not started") % __LINE__; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: not started")%__LINE__; return false; } @@ -10941,7 +10874,7 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova { wxCHECK_RET(!(output_path.empty()), "export_gcode: output_path and upload_job empty"); - BOOST_LOG_TRIVIAL(trace) << boost::format("export_gcode: output_path %1%") % output_path.string(); + BOOST_LOG_TRIVIAL(trace) << boost::format("export_gcode: output_path %1%")%output_path.string(); if (model.objects.empty()) return; @@ -10959,9 +10892,9 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova return; show_warning_dialog = true; - if (!output_path.empty()) { + if (! output_path.empty()) { background_process.schedule_export(output_path.string(), output_path_on_removable_media); - notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() { return true; }, 1000, 0); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); } else { BOOST_LOG_TRIVIAL(info) << "output_path is empty"; } @@ -10991,9 +10924,9 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova return; show_warning_dialog = true; - if (!output_path.empty()) { + if (! output_path.empty()) { background_process.schedule_export(output_path.string(), output_path_on_removable_media); - notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() { return true; }, 1000, 0); + notification_manager->push_delayed_notification(NotificationType::ExportOngoing, []() {return true; }, 1000, 0); } else { background_process.schedule_upload(std::move(upload_job)); } @@ -11005,9 +10938,10 @@ void Plater::priv::export_gcode(fs::path output_path, bool output_path_on_remova unsigned int Plater::priv::update_restart_background_process(bool force_update_scene, bool force_update_preview) { bool switch_print = true; - // BBS: judge whether can switch print or not - if ((partplate_list.get_plate_count() > 1) && !this->background_process.can_switch_print()) { - // can not switch print currently + //BBS: judge whether can switch print or not + if ((partplate_list.get_plate_count() > 1) && !this->background_process.can_switch_print()) + { + //can not switch print currently BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": plate count %1%, can not switch") % partplate_list.get_plate_count(); switch_print = false; } @@ -11028,19 +10962,21 @@ void Plater::priv::update_fff_scene() this->preview->reload_print(); // In case this was MM print, wipe tower bounding box on 3D tab might need redrawing with exact depth: view3D->reload_scene(true); - // BBS: add assemble view related logic + //BBS: add assemble view related logic assemble_view->reload_scene(true); q->mark_plate_toolbar_image_dirty(); } -// BBS: add print project related logic +//BBS: add print project related logic void Plater::priv::update_fff_scene_only_shells(bool only_shells) { - if (this->preview != nullptr) { + if (this->preview != nullptr) + { const Print* current_print = this->background_process.fff_print(); - if (current_print) { - // this->preview->reset_shells(); + if (current_print) + { + //this->preview->reset_shells(); this->preview->load_shells(*current_print); } } @@ -11068,14 +11004,13 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const try { const bool is_step = boost::algorithm::iends_with(path, ".stp") || boost::algorithm::iends_with(path, ".step"); if (is_step) { - auto config = wxGetApp().app_config; - double linear = std::max(0.003, string_to_double_decimal_point(config->get("linear_deflection"))); - double angle = std::max(0.5, string_to_double_decimal_point(config->get("angle_deflection"))); + auto config = wxGetApp().app_config; + double linear = std::max(0.003, string_to_double_decimal_point(config->get("linear_deflection"))); + double angle = std::max(0.5, string_to_double_decimal_point(config->get("angle_deflection"))); bool split_compound = config->get_bool("is_split_compound"); bool is_user_cancel = false; - auto callback = [&is_user_cancel, linear, angle, split_compound](Slic3r::Step& file, double& linear_value, double& angle_value, - bool& is_split) -> int { + auto callback = [&is_user_cancel, linear, angle, split_compound](Slic3r::Step &file, double &linear_value, double &angle_value, bool &is_split) -> int { if (wxGetApp().app_config->get_bool("enable_step_mesh_setting")) { StepMeshDialog mesh_dlg(nullptr, file, linear, angle); if (mesh_dlg.ShowModal() == wxID_OK) { @@ -11094,10 +11029,8 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const return -1; }; - new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, callback, - linear, angle, split_compound); - if (is_user_cancel) - return false; + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, callback, linear, angle, split_compound); + if (is_user_cancel) return false; } else { new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel); } @@ -11105,14 +11038,14 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const model_object->center_around_origin(); model_object->ensure_on_bed(); } - } catch (std::exception&) { + } + catch (std::exception&) { // error while loading return false; } if (new_model.objects.size() > 1 || new_model.objects.front()->volumes.size() > 1) { - MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replacement"), - wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("Unable to replace with more than one volume"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return false; } @@ -11123,7 +11056,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const q->take_snapshot(snapshot); ModelObject* old_model_object = model.objects[object_idx]; - ModelVolume* old_volume = old_model_object->volumes[volume_idx]; + ModelVolume* old_volume = old_model_object->volumes[volume_idx]; bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; @@ -11135,8 +11068,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * - (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); @@ -11176,7 +11108,7 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const void Plater::priv::replace_with_stl() { - if (!q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) return; const Selection& selection = get_selection(); @@ -11185,8 +11117,8 @@ void Plater::priv::replace_with_stl() return; const GLVolume* v = selection.get_first_volume(); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); // collects paths of files to load @@ -11205,8 +11137,7 @@ void Plater::priv::replace_with_stl() fs::path out_path = dialog.GetPath().ToUTF8().data(); if (out_path.empty()) { - MessageDialog dlg(q, _L("File for the replacement wasn\'t selected"), _L("Error during replacement"), - wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("File for the replacement wasn\'t selected"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return; } @@ -11225,7 +11156,7 @@ void Plater::priv::replace_with_stl() void Plater::priv::replace_all_with_stl() { - if (!q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) return; const Selection& selection = get_selection(); @@ -11249,7 +11180,7 @@ void Plater::priv::replace_all_with_stl() Slic3r::GUI::ItemType item_type = wxGetApp().obj_list()->GetModel()->GetItemType(item); if (item_type & itPlate) { if (item.IsOk()) { - ObjectDataViewModelNode* node = static_cast(item.GetID()); + ObjectDataViewModelNode *node = static_cast(item.GetID()); selected_plate_idxs.push_back(node->GetPlateIdx()); } } @@ -11270,8 +11201,8 @@ void Plater::priv::replace_all_with_stl() for (unsigned int idx : volume_idxs) { const GLVolume* v = selection.get_volume(idx); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); const ModelObject* object = model.objects[object_idx]; const ModelVolume* volume = object->volumes[volume_idx]; @@ -11290,8 +11221,7 @@ void Plater::priv::replace_all_with_stl() fs::path out_path = dialog.GetPath().ToUTF8().data(); if (out_path.empty()) { - MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replacement"), - wxOK | wxOK_DEFAULT | wxICON_WARNING); + MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replacement"), wxOK | wxOK_DEFAULT | wxICON_WARNING); dlg.ShowModal(); return; } @@ -11300,8 +11230,8 @@ void Plater::priv::replace_all_with_stl() for (unsigned int idx : volume_idxs) { const GLVolume* v = selection.get_volume(idx); - int object_idx = v->object_idx(); - int volume_idx = v->volume_idx(); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); const ModelObject* object = model.objects[object_idx]; const ModelVolume* volume = object->volumes[volume_idx]; @@ -11351,20 +11281,19 @@ void Plater::priv::replace_all_with_stl() } #if ENABLE_RELOAD_FROM_DISK_REWORK -static std::vector> reloadable_volumes(const Model& model, const Selection& selection) +static std::vector> reloadable_volumes(const Model &model, const Selection &selection) { std::vector> ret; - const std::set& selected_volumes_idxs = selection.get_volume_idxs(); + const std::set & selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { - const GLVolume& v = *selection.get_volume(idx); - const int o_idx = v.object_idx(); + const GLVolume &v = *selection.get_volume(idx); + const int o_idx = v.object_idx(); if (0 <= o_idx && o_idx < int(model.objects.size())) { - const ModelObject* obj = model.objects[o_idx]; - const int v_idx = v.volume_idx(); + const ModelObject *obj = model.objects[o_idx]; + const int v_idx = v.volume_idx(); if (0 <= v_idx && v_idx < int(obj->volumes.size())) { - const ModelVolume* vol = obj->volumes[v_idx]; - if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() && - !fs::path(vol->source.input_file).extension().string().empty()) + const ModelVolume *vol = obj->volumes[v_idx]; + if (!vol->source.is_from_builtin_objects && !vol->source.input_file.empty() && !fs::path(vol->source.input_file).extension().string().empty()) ret.push_back({o_idx, v_idx}); } } @@ -11383,14 +11312,12 @@ void Plater::priv::reload_from_disk() if (selected_volumes.empty()) return; - std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair& v1, const std::pair& v2) { + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); - }); - selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), - [](const std::pair& v1, const std::pair& v2) { - return (v1.first == v2.first) && (v1.second == v2.second); - }), - selected_volumes.end()); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), selected_volumes.end()); #else Plater::TakeSnapshot snapshot(q, _u8L("Reload from disk")); @@ -11406,9 +11333,8 @@ void Plater::priv::reload_from_disk() int volume_idx; // operators needed by std::algorithms - bool operator<(const SelectedVolume& other) const - { return object_idx < other.object_idx || (object_idx == other.object_idx && volume_idx < other.volume_idx); } - bool operator==(const SelectedVolume& other) const { return object_idx == other.object_idx && volume_idx == other.volume_idx; } + bool operator < (const SelectedVolume& other) const { return object_idx < other.object_idx || (object_idx == other.object_idx && volume_idx < other.volume_idx); } + bool operator == (const SelectedVolume& other) const { return object_idx == other.object_idx && volume_idx == other.volume_idx; } }; std::vector selected_volumes; @@ -11416,11 +11342,11 @@ void Plater::priv::reload_from_disk() const std::set& selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { const GLVolume* v = selection.get_volume(idx); - int v_idx = v->volume_idx(); + int v_idx = v->volume_idx(); if (v_idx >= 0) { int o_idx = v->object_idx(); - if (0 <= o_idx && o_idx < (int) model.objects.size()) - selected_volumes.push_back({o_idx, v_idx}); + if (0 <= o_idx && o_idx < (int)model.objects.size()) + selected_volumes.push_back({ o_idx, v_idx }); } } std::sort(selected_volumes.begin(), selected_volumes.end()); @@ -11433,8 +11359,8 @@ void Plater::priv::reload_from_disk() #if ENABLE_RELOAD_FROM_DISK_REWORK std::vector> replace_paths; for (auto [obj_idx, vol_idx] : selected_volumes) { - const ModelObject* object = model.objects[obj_idx]; - const ModelVolume* volume = object->volumes[vol_idx]; + const ModelObject *object = model.objects[obj_idx]; + const ModelVolume *volume = object->volumes[vol_idx]; if (fs::exists(volume->source.input_file)) input_paths.push_back(volume->source.input_file); else { @@ -11480,8 +11406,8 @@ void Plater::priv::reload_from_disk() if (!found) missing_input_paths.push_back(volume->source.input_file); } - } else if (!object->input_file.empty() && volume->is_model_part() && !volume->name.empty() && - !volume->source.is_from_builtin_objects) + } + else if (!object->input_file.empty() && volume->is_model_part() && !volume->name.empty() && !volume->source.is_from_builtin_objects) missing_input_paths.push_back(volume->name); } #endif // ENABLE_RELOAD_FROM_DISK_REWORK @@ -11492,7 +11418,7 @@ void Plater::priv::reload_from_disk() while (!missing_input_paths.empty()) { // ask user to select the missing file fs::path search = missing_input_paths.back(); - wxString title = _L("Please select a file"); + wxString title = _L("Please select a file"); #if defined(__APPLE__) title += " (" + from_u8(search.filename().string()) + ")"; #endif // __APPLE__ @@ -11502,7 +11428,7 @@ void Plater::priv::reload_from_disk() return; std::string sel_filename_path = dialog.GetPath().ToUTF8().data(); - std::string sel_filename = fs::path(sel_filename_path).filename().string(); + std::string sel_filename = fs::path(sel_filename_path).filename().string(); if (boost::algorithm::iequals(search.filename().string(), sel_filename)) { input_paths.push_back(sel_filename_path); missing_input_paths.pop_back(); @@ -11517,11 +11443,13 @@ void Plater::priv::reload_from_disk() if (fs::exists(repathed_filename)) { input_paths.push_back(repathed_filename.string()); it = missing_input_paths.erase(it); - } else + } + else ++it; } - } else { - wxString message = _L("Do you want to replace it") + " ?"; + } + else { + wxString message = _L("Do you want to replace it") + " ?"; MessageDialog dlg(q, message, _L("Message"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION); if (dlg.ShowModal() == wxID_YES) #if ENABLE_RELOAD_FROM_DISK_REWORK @@ -11551,7 +11479,7 @@ void Plater::priv::reload_from_disk() auto obj_color_fun = [&path](ObjDialogInOut &in_out) { if (!boost::iends_with(path, ".obj")) { return; } const std::vector extruder_colours = wxGetApp().plater()->get_extruder_colors_from_plater_config(); - ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); + ObjColorDialog color_dlg(nullptr, in_out, extruder_colours, Sidebar::should_show_SEMM_buttons()); if (color_dlg.ShowModal() != wxID_OK) { in_out.filament_ids.clear(); } @@ -11560,68 +11488,72 @@ void Plater::priv::reload_from_disk() wxBusyInfo info(_L("Reload from:") + " " + from_u8(path), q->get_current_canvas3D()->get_wxglcanvas()); Model new_model; - try { - // BBS: add plate data related logic + try + { + //BBS: add plate data related logic PlateDataPtrs plate_data; - // BBS: project embedded settings + //BBS: project embedded settings std::vector project_presets; // BBS: backup - if (boost::iends_with(path, ".stp") || boost::iends_with(path, ".step")) { + if (boost::iends_with(path, ".stp") || + boost::iends_with(path, ".step")) { double linear = string_to_double_decimal_point(wxGetApp().app_config->get("linear_deflection")); - double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); - bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); - new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, - nullptr, linear, angle, is_split); - } else { - new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, - &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, - obj_color_fun); + double angle = string_to_double_decimal_point(wxGetApp().app_config->get("angle_deflection")); + bool is_split = wxGetApp().app_config->get_bool("is_split_compound"); + new_model = Model::read_from_step(path, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, nullptr, nullptr, nullptr, linear, angle, is_split); + }else { + new_model = Model::read_from_file(path, nullptr, nullptr, LoadStrategy::AddDefaultInstances | LoadStrategy::LoadModel, &plate_data, &project_presets, nullptr, nullptr, nullptr, nullptr, nullptr, 0, obj_color_fun); } - for (ModelObject* model_object : new_model.objects) { + + for (ModelObject* model_object : new_model.objects) + { model_object->center_around_origin(); model_object->ensure_on_bed(); } - if (plate_data.size() > 0) { - // partplate_list.load_from_3mf_structure(plate_data); + if (plate_data.size() > 0) + { + //partplate_list.load_from_3mf_structure(plate_data); partplate_list.update_slice_context_to_current_plate(background_process); this->preview->update_gcode_result(partplate_list.get_current_slice_result()); release_PlateData_list(plate_data); sidebar->obj_list()->reload_all_plates(); } - } catch (std::exception&) { + } + catch (std::exception&) + { // error while loading return; } #if ENABLE_RELOAD_FROM_DISK_REWORK for (auto [obj_idx, vol_idx] : selected_volumes) { - ModelObject* old_model_object = model.objects[obj_idx]; - ModelVolume* old_volume = old_model_object->volumes[vol_idx]; + ModelObject *old_model_object = model.objects[obj_idx]; + ModelVolume *old_volume = old_model_object->volumes[vol_idx]; bool sinking = old_model_object->min_z() < SINKING_Z_THRESHOLD; bool has_source = !old_volume->source.input_file.empty() && - boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), - fs::path(path).filename().string()); - bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); if (has_source || has_name) { - int new_volume_idx = -1; - int new_object_idx = -1; - bool match_found = false; + int new_volume_idx = -1; + int new_object_idx = -1; + bool match_found = false; // take idxs from the matching volume if (has_source && old_volume->source.object_idx < int(new_model.objects.size())) { - const ModelObject* obj = new_model.objects[old_volume->source.object_idx]; + const ModelObject *obj = new_model.objects[old_volume->source.object_idx]; if (old_volume->source.volume_idx < int(obj->volumes.size())) { - const std::string& new_input_file = obj->volumes[old_volume->source.volume_idx]->source.input_file; - const std::string& old_input_file = old_volume->source.input_file; + const std::string &new_input_file = obj->volumes[old_volume->source.volume_idx]->source.input_file; + const std::string &old_input_file = old_volume->source.input_file; // Orca: match on the exact source path first, then fall back to filename-only so reload // still matches when the project stored a bare filename and the file was found next to // the project (same-folder fallback) or picked via the locate dialog (#12992). - if (new_input_file == old_input_file || boost::algorithm::iequals(fs::path(new_input_file).filename().string(), - fs::path(old_input_file).filename().string())) { + if (new_input_file == old_input_file || + boost::algorithm::iequals(fs::path(new_input_file).filename().string(), + fs::path(old_input_file).filename().string())) { new_volume_idx = old_volume->source.volume_idx; new_object_idx = old_volume->source.object_idx; match_found = true; @@ -11632,8 +11564,8 @@ void Plater::priv::reload_from_disk() if (!match_found && has_name) { // take idxs from the 1st matching volume for (size_t o = 0; o < new_model.objects.size(); ++o) { - ModelObject* obj = new_model.objects[o]; - bool found = false; + ModelObject *obj = new_model.objects[o]; + bool found = false; for (size_t v = 0; v < obj->volumes.size(); ++v) { if (obj->volumes[v]->name == old_volume->name) { new_volume_idx = (int) v; @@ -11642,8 +11574,7 @@ void Plater::priv::reload_from_disk() break; } } - if (found) - break; + if (found) break; // BBS: step model,object loaded as a volume. GUI_ObfectList.cpp load_modifier() if (obj->name == old_volume->name) { new_object_idx = (int) o; @@ -11656,24 +11587,24 @@ void Plater::priv::reload_from_disk() fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); continue; } - ModelObject* new_model_object = new_model.objects[new_object_idx]; + ModelObject *new_model_object = new_model.objects[new_object_idx]; if (int(new_model_object->volumes.size()) <= new_volume_idx) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); continue; } - ModelVolume* new_volume = nullptr; + ModelVolume *new_volume = nullptr; // BBS: step model if (new_volume_idx < 0 && new_object_idx >= 0) { - TriangleMesh mesh = new_model_object->mesh(); - new_volume = old_model_object->add_volume(std::move(mesh)); - new_volume->name = new_model_object->name; + TriangleMesh mesh = new_model_object->mesh(); + new_volume = old_model_object->add_volume(std::move(mesh)); + new_volume->name = new_model_object->name; new_volume->source.input_file = new_model_object->input_file; - } else { + }else { new_volume = old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); // new_volume = old_model_object->volumes.back(); } - + new_volume->set_new_unique_id(); new_volume->config.apply(old_volume->config); new_volume->set_type(old_volume->type()); @@ -11701,8 +11632,7 @@ void Plater::priv::reload_from_disk() std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back()); old_model_object->delete_volume(old_model_object->volumes.size() - 1); - if (!sinking) - old_model_object->ensure_on_bed(); + if (!sinking) old_model_object->ensure_on_bed(); old_model_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); sla::reproject_points_and_holes(old_model_object); @@ -11715,39 +11645,37 @@ void Plater::priv::reload_from_disk() // update the selected volumes whose source is the current file for (const SelectedVolume& sel_v : selected_volumes) { ModelObject* old_model_object = model.objects[sel_v.object_idx]; - ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; bool sinking = old_model_object->bounding_box().min.z() < SINKING_Z_THRESHOLD; - bool has_source = !old_volume->source.input_file.empty() && - boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), - fs::path(path).filename().string()); - bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); + bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); + bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); if (has_source || has_name) { int new_volume_idx = -1; int new_object_idx = -1; - // if (has_source) { - // // take idxs from source - // new_volume_idx = old_volume->source.volume_idx; - // new_object_idx = old_volume->source.object_idx; - // } - // else { - // take idxs from the 1st matching volume - for (size_t o = 0; o < new_model.objects.size(); ++o) { - ModelObject* obj = new_model.objects[o]; - bool found = false; - for (size_t v = 0; v < obj->volumes.size(); ++v) { - if (obj->volumes[v]->name == old_volume->name) { - new_volume_idx = (int) v; - new_object_idx = (int) o; - found = true; - break; +// if (has_source) { +// // take idxs from source +// new_volume_idx = old_volume->source.volume_idx; +// new_object_idx = old_volume->source.object_idx; +// } +// else { + // take idxs from the 1st matching volume + for (size_t o = 0; o < new_model.objects.size(); ++o) { + ModelObject* obj = new_model.objects[o]; + bool found = false; + for (size_t v = 0; v < obj->volumes.size(); ++v) { + if (obj->volumes[v]->name == old_volume->name) { + new_volume_idx = (int)v; + new_object_idx = (int)o; + found = true; + break; + } } + if (found) + break; } - if (found) - break; - } - // } +// } if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); @@ -11766,11 +11694,10 @@ void Plater::priv::reload_from_disk() new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); new_volume->set_transformation(old_volume->get_transformation()); - new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * - (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); + new_volume->translate(new_volume->get_transformation().get_matrix_no_offset() * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); new_volume->source.object_idx = old_volume->source.object_idx; new_volume->source.volume_idx = old_volume->source.volume_idx; - assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); + assert(! old_volume->source.is_converted_from_inches || ! old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); else if (old_volume->source.is_converted_from_meters) @@ -11791,10 +11718,8 @@ void Plater::priv::reload_from_disk() for (auto [src, dest] : replace_paths) { for (auto [obj_idx, vol_idx] : selected_volumes) { if (boost::algorithm::iequals(model.objects[obj_idx]->volumes[vol_idx]->source.input_file, src.string())) - // When an error occurs, either the dest parsing error occurs, or the number of objects in the dest is greater than 1 and - // cannot be replaced, and cannot be replaced in this loop. - if (!replace_volume_with_stl(obj_idx, vol_idx, dest, "")) - break; + // When an error occurs, either the dest parsing error occurs, or the number of objects in the dest is greater than 1 and cannot be replaced, and cannot be replaced in this loop. + if (!replace_volume_with_stl(obj_idx, vol_idx, dest, "")) break; } } #else @@ -11802,10 +11727,8 @@ void Plater::priv::reload_from_disk() const auto& path = replace_paths[i].string(); for (const SelectedVolume& sel_v : selected_volumes) { ModelObject* old_model_object = model.objects[sel_v.object_idx]; - ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; - bool has_source = !old_volume->source.input_file.empty() && - boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), - fs::path(path).filename().string()); + ModelVolume* old_volume = old_model_object->volumes[sel_v.volume_idx]; + bool has_source = !old_volume->source.input_file.empty() && boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); if (!replace_volume_with_stl(sel_v.object_idx, sel_v.volume_idx, path, "")) { fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); } @@ -11841,7 +11764,7 @@ void Plater::priv::reload_all_from_disk() Plater::TakeSnapshot snapshot(q, _u8L("Reload all")); Plater::SuppressSnapshots suppress(q); - Selection& selection = get_selection(); + Selection& selection = get_selection(); Selection::IndicesList curr_idxs = selection.get_volume_idxs(); // reload from disk uses selection select_all(); @@ -11853,88 +11776,92 @@ void Plater::priv::reload_all_from_disk() } } -// BBS: add no_slice logic +//BBS: add no_slice logic void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) { if (std::find(panels.begin(), panels.end(), panel) == panels.end()) return; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current_panel %1%, new_panel %2%") % current_panel % panel; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current_panel %1%, new_panel %2%")%current_panel%panel; #ifdef __WXMAC__ bool force_render = (current_panel != nullptr); #endif // __WXMAC__ - // BBS: add slice logic when switch to preview page + //BBS: add slice logic when switch to preview page auto do_reslice = [this, no_slice]() { - // see: Plater::priv::object_list_changed() - // FIXME: it may be better to have a single function making this check and let it be called wherever needed - bool export_in_progress = this->background_process.is_export_scheduled(); - ObjectFilamentResults object_results; - bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; - model_fits = model_fits && object_results.filaments.empty(); - // BBS: add partplate logic - PartPlate* current_plate = this->partplate_list.get_curr_plate(); - bool only_has_gcode_need_preview = false; - bool current_has_print_instances = current_plate->has_printable_instances(); - if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) - only_has_gcode_need_preview = true; + // see: Plater::priv::object_list_changed() + // FIXME: it may be better to have a single function making this check and let it be called wherever needed + bool export_in_progress = this->background_process.is_export_scheduled(); + ObjectFilamentResults object_results; + bool model_fits = this->view3D->get_canvas3d()->check_volumes_outside_state(&object_results) != ModelInstancePVS_Partly_Outside; + model_fits = model_fits&&object_results.filaments.empty(); + //BBS: add partplate logic + PartPlate * current_plate = this->partplate_list.get_curr_plate(); + bool only_has_gcode_need_preview = false; + bool current_has_print_instances = current_plate->has_printable_instances(); + if (current_plate->is_slice_result_valid() && this->model.objects.empty() && !current_has_print_instances) + only_has_gcode_need_preview = true; - bool mixed_broken = sidebar->has_broken_mixed_filament(); + bool mixed_broken = sidebar->has_broken_mixed_filament(); - BOOST_LOG_TRIVIAL(info) - << __FUNCTION__ - << boost::format( - ": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%, mixed_broken %5%") % - no_slice % export_in_progress % model_fits % m_is_slicing % mixed_broken; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": from set_current_panel, no_slice %1%, export_in_progress %2%, model_fits %3%, m_is_slicing %4%, mixed_broken %5%")%no_slice%export_in_progress%model_fits%m_is_slicing%mixed_broken; - if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances && !mixed_broken) { - // if already running in background, not relice here - // BBS: add more judge for slicing - if (!this->background_process.running() && !this->m_is_slicing) { + if (!no_slice && !this->model.objects.empty() && !export_in_progress && model_fits && current_has_print_instances && !mixed_broken) + { + //if already running in background, not relice here + //BBS: add more judge for slicing + if (!this->background_process.running() && !this->m_is_slicing) + { + this->m_slice_all = false; + this->q->reslice(); + } + else { + //reset current plate to the slicing plate + int plate_index = this->background_process.get_current_plate()->get_index(); + this->partplate_list.select_plate(plate_index); + } + } + else if (only_has_gcode_need_preview) + { this->m_slice_all = false; this->q->reslice(); - } else { - // reset current plate to the slicing plate - int plate_index = this->background_process.get_current_plate()->get_index(); - this->partplate_list.select_plate(plate_index); } - } else if (only_has_gcode_need_preview) { - this->m_slice_all = false; - this->q->reslice(); - } - // BBS: process empty plate, reset previous toolpath - else { - // if (!this->m_slice_all) - if (!current_has_print_instances) - reset_gcode_toolpaths(); - // this->q->refresh_print(); - if (!preview->get_canvas3d()->is_initialized()) { - preview->get_canvas3d()->render(true); - } - } - // TODO: turn off this switch currently - /*auto canvas_w = float(preview->get_canvas3d()->get_canvas_size().get_width()); - auto canvas_h = float(preview->get_canvas3d()->get_canvas_size().get_height()); - Point screen_center(canvas_w/2, canvas_h/2); - auto center_point = preview->get_canvas3d()->_mouse_to_3d(screen_center); - center_point(2) = 0.f; - if (!current_plate->contains(center_point)) - this->partplate_list.select_plate_view();*/ - - // keeps current gcode preview, if any - if (this->m_slice_all) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": slicing all, just reload shells"); - this->update_fff_scene_only_shells(); - } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": single slice, reload print"); - if (model_fits) - this->preview->reload_print(); // TODO + //BBS: process empty plate, reset previous toolpath else - this->update_fff_scene_only_shells(); - } + { + //if (!this->m_slice_all) + if (!current_has_print_instances) + reset_gcode_toolpaths(); + //this->q->refresh_print(); + if (!preview->get_canvas3d()->is_initialized()) + { + preview->get_canvas3d()->render(true); + } + } + //TODO: turn off this switch currently + /*auto canvas_w = float(preview->get_canvas3d()->get_canvas_size().get_width()); + auto canvas_h = float(preview->get_canvas3d()->get_canvas_size().get_height()); + Point screen_center(canvas_w/2, canvas_h/2); + auto center_point = preview->get_canvas3d()->_mouse_to_3d(screen_center); + center_point(2) = 0.f; + if (!current_plate->contains(center_point)) + this->partplate_list.select_plate_view();*/ - preview->set_as_dirty(); - }; + // keeps current gcode preview, if any + if (this->m_slice_all) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": slicing all, just reload shells"); + this->update_fff_scene_only_shells(); + } + else { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": single slice, reload print"); + if (model_fits) + this->preview->reload_print(); // TODO + else + this->update_fff_scene_only_shells(); + } + + preview->set_as_dirty(); + }; // Add sidebar and toolbar collapse logic if (panel == view3D || panel == preview) { @@ -11950,7 +11877,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } } - if (current_panel == panel) { + if (current_panel == panel) + { if (panel == view3D) { if (view3D->is_reload_delayed()) { // Delayed loading of the 3D scene when caller requests the already active tab. @@ -11965,8 +11893,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) if (notification_manager != nullptr) notification_manager->set_in_preview(false); } - // BBS: add slice logic when switch to preview page - // BBS: add only gcode mode + //BBS: add slice logic when switch to preview page + //BBS: add only gcode mode if (!q->only_gcode_mode() && (current_panel == preview) && (wxGetApp().is_editor())) { do_reslice(); } @@ -11974,17 +11902,17 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) return; } - // BBS: wish to reset all plates stats item selected state when back to View3D Tab + //BBS: wish to reset all plates stats item selected state when back to View3D Tab preview->get_canvas3d()->reset_select_plate_toolbar_selection(); wxPanel* old_panel = current_panel; - // #if BBL_HAS_FIRST_PAGE +//#if BBL_HAS_FIRST_PAGE if (!old_panel) { // Wayland may report the first canvas as not yet shown while the frame is still mapping. // Keep the panel switch anyway so handlers are bound and the first paint can initialize GL later. panel->Show(); } - // #endif +//#endif current_panel = panel; // to reduce flickering when changing view, first set as visible the new current panel @@ -12019,7 +11947,8 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } if (current_panel == view3D || current_panel == preview) { cam.load_camera_view(view3D->get_canvas3d()->get_camera()); - } else if (current_panel == assemble_view) { + } + else if (current_panel == assemble_view) { cam.load_camera_view(assemble_view->get_canvas3d()->get_camera()); } } @@ -12030,12 +11959,12 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) else if (old_panel == assemble_view) { assemble_view->get_canvas3d()->unbind_event_handlers(); - GLCanvas3D* assemble_canvas = assemble_view->get_canvas3d(); + GLCanvas3D* assemble_canvas = assemble_view->get_canvas3d(); Selection::IndicesList select_idxs = assemble_canvas->get_selection().get_volume_idxs(); - Selection& view3d_selection = view3D->get_canvas3d()->get_selection(); + Selection& view3d_selection = view3D->get_canvas3d()->get_selection(); view3d_selection.clear(); for (unsigned int idx : select_idxs) { - auto v = assemble_canvas->get_selection().get_volume(idx); + auto v = assemble_canvas->get_selection().get_volume(idx); auto real_idx = view3d_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); if (real_idx >= 0) { view3d_selection.add(real_idx, false); @@ -12060,10 +11989,11 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size view3D->get_canvas3d()->reset_old_size(); // BBS - // view_toolbar.select_item("3D"); + //view_toolbar.select_item("3D"); if (notification_manager != nullptr) notification_manager->set_in_preview(false); - } else if (current_panel == preview) { + } + else if (current_panel == preview) { q->invalid_all_plate_thumbnails(); if (old_panel == view3D) view3D->get_canvas3d()->unbind_event_handlers(); @@ -12106,27 +12036,28 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // reset cached size to force a resize on next call to render() to keep imgui in synch with canvas size preview->get_canvas3d()->reset_old_size(); // BBS - // view_toolbar.select_item("Preview"); + //view_toolbar.select_item("Preview"); if (notification_manager != nullptr) notification_manager->set_in_preview(true); - } else if (current_panel == assemble_view) { + } + else if (current_panel == assemble_view) { if (old_panel == view3D) { view3D->get_canvas3d()->unbind_event_handlers(); - } else if (old_panel == preview) + } + else if (old_panel == preview) preview->get_canvas3d()->unbind_event_handlers(); assemble_view->get_canvas3d()->bind_event_handlers(); assemble_view->reload_scene(true); if (old_panel == view3D) { - GLCanvas3D* view3D_canvas = view3D->get_canvas3d(); + GLCanvas3D* view3D_canvas = view3D->get_canvas3d(); Selection::IndicesList select_idxs = view3D_canvas->get_selection().get_volume_idxs(); - Selection& assemble_selection = assemble_view->get_canvas3d()->get_selection(); + Selection& assemble_selection = assemble_view->get_canvas3d()->get_selection(); assemble_selection.clear(); for (unsigned int idx : select_idxs) { auto v = view3D_canvas->get_selection().get_volume(idx); - auto real_idx = assemble_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), - v->volume_idx()); + auto real_idx = assemble_selection.query_real_volume_idx_from_other_view(v->object_idx(), v->instance_idx(), v->volume_idx()); if (real_idx >= 0) { assemble_selection.add(real_idx, false); } @@ -12136,12 +12067,12 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) // BBS set default view and zoom if (first_enter_assemble) { wxGetApp().plater()->get_camera().requires_zoom_to_volumes = true; - first_enter_assemble = false; + first_enter_assemble = false; } assemble_view->set_as_dirty(); // BBS - // view_toolbar.select_item("Assemble"); + //view_toolbar.select_item("Assemble"); } current_panel->SetFocusFromKbd(); @@ -12150,32 +12081,33 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice) } // BBS -void Plater::priv::on_combobox_select(wxCommandEvent& evt) +void Plater::priv::on_combobox_select(wxCommandEvent &evt) { PlaterPresetComboBox* preset_combo_box = dynamic_cast(evt.GetEventObject()); if (preset_combo_box) { this->on_select_preset(evt); sidebar->update_printer_thumbnail(); - } else { + } + else { this->on_select_bed_type(evt); } } -void Plater::priv::on_select_bed_type(wxCommandEvent& evt) +void Plater::priv::on_select_bed_type(wxCommandEvent &evt) { - ComboBox* combo = static_cast(evt.GetEventObject()); - auto select_bed_type = sidebar->get_cur_select_bed_type(); - std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[(int) select_bed_type - 1]; + ComboBox* combo = static_cast(evt.GetEventObject()); + auto select_bed_type = sidebar->get_cur_select_bed_type(); + std::string bed_type_name = print_config_def.get("curr_bed_type")->enum_values[(int)select_bed_type - 1]; - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; - DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; if (keys_map) { BedType new_bed_type = btCount; for (auto item : *keys_map) { if (item.first == bed_type_name) { - new_bed_type = (BedType) item.second; + new_bed_type = (BedType)item.second; break; } } @@ -12193,10 +12125,10 @@ void Plater::priv::on_select_bed_type(wxCommandEvent& evt) // update app_config AppConfig* app_config = wxGetApp().app_config; app_config->set("curr_bed_type", std::to_string(int(new_bed_type))); - app_config->set_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), "curr_bed_type", - std::to_string(int(new_bed_type))); + app_config->set_printer_setting(wxGetApp().preset_bundle->printers.get_selected_preset_name(), + "curr_bed_type", std::to_string(int(new_bed_type))); - // update slice status + //update slice status auto plate_list = partplate_list.get_plate_list(); for (auto plate : plate_list) { if (plate->get_bed_type() == btDefault) { @@ -12212,7 +12144,7 @@ void Plater::priv::on_select_bed_type(wxCommandEvent& evt) } } -void Plater::priv::on_select_preset(wxCommandEvent& evt) +void Plater::priv::on_select_preset(wxCommandEvent &evt) { PlaterPresetComboBox* combo = static_cast(evt.GetEventObject()); Preset::Type preset_type = combo->get_type(); @@ -12232,11 +12164,11 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) // BBS:Save the plate parameters before switching PartPlateList& old_plate_list = this->partplate_list; - PartPlate* old_plate = old_plate_list.get_selected_plate(); - Vec3d old_plate_pos = old_plate->get_center_origin(); + PartPlate* old_plate = old_plate_list.get_selected_plate(); + Vec3d old_plate_pos = old_plate->get_center_origin(); // BBS: Save the model in the current platelist - std::vector> plate_object; + std::vector > plate_object; for (size_t i = 0; i < old_plate_list.get_plate_count(); ++i) { PartPlate* plate = old_plate_list.get_plate(i); std::vector obj_idxs; @@ -12264,8 +12196,8 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) preset_name = Preset::remove_suffix_modified(wx_stored_name.ToUTF8().data()); } else { wxString wx_name = combo->GetString(selection); - preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, - Preset::remove_suffix_modified(wx_name.ToUTF8().data())); + preset_name = wxGetApp().preset_bundle->get_preset_name_by_alias(preset_type, + Preset::remove_suffix_modified(wx_name.ToUTF8().data())); } if (preset_type == Preset::TYPE_FILAMENT) { @@ -12278,7 +12210,7 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) sidebar->auto_calc_flushing_volumes(idx); } auto select_flag = combo->GetFlag(selection); - combo->ShowBadge(select_flag == (int) PresetComboBox::FilamentAMSType::FROM_AMS); + combo->ShowBadge(select_flag == (int)PresetComboBox::FilamentAMSType::FROM_AMS); q->on_filament_change(idx); } bool select_preset = !combo->selection_is_changed_according_to_physical_printers(); @@ -12286,10 +12218,11 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) if (preset_type == Preset::TYPE_FILAMENT && sidebar->is_multifilament()) { // Only update the plater UI for the 2nd and other filaments. combo->update(); - } else if (select_preset) { + } + else if (select_preset) { if (preset_type == Preset::TYPE_PRINTER) { PhysicalPrinterCollection& physical_printers = wxGetApp().preset_bundle->physical_printers; - if (combo->is_selected_physical_printer()) + if(combo->is_selected_physical_printer()) preset_name = physical_printers.get_selected_printer_preset_name(); else physical_printers.unselect_printer(); @@ -12301,7 +12234,7 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) dlg.ShowModal(); } preset->is_visible = true; // force visible - preset_name = preset->name; + preset_name = preset->name; } std::string old_preset_name = wxGetApp().preset_bundle->printers.get_edited_preset().name; @@ -12312,24 +12245,24 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) q->on_config_change(wxGetApp().preset_bundle->full_config()); }); + if (old_preset_name != preset_name && wxGetApp().app_config->get("auto_calculate_flush") == "all") { wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(-1); } // sync extruder info when select multi_extruder preset - if (Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager()) { - MachineObject* obj = dev->get_selected_machine(); + if (Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager()) { + MachineObject *obj = dev->get_selected_machine(); if (obj && obj->is_multi_extruders()) { - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - Preset& cur_preset = preset_bundle->printers.get_edited_preset(); + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + Preset& cur_preset = preset_bundle->printers.get_edited_preset(); if (cur_preset.get_printer_type(preset_bundle) == obj->get_show_printer_type()) { double preset_nozzle_diameter = cur_preset.config.option("nozzle_diameter")->values[0]; - bool same_nozzle_diameter = true; + bool same_nozzle_diameter = true; const auto& extruders = obj->GetExtderSystem()->GetExtruders(); - for (const DevExtder& extruder : extruders) { - if (!obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(extruder.GetExtId(), - float(preset_nozzle_diameter))) { + for (const DevExtder &extruder : extruders) { + if (!obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(extruder.GetExtId(), float(preset_nozzle_diameter))) { same_nozzle_diameter = false; } } @@ -12359,17 +12292,17 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) // update plater with new config q->on_config_change(wxGetApp().preset_bundle->full_config()); if (preset_type == Preset::TYPE_PRINTER) { - /* Settings list can be changed after printer preset changing, so - * update all settings items for all item had it. - * Furthermore, Layers editing is implemented only for FFF printers - * and for SLA presets they should be deleted - */ + /* Settings list can be changed after printer preset changing, so + * update all settings items for all item had it. + * Furthermore, Layers editing is implemented only for FFF printers + * and for SLA presets they should be deleted + */ wxGetApp().obj_list()->update_object_list_by_printer_technology(); // BBS:Model reset by plate center PartPlateList& cur_plate_list = this->partplate_list; - PartPlate* cur_plate = cur_plate_list.get_curr_plate(); - Vec3d cur_plate_pos = cur_plate->get_center_origin(); + PartPlate* cur_plate = cur_plate_list.get_curr_plate(); + Vec3d cur_plate_pos = cur_plate->get_center_origin(); if (old_plate_pos.x() != cur_plate_pos.x() || old_plate_pos.y() != cur_plate_pos.y()) { for (int i = 0; i < plate_object.size(); ++i) { @@ -12380,15 +12313,16 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) view3D->deselect_all(); } -#if 0 // do not toggle auto calc when change printer - // update flush matrix +#if 0 // do not toggle auto calc when change printer + // update flush matrix size_t filament_size = wxGetApp().plater()->get_extruder_colors_from_plater_config().size(); for (size_t idx = 0; idx < filament_size; ++idx) wxGetApp().plater()->sidebar().auto_calc_flushing_volumes(idx); #endif // Show shared profiles notification for the newly selected printer - if (wxGetApp().app_config->get_bool("show_shared_profiles_notification")) { + if (wxGetApp().app_config->get_bool("show_shared_profiles_notification")) + { std::string printer_name = wxGetApp().preset_bundle->printers.get_selected_preset_base().name; std::string encoded_name = Http::url_encode(printer_name); @@ -12423,29 +12357,27 @@ void Plater::priv::on_select_preset(wxCommandEvent& evt) // update slice state and set bedtype default for 3rd-party printer auto plate_list = partplate_list.get_plate_list(); for (auto plate : plate_list) { - plate->update_slice_result_valid_state(false); + plate->update_slice_result_valid_state(false); } } -void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) +void Plater::priv::on_slicing_update(SlicingStatusEvent &evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % - evt.status.text; - // BBS: add slice project logic + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, percent %2%, text %3%") % evt.GetEventType() % evt.status.percent % evt.status.text; + //BBS: add slice project logic std::string title_text = _u8L("Slicing"); - evt.status.text = title_text + evt.status.text; + evt.status.text = title_text + evt.status.text; if (evt.status.percent >= 0) { - if (!m_worker.is_idle()) { + if (!m_worker.is_idle()) { // Avoid a race condition return; } - notification_manager->set_slicing_progress_percentage(evt.status.text, (float) evt.status.percent / 100.0f); + notification_manager->set_slicing_progress_percentage(evt.status.text, (float)evt.status.percent / 100.0f); // update slicing percent PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); - // slicing parallel, only update if percent is greater than before + //slicing parallel, only update if percent is greater than before if (evt.status.percent > plate_list.get_curr_plate()->get_slicing_percent()) plate_list.get_curr_plate()->update_slicing_percent(evt.status.percent); } @@ -12453,9 +12385,9 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) if (evt.status.flags & (PrintBase::SlicingStatus::RELOAD_SCENE | PrintBase::SlicingStatus::RELOAD_SLA_SUPPORT_POINTS)) { switch (this->printer_technology) { case ptFFF: - // BBS: add slice project logic, only display shells at the beginning + //BBS: add slice project logic, only display shells at the beginning if (!m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1))) - // this->update_fff_scene(); + //this->update_fff_scene(); this->update_fff_scene_only_shells(); break; case ptSLA: @@ -12472,29 +12404,28 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) this->preview->reload_print(); } - if (evt.status.flags & - (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { + if (evt.status.flags & (PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS | PrintBase::SlicingStatus::UPDATE_PRINT_OBJECT_STEP_WARNINGS)) { // Update notification center with warnings of object_id and its warning_step. ObjectID object_id = evt.status.warning_object_id; - int warning_step = evt.status.warning_step; + int warning_step = evt.status.warning_step; PrintStateBase::StateWithWarnings state; - ModelObject const* model_object = nullptr; + ModelObject const * model_object = nullptr; - // BBS: add partplate related logic, use the print in background process + //BBS: add partplate related logic, use the print in background process if (evt.status.flags & PrintBase::SlicingStatus::UPDATE_PRINT_STEP_WARNINGS) { state = this->printer_technology == ptFFF ? - this->background_process.m_fff_print->step_state_with_warnings(static_cast(warning_step)) : - this->background_process.m_sla_print->step_state_with_warnings(static_cast(warning_step)); + this->background_process.m_fff_print->step_state_with_warnings(static_cast(warning_step)) : + this->background_process.m_sla_print->step_state_with_warnings(static_cast(warning_step)); } else if (this->printer_technology == ptFFF) { - const PrintObject* print_object = this->background_process.m_fff_print->get_object(object_id); + const PrintObject *print_object = this->background_process.m_fff_print->get_object(object_id); if (print_object) { - state = print_object->step_state_with_warnings(static_cast(warning_step)); + state = print_object->step_state_with_warnings(static_cast(warning_step)); model_object = print_object->model_object(); } } else { - const SLAPrintObject* print_object = this->background_process.m_sla_print->get_object(object_id); + const SLAPrintObject *print_object = this->background_process.m_sla_print->get_object(object_id); if (print_object) { - state = print_object->step_state_with_warnings(static_cast(warning_step)); + state = print_object->step_state_with_warnings(static_cast(warning_step)); model_object = print_object->model_object(); } } @@ -12502,12 +12433,10 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) for (auto const& warning : state.warnings) { if (warning.current) { NotificationManager::NotificationLevel notif_level = NotificationManager::NotificationLevel::WarningNotificationLevel; - if (evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingReplaceInitEmptyLayers || - evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingEmptyGcodeLayers) { + if (evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingReplaceInitEmptyLayers || evt.status.message_type == PrintStateBase::SlicingNotificationType::SlicingEmptyGcodeLayers) { notif_level = NotificationManager::NotificationLevel::SeriousWarningNotificationLevel; } - notification_manager->push_slicing_warning_notification(warning.message, false, model_object, object_id, warning_step, - warning.message_id, notif_level); + notification_manager->push_slicing_warning_notification(warning.message, false, model_object, object_id, warning_step, warning.message_id, notif_level); add_warning(warning, object_id.id); } } @@ -12515,13 +12444,12 @@ void Plater::priv::on_slicing_update(SlicingStatusEvent& evt) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); } -void Plater::priv::on_slicing_completed(wxCommandEvent& evt) +void Plater::priv::on_slicing_completed(wxCommandEvent & evt) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": event_type %1%, string %2%") % evt.GetEventType() % evt.GetString(); - // BBS: add slice project logic + //BBS: add slice project logic if (m_slice_all && (m_cur_slice_plate < (partplate_list.get_plate_count() - 1))) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ - << boost::format("slicing all, finished plate %1%, will continue next.") % m_cur_slice_plate; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("slicing all, finished plate %1%, will continue next.")%m_cur_slice_plate; return; } @@ -12529,10 +12457,11 @@ void Plater::priv::on_slicing_completed(wxCommandEvent& evt) delayed_scene_refresh = true; else { if (this->printer_technology == ptFFF) { - // BBS: only reload shells + //BBS: only reload shells this->update_fff_scene_only_shells(false); - // this->update_fff_scene(); - } else + //this->update_fff_scene(); + } + else this->update_sla_scene(); } BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format("exit."); @@ -12562,8 +12491,8 @@ void Plater::priv::on_slicing_began() clear_warnings(); notification_manager->close_notification_of_type(NotificationType::SignDetected); notification_manager->close_notification_of_type(NotificationType::ExportFinished); - bool is_first_plate = m_cur_slice_plate == 0; - bool slice_all = q->m_only_gcode ? m_slice_all_only_has_gcode : m_slice_all; + bool is_first_plate = m_cur_slice_plate == 0; + bool slice_all = q->m_only_gcode ? m_slice_all_only_has_gcode : m_slice_all; bool need_change_dailytips = !(slice_all && !is_first_plate); notification_manager->set_slicing_progress_began(); notification_manager->update_slicing_notif_dailytips(need_change_dailytips); @@ -12572,7 +12501,8 @@ void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, s { for (auto& it : current_warnings) { if (warning.message_id == it.first.message_id) { - if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) { + if (warning.message_id != 0 || (warning.message_id == 0 && warning.message == it.first.message)) + { if (warning.message_id != 0) it.first.message = warning.message; return; @@ -12581,7 +12511,7 @@ void Plater::priv::add_warning(const Slic3r::PrintStateBase::Warning& warning, s } current_warnings.emplace_back(std::pair(warning, oid)); } -void Plater::priv::actualize_slicing_warnings(const PrintBase& print) +void Plater::priv::actualize_slicing_warnings(const PrintBase &print) { std::vector ids = print.print_object_ids(); if (ids.empty()) { @@ -12596,7 +12526,8 @@ void Plater::priv::actualize_slicing_warnings(const PrintBase& print) void Plater::priv::actualize_object_warnings(const PrintBase& print) { std::vector ids; - for (const ModelObject* object : print.model().objects) { + for (const ModelObject* object : print.model().objects ) + { ids.push_back(object->id()); } std::sort(ids.begin(), ids.end()); @@ -12620,29 +12551,31 @@ bool Plater::priv::warnings_dialog() else text += it.first.message; } - // text += "\n\nDo you still wish to export?"; + //text += "\n\nDo you still wish to export?"; MessageDialog msg_window(this->q, from_u8(text), _L("warnings"), wxOK); - const auto res = msg_window.ShowModal(); + const auto res = msg_window.ShowModal(); return res == wxID_OK; + } -// BBS: add project slice logic -void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) +//BBS: add project slice logic +void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, m_ignore_event %1%, status %2%") % m_ignore_event % evt.status(); - // BBS:ignore cancel event for some special case - if (m_ignore_event) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": enter, m_ignore_event %1%, status %2%")%m_ignore_event %evt.status(); + //BBS:ignore cancel event for some special case + if (m_ignore_event) + { m_ignore_event = false; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": ignore this event %1%") % evt.status(); return; } - // BBS: add project slice logic + //BBS: add project slice logic bool is_finished = !m_slice_all || (m_cur_slice_plate == (partplate_list.get_plate_count() - 1)); - // BBS: slice .gcode.3mf file related logic, assign is_finished again + //BBS: slice .gcode.3mf file related logic, assign is_finished again bool only_has_gcode_need_preview = false; - auto plate_list = this->partplate_list.get_plate_list(); - bool has_print_instances = false; + auto plate_list = this->partplate_list.get_plate_list(); + bool has_print_instances = false; for (auto plate : plate_list) has_print_instances = has_print_instances || plate->has_printable_instances(); if (this->model.objects.empty() && !has_print_instances) @@ -12675,26 +12608,25 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) notification_manager->set_slicing_progress_hidden(); } } else { - std::vector ptrs; - for (auto oid : message.second) { - const PrintObject* print_object = this->background_process.m_fff_print->get_object(ObjectID(oid)); - if (print_object) { - ptrs.push_back(print_object->model_object()); - } + std::vector ptrs; + for (auto oid : message.second) + { + const PrintObject *print_object = this->background_process.m_fff_print->get_object(ObjectID(oid)); + if (print_object) { ptrs.push_back(print_object->model_object()); } } notification_manager->push_slicing_error_notification(message.first, ptrs); } - if (evt.invalidate_plater()) { + if (evt.invalidate_plater()) + { // BBS #if 0 const wxString invalid_str = _L("Invalid data"); for (auto btn : { ActionButtonType::abReslice, ActionButtonType::abSendGCode, ActionButtonType::abExport }) sidebar->set_btn_label(btn, invalid_str); #endif - process_completed_with_error = partplate_list.get_curr_plate_index(); - ; + process_completed_with_error = partplate_list.get_curr_plate_index();; } - has_error = true; + has_error = true; is_finished = true; } if (evt.cancelled()) { @@ -12703,11 +12635,11 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) is_finished = true; } - // BBS: set the current plater's slice result to valid + //BBS: set the current plater's slice result to valid if (!this->background_process.empty()) this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); - // BBS: update the action button according to the current plate's status + //BBS: update the action button according to the current plate's status bool ready_to_slice = !this->partplate_list.get_curr_plate()->is_slice_result_valid(); // BBS @@ -12718,8 +12650,8 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) // This updates the "Slice now", "Export G-code", "Arrange" buttons status. // Namely, it refreshes the "Out of print bed" property of all the ModelObjects, and it enables // the "Slice now" and "Export G-code" buttons based on their "out of bed" status. - // BBS: remove this update here, will be updated in update_fff_scene later - // this->object_list_changed(); + //BBS: remove this update here, will be updated in update_fff_scene later + //this->object_list_changed(); // refresh preview if (view3D->is_dragging()) // updating scene now would interfere with the gizmo dragging @@ -12728,27 +12660,28 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) if (this->printer_technology == ptFFF) { if (is_finished) this->update_fff_scene(); - } else + } + else this->update_sla_scene(); } - // BBS: add slice&&print status update logic + //BBS: add slice&&print status update logic if (evt.cancelled()) { /*if (wxGetApp().get_mode() == comSimple) sidebar->set_btn_label(ActionButtonType::abReslice, "Slice now"); show_action_buttons(true);*/ ready_to_slice = true; - // this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true, true); + //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true, true); - // BBS + //BBS if (m_is_publishing) { m_publish_dlg->cancel(); } } else { - if ((ready_to_slice) || (wxGetApp().get_mode() == comSimple)) { - // this means the current plate is not the slicing plate - // show_action_buttons(ready_to_slice); - // this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); + if((ready_to_slice) || (wxGetApp().get_mode() == comSimple)) { + //this means the current plate is not the slicing plate + //show_action_buttons(ready_to_slice); + //this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); } if (exporting_status != ExportingStatus::NOT_EXPORTING && !has_error) { notification_manager->stop_delayed_notifications_of_type(NotificationType::ExportOngoing); @@ -12756,14 +12689,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) } // If writing to removable drive was scheduled, show notification with eject button if (exporting_status == ExportingStatus::EXPORTING_TO_REMOVABLE && !has_error) { - // show_action_buttons(ready_to_slice); + //show_action_buttons(ready_to_slice); this->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, ready_to_slice, true); - notification_manager - ->push_exporting_finished_notification(last_output_path, last_output_dir_path, - // Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it. - platform_flavor() != PlatformFlavor::LinuxOnChromium); + notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, + // Don't offer the "Eject" button on ChromeOS, the Linux side has no control over it. + platform_flavor() != PlatformFlavor::LinuxOnChromium); wxGetApp().removable_drive_manager()->set_exporting_finished(true); - } else if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error) + }else + if (exporting_status == ExportingStatus::EXPORTING_TO_LOCAL && !has_error) notification_manager->push_exporting_finished_notification(last_output_path, last_output_dir_path, false); // BBS, Generate calibration thumbnail for current plate @@ -12773,19 +12706,20 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) preview->reload_print(); ThumbnailData* calibration_data = &partplate_list.get_curr_plate()->cali_thumbnail_data; const ThumbnailsParams calibration_params = { {}, false, true, true, true, partplate_list.get_curr_plate_index() }; - generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, - calibration_params); preview->get_canvas3d()->reset_gcode_toolpaths();*/ + generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); + preview->get_canvas3d()->reset_gcode_toolpaths();*/ // generate bbox data PlateBBoxData* plate_bbox_data = &partplate_list.get_curr_plate()->cali_bboxes_data; - *plate_bbox_data = generate_first_layer_bbox(); + *plate_bbox_data = generate_first_layer_bbox(); } } exporting_status = ExportingStatus::NOT_EXPORTING; + // BBS stop publishing if error occur - // if (m_is_publishing) { + //if (m_is_publishing) { // GCodeProcessorResult *gcode_result = background_process.get_current_gcode_result(); // m_publish_dlg->UpdateStatus(_L("Error occurred during slicing"), -1, false); // // if toolpath is outside @@ -12794,7 +12728,9 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) // } //} - if (is_finished) { + + if (is_finished) + { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":finished, reload print soon"); m_is_slicing = false; this->preview->reload_print(false); @@ -12810,9 +12746,10 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) } } q->SetDropTarget(new PlaterDropTarget(*main_frame, *q)); - } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(":slicing all, plate %1% finished, start next slice...") % m_cur_slice_plate; + } + else + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% finished, start next slice...")%m_cur_slice_plate; m_cur_slice_plate++; q->Freeze(); @@ -12820,11 +12757,10 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) partplate_list.select_plate_view(); int ret = q->start_next_slice(); if (ret) { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(":slicing all, plate %1% can not be sliced, will stop") % m_cur_slice_plate; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(":slicing all, plate %1% can not be sliced, will stop")%m_cur_slice_plate; m_is_slicing = false; } - // not the last plate + //not the last plate update_fff_scene_only_shells(); q->Thaw(); if (m_is_publishing) { @@ -12846,13 +12782,13 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent& evt) void Plater::priv::on_action_add(SimpleEvent&) { if (q != nullptr) { - // q->add_model(); - // BBS open file in toolbar add + //q->add_model(); + //BBS open file in toolbar add q->add_file(); } } -// BBS: add plate from toolbar +//BBS: add plate from toolbar void Plater::priv::on_action_add_plate(SimpleEvent&) { if (q != nullptr) { @@ -12863,22 +12799,22 @@ void Plater::priv::on_action_add_plate(SimpleEvent&) update(); // BBS set default view - // q->get_camera().select_view("topfront"); + //q->get_camera().select_view("topfront"); q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; } } -// BBS: remove plate from toolbar +//BBS: remove plate from toolbar void Plater::priv::on_action_del_plate(SimpleEvent&) { if (q != nullptr) { q->delete_plate(); - // q->get_camera().select_view("topfront"); - // q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + //q->get_camera().select_view("topfront"); + //q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; } } -// BBS: GUI refactor: GLToolbar +//BBS: GUI refactor: GLToolbar void Plater::priv::on_action_open_project(SimpleEvent&) { if (q != nullptr) { @@ -12886,16 +12822,16 @@ void Plater::priv::on_action_open_project(SimpleEvent&) } } -// BBS: GUI refactor: slice plate +//BBS: GUI refactor: slice plate void Plater::priv::on_action_slice_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n"; - // BBS update extruder params and speed table before slicing + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice plate event\n" ; + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); @@ -12905,33 +12841,33 @@ void Plater::priv::on_action_slice_plate(SimpleEvent&) } } -// BBS: GUI refactor: slice all +//BBS: GUI refactor: slice all void Plater::priv::on_action_slice_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n"; - // BBS update extruder params and speed table before slicing + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received slice project event\n" ; + //BBS update extruder params and speed table before slicing const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config(); - auto& print = q->get_partplate_list().get_current_fff_print(); - auto print_config = print.config(); - int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); + auto& print = q->get_partplate_list().get_current_fff_print(); + auto print_config = print.config(); + int numExtruders = wxGetApp().preset_bundle->filament_presets.size(); Model::setExtruderParams(config, numExtruders); Model::setPrintSpeedTable(config, print_config); - m_slice_all = true; + m_slice_all = true; m_slice_all_only_has_gcode = true; - m_cur_slice_plate = 0; - // select plate + m_cur_slice_plate = 0; + //select plate q->select_plate(m_cur_slice_plate); q->reslice(); if (!m_is_publishing) q->select_view_3D("Preview"); - // BBS: wish to select all plates stats item + //BBS: wish to select all plates stats item preview->get_canvas3d()->_update_select_plate_toolbar_stats_item(true); } } -void Plater::priv::on_action_publish(wxCommandEvent& event) +void Plater::priv::on_action_publish(wxCommandEvent &event) { if (q != nullptr) { if (event.GetInt() == EVT_PUBLISHING_START) { @@ -12961,7 +12897,7 @@ void Plater::priv::on_action_publish(wxCommandEvent& event) void Plater::priv::on_action_print_plate(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n" ; } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; @@ -12996,7 +12932,7 @@ void Plater::priv::on_action_print_plate_from_sdcard(SimpleEvent&) BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print plate event\n"; } - // BBS + //BBS open_machine_select_dialog(0, PrintFromType::FROM_SDCARD_VIEW); } @@ -13015,11 +12951,12 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) return; } const wxString new_name = main_frame->m_tabpanel->GetPageName(new_sel); - sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; + sidebar_layout.show = new_name == TAB_ID_PREPARE || new_name == TAB_ID_PREVIEW; update_sidebar(); - int old_sel = e.GetOldSelection(); - const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); - const bool use_native_device_tab = wxGetApp().preset_bundle && (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); + int old_sel = e.GetOldSelection(); + const bool use_printer_agents = wxGetApp().app_config->get_bool("use_printer_agents"); + const bool use_native_device_tab = wxGetApp().preset_bundle && + (wxGetApp().preset_bundle->use_bbl_device_tab() || use_printer_agents); if (use_native_device_tab && new_name == TAB_ID_MONITOR) { // BBL network module is only required for BBL-vendor printers. // Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it. @@ -13036,12 +12973,12 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) // while the native Device tab holds TAB_ID_MONITOR, and in legacy-web mode it holds // TAB_ID_MONITOR itself. const bool selecting_web_device_tab = main_frame->m_printer_view && - main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; + main_frame->m_tabpanel->GetPage(new_sel) == main_frame->m_printer_view; if (selecting_web_device_tab) { // Use the selected discovered machine when the preset has no host. main_frame->load_printer_url(); } else if (new_name == TAB_ID_MONITOR && wxGetApp().preset_bundle != nullptr) { - auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; wxString url = from_u8(PrintHost::get_print_host_webui(&cfg)); if (main_frame->m_printer_view && url.empty()) { // It's missing_connection page, reload so that we can replay the gif image @@ -13051,34 +12988,30 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e) } } -int Plater::priv::update_print_required_data(Slic3r::DynamicPrintConfig config, - Slic3r::Model model, - Slic3r::PlateDataPtrs plate_data_list, - std::string file_name, - std::string file_path) +int Plater::priv::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) { - if (!m_select_machine_dlg) - m_select_machine_dlg = new SelectMachineDialog(q); + if (!m_select_machine_dlg) m_select_machine_dlg = new SelectMachineDialog(q); return m_select_machine_dlg->update_print_required_data(config, model, plate_data_list, file_name, file_path); } void Plater::priv::on_action_send_to_printer(bool isall) { - if (!m_send_to_sdcard_dlg) - m_send_to_sdcard_dlg = new SendToPrinterDialog(q); + if (!m_send_to_sdcard_dlg) m_send_to_sdcard_dlg = new SendToPrinterDialog(q); if (isall) { m_send_to_sdcard_dlg->prepare(PLATE_ALL_IDX); - } else { + } + else { m_send_to_sdcard_dlg->prepare(partplate_list.get_curr_plate_index()); } - m_send_to_sdcard_dlg->ShowModal(); + m_send_to_sdcard_dlg->ShowModal(); } -void Plater::priv::on_action_select_sliced_plate(wxCommandEvent& evt) + +void Plater::priv::on_action_select_sliced_plate(wxCommandEvent &evt) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received select sliced plate event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received select sliced plate event\n" ; } bool skip_zoom = evt.GetExtraLong() == 1; q->select_sliced_plate(evt.GetInt(), skip_zoom); @@ -13087,7 +13020,7 @@ void Plater::priv::on_action_select_sliced_plate(wxCommandEvent& evt) void Plater::priv::on_action_print_all(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print all event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received print all event\n" ; } PresetBundle& preset_bundle = *wxGetApp().preset_bundle; @@ -13101,7 +13034,7 @@ void Plater::priv::on_action_print_all(SimpleEvent&) void Plater::priv::on_action_export_gcode(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; q->export_gcode(false); } } @@ -13109,7 +13042,7 @@ void Plater::priv::on_action_export_gcode(SimpleEvent&) void Plater::priv::on_action_send_gcode(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export gcode event\n" ; q->send_gcode_legacy(); } } @@ -13117,12 +13050,12 @@ void Plater::priv::on_action_send_gcode(SimpleEvent&) void Plater::priv::on_action_export_sliced_file(SimpleEvent&) { if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n" ; q->export_gcode_3mf(); } } -void Plater::priv::on_action_export_all_sliced_file(SimpleEvent&) +void Plater::priv::on_action_export_all_sliced_file(SimpleEvent &) { if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export all sliced file event\n"; @@ -13132,10 +13065,10 @@ void Plater::priv::on_action_export_all_sliced_file(SimpleEvent&) void Plater::priv::on_action_export_to_sdcard(SimpleEvent&) { - if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; - q->send_to_printer(); - } + if (q != nullptr) { + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received export sliced file event\n"; + q->send_to_printer(); + } } void Plater::priv::on_action_export_to_sdcard_all(SimpleEvent&) @@ -13146,16 +13079,16 @@ void Plater::priv::on_action_export_to_sdcard_all(SimpleEvent&) } } -// BBS: add plate select logic +//BBS: add plate select logic void Plater::priv::on_plate_selected(SimpleEvent&) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received plate selected event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received plate selected event\n" ; sidebar->obj_list()->on_plate_selected(partplate_list.get_curr_plate_index()); } void Plater::priv::on_action_request_model_id(wxCommandEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received import model id event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received import model id event\n" ; if (q != nullptr) { q->import_model_id(evt.GetString()); } @@ -13163,23 +13096,29 @@ void Plater::priv::on_action_request_model_id(wxCommandEvent& evt) void Plater::priv::on_action_download_project(wxCommandEvent& evt) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received download project event\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":received download project event\n" ; if (q != nullptr) { q->download_project(evt.GetString()); } } -// BBS: add slice button status update logic +//BBS: add slice button status update logic void Plater::priv::on_slice_button_status(bool enable) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": enable = " << enable << "\n"; + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": enable = "<update_slice_print_status(MainFrame::eEventObjectUpdate, enable); } -void Plater::priv::on_action_split_objects(SimpleEvent&) { split_object(); } +void Plater::priv::on_action_split_objects(SimpleEvent&) +{ + split_object(); +} -void Plater::priv::on_action_split_volumes(SimpleEvent&) { split_volume(); } +void Plater::priv::on_action_split_volumes(SimpleEvent&) +{ + split_volume(); +} void Plater::priv::on_object_select(SimpleEvent& evt) { @@ -13190,13 +13129,16 @@ void Plater::priv::on_object_select(SimpleEvent& evt) selection_changed(); } -// BBS: repair model through cgal -void Plater::priv::on_repair_model(wxCommandEvent& event) { wxGetApp().obj_list()->fix_through_cgal(); } - -void Plater::priv::on_filament_color_changed(wxCommandEvent& event) +//BBS: repair model through cgal +void Plater::priv::on_repair_model(wxCommandEvent &event) { - // q->update_all_plate_thumbnails(true); - // q->get_preview_canvas3D()->update_plate_thumbnails(); + wxGetApp().obj_list()->fix_through_cgal(); +} + +void Plater::priv::on_filament_color_changed(wxCommandEvent &event) +{ + //q->update_all_plate_thumbnails(true); + //q->get_preview_canvas3D()->update_plate_thumbnails(); int modify_id = event.GetInt(); auto& ams_multi_color_filment = wxGetApp().preset_bundle->ams_multi_color_filment; @@ -13211,17 +13153,17 @@ void Plater::priv::on_filament_color_changed(wxCommandEvent& event) sidebar->update_mixed_filament_list(); } -void Plater::priv::install_network_plugin(wxCommandEvent& event) +void Plater::priv::install_network_plugin(wxCommandEvent &event) { wxGetApp().ShowDownNetPluginDlg(); return; } -void Plater::priv::update_plugin_when_launch(wxCommandEvent& event) +void Plater::priv::update_plugin_when_launch(wxCommandEvent &event) { std::string data_dir_str = data_dir(); boost::filesystem::path data_dir_path(data_dir_str); - auto cache_folder = data_dir_path / "ota"; + auto cache_folder = data_dir_path / "ota"; std::string changelog_file = cache_folder.string() + "/plugins/network_plugins.json"; UpdatePluginDialog dlg(wxGetApp().mainframe); @@ -13229,8 +13171,7 @@ void Plater::priv::update_plugin_when_launch(wxCommandEvent& event) auto result = dlg.ShowModal(); auto app_config = wxGetApp().app_config; - if (!app_config) - return; + if (!app_config) return; if (result == wxID_OK) { // Apply the downloaded update right away and hot-reload the plug-in, the same @@ -13241,36 +13182,32 @@ void Plater::priv::update_plugin_when_launch(wxCommandEvent& event) notification_manager->close_notification_of_type(NotificationType::BBLPluginUpdateAvailable); app_config->set("update_network_plugin", "false"); if (wxGetApp().hot_reload_network_plugin()) { - MessageDialog dlg_ok(wxGetApp().mainframe, _L("Network plug-in switched successfully."), _L("Success"), - wxOK | wxICON_INFORMATION); + MessageDialog dlg_ok(wxGetApp().mainframe, _L("Network plug-in switched successfully."), _L("Success"), wxOK | wxICON_INFORMATION); dlg_ok.ShowModal(); } else { - MessageDialog dlg_fail(wxGetApp().mainframe, _L("Failed to load network plug-in. Please restart the application."), - _L("Restart Required"), wxOK | wxICON_WARNING); + MessageDialog dlg_fail(wxGetApp().mainframe, _L("Failed to load network plug-in. Please restart the application."), _L("Restart Required"), wxOK | wxICON_WARNING); dlg_fail.ShowModal(); } } else { app_config->set("update_network_plugin", had_cache ? "true" : "false"); } - } else if (result == wxID_NO) { + } + else if (result == wxID_NO) { app_config->set("update_network_plugin", "false"); } } -void Plater::priv::show_install_plugin_hint(wxCommandEvent& event) +void Plater::priv::show_install_plugin_hint(wxCommandEvent &event) { - notification_manager->bbl_show_plugin_install_notification( - into_u8(_L("The network plug-in was not detected. Network related features are unavailable."))); + notification_manager->bbl_show_plugin_install_notification(into_u8(_L("The network plug-in was not detected. Network related features are unavailable."))); } -void Plater::priv::show_preview_only_hint(wxCommandEvent& event) +void Plater::priv::show_preview_only_hint(wxCommandEvent &event) { - notification_manager->bbl_show_preview_only_notification( - into_u8(_L("Preview only mode:\nThe loaded file contains G-code only, cannot enter the Prepare page."))); + notification_manager->bbl_show_preview_only_notification(into_u8(_L("Preview only mode:\nThe loaded file contains G-code only, cannot enter the Prepare page."))); } -void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) -{ +void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) { m_is_dark = wxSystemSettings::GetAppearance().IsDark(); if (view3D->get_canvas3d() && view3D->get_canvas3d()->is_initialized()) { view3D->get_canvas3d()->on_change_color_mode(m_is_dark); @@ -13281,25 +13218,22 @@ void Plater::priv::on_apple_change_color_mode(wxSysColourChangedEvent& evt) apply_color_mode(); } -void Plater::priv::on_change_color_mode(SimpleEvent& evt) -{ +void Plater::priv::on_change_color_mode(SimpleEvent& evt) { m_is_dark = wxGetApp().app_config->get("dark_color_mode") == "1"; sidebar->on_change_color_mode(m_is_dark); view3D->get_canvas3d()->on_change_color_mode(m_is_dark); preview->get_canvas3d()->on_change_color_mode(m_is_dark); assemble_view->get_canvas3d()->on_change_color_mode(m_is_dark); - if (m_send_to_sdcard_dlg) - m_send_to_sdcard_dlg->on_change_color_mode(); + if (m_send_to_sdcard_dlg) m_send_to_sdcard_dlg->on_change_color_mode(); apply_color_mode(); } void Plater::priv::apply_color_mode() { - const bool is_dark = wxGetApp().dark_mode(); - wxColour orca_color = wxColour(59, 68, - 70); // wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar()); - orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color); + const bool is_dark = wxGetApp().dark_mode(); + wxColour orca_color = wxColour(59, 68, 70);//wxColour(ColorRGBA::ORCA().r_uchar(), ColorRGBA::ORCA().g_uchar(), ColorRGBA::ORCA().b_uchar()); + orca_color = is_dark ? StateColor::darkModeColorFor(orca_color) : StateColor::lightModeColorFor(orca_color); wxColour sash_color = is_dark ? wxColour(38, 46, 48) : wxColour(206, 206, 206); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_COLOUR, sash_color); m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_INACTIVE_CAPTION_TEXT_COLOUR, *wxWHITE); @@ -13307,8 +13241,7 @@ void Plater::priv::apply_color_mode() m_aui_mgr.GetArtProvider()->SetColour(wxAUI_DOCKART_BORDER_COLOUR, is_dark ? *wxBLACK : wxColour(165, 165, 165)); } -static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) -{ +static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x, int& y) { int res_x = 0, res_y = 0; while (child != until_parent && child != nullptr) { @@ -13324,7 +13257,7 @@ static void get_position(wxWindowBase* child, wxWindowBase* until_parent, int& x y = res_y; } -void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu* menu) +void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu *menu) { // BBS: GUI refactor: move sidebar to the left int x, y; @@ -13335,7 +13268,7 @@ void Plater::priv::show_right_click_menu(Vec2d mouse_position, wxMenu* menu) // specified (even though the position is sane). position = wxDefaultPosition; #endif - GLCanvas3D& canvas = *q->canvas3D(); + GLCanvas3D &canvas = *q->canvas3D(); canvas.apply_retina_scale(mouse_position); canvas.set_popup_menu_position(mouse_position); q->PopupMenu(menu, position); @@ -13348,19 +13281,22 @@ void Plater::priv::on_right_click(RBtnEvent& evt) wxMenu* menu = nullptr; - if (obj_idx == -1) { // no one or several object are selected - if (evt.data.second) { // right button was clicked on empty space + if (obj_idx == -1) { // no one or several object are selected + if (evt.data.second) { // right button was clicked on empty space if (!get_selection().is_empty()) // several objects are selected in 3DScene return; menu = menus.default_menu(); - } else { + } + else { if (current_panel == assemble_view) { menu = menus.assemble_multi_selection_menu(); - } else { + } + else { menu = menus.multi_selection_menu(); } } - } else { + } + else { // If in 3DScene is(are) selected volume(s), but right button was clicked on empty space if (evt.data.second) return; @@ -13374,24 +13310,24 @@ void Plater::priv::on_right_click(RBtnEvent& evt) else { const Selection& selection = get_selection(); // show "Object menu" for each one or several FullInstance instead of FullObject - const bool is_some_full_instances = selection.is_single_full_instance() || selection.is_single_full_object() || + const bool is_some_full_instances = selection.is_single_full_instance() || + selection.is_single_full_object() || selection.is_multiple_full_instance(); - const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); + const bool is_part = selection.is_single_volume() || selection.is_single_modifier(); - // BBS get assemble view menu + //BBS get assemble view menu if (current_panel == assemble_view) { - menu = is_some_full_instances ? menus.assemble_object_menu() : - is_part ? menus.assemble_part_menu() : - menus.assemble_multi_selection_menu(); + menu = is_some_full_instances ? menus.assemble_object_menu() : + is_part ? menus.assemble_part_menu() : menus.assemble_multi_selection_menu(); } else { if (is_some_full_instances) menu = printer_technology == ptSLA ? menus.sla_object_menu() : menus.object_menu(); else if (is_part) { - const GLVolume* gl_volume = selection.get_first_volume(); - const ModelVolume* model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); - menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : - (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : - menus.part_menu(); + const GLVolume* gl_volume = selection.get_first_volume(); + const ModelVolume *model_volume = get_model_volume(*gl_volume, selection.get_model()->objects); + menu = (model_volume != nullptr && model_volume->is_text()) ? menus.text_part_menu() : + (model_volume != nullptr && model_volume->is_svg()) ? menus.svg_part_menu() : + menus.part_menu(); } else menu = menus.multi_selection_menu(); } @@ -13403,10 +13339,10 @@ void Plater::priv::on_right_click(RBtnEvent& evt) } } -// BBS: add part plate related logic +//BBS: add part plate related logic void Plater::priv::on_plate_right_click(RBtnPlateEvent& evt) { - wxMenu* menu = menus.plate_menu(); + wxMenu *menu = menus.plate_menu(); show_right_click_menu(evt.data.first, menu); } @@ -13415,7 +13351,10 @@ void Plater::priv::on_update_geometry(Vec3dsEvent<2>&) // TODO } -void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&) { view3D->get_canvas3d()->reset_sequential_print_clearance(); } +void Plater::priv::on_3dcanvas_mouse_dragging_started(SimpleEvent&) +{ + view3D->get_canvas3d()->reset_sequential_print_clearance(); +} // Update the scene from the background processing, // if the update message was received during mouse manipulation. @@ -13426,21 +13365,23 @@ void Plater::priv::on_3dcanvas_mouse_dragging_finished(SimpleEvent&) update_sla_scene(); } - // partplate_list.reload_all_objects(); + //partplate_list.reload_all_objects(); } -// BBS: add plate id for thumbnail generate param -void Plater::priv::generate_thumbnail(ThumbnailData& data, - unsigned int w, - unsigned int h, - const ThumbnailsParams& thumbnail_params, - Camera::EType camera_type, - Camera::ViewAngleType camera_view_angle_type, - bool for_picking, - bool ban_light) -{ view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, camera_view_angle_type, for_picking, ban_light); } +//BBS: add plate id for thumbnail generate param +void Plater::priv::generate_thumbnail(ThumbnailData & data, + unsigned int w, + unsigned int h, + const ThumbnailsParams &thumbnail_params, + Camera::EType camera_type, + Camera::ViewAngleType camera_view_angle_type, + bool for_picking, + bool ban_light) +{ + view3D->get_canvas3d()->render_thumbnail(data, w, h, thumbnail_params, camera_type, camera_view_angle_type, for_picking, ban_light); +} -// BBS: add plate id for thumbnail generate param +//BBS: add plate id for thumbnail generate param ThumbnailsList Plater::priv::generate_thumbnails(const ThumbnailsParams& params, Camera::EType camera_type) { ThumbnailsList thumbnails; @@ -13459,48 +13400,49 @@ PlateBBoxData Plater::priv::generate_first_layer_bbox() PlateBBoxData bboxdata; std::vector& id_bboxes = bboxdata.bbox_objs; BoundingBoxf bbox_all; - auto print = this->background_process.m_fff_print; - auto curr_plate = this->partplate_list.get_curr_plate(); - auto curr_plate_seq = curr_plate->get_real_print_seq(); - bboxdata.is_seq_print = (curr_plate_seq == PrintSequence::ByObject); - bboxdata.first_extruder = print->get_tool_ordering().first_extruder(); - bboxdata.bed_type = bed_type_to_gcode_string(print->config().curr_bed_type.value); + auto print = this->background_process.m_fff_print; + auto curr_plate = this->partplate_list.get_curr_plate(); + auto curr_plate_seq = curr_plate->get_real_print_seq(); + bboxdata.is_seq_print = (curr_plate_seq == PrintSequence::ByObject); + bboxdata.first_extruder = print->get_tool_ordering().first_extruder(); + bboxdata.bed_type = bed_type_to_gcode_string(print->config().curr_bed_type.value); bboxdata.first_layer_time = partplate_list.get_curr_plate()->get_slice_result()->initial_layer_time; // get nozzle diameter auto opt_nozzle_diameters = print->config().option("nozzle_diameter"); if (opt_nozzle_diameters != nullptr) bboxdata.nozzle_diameter = float(opt_nozzle_diameters->get_at(bboxdata.first_extruder)); - // PrintObjectPtrs objects; - // if (this->printer_technology == ptFFF) { - // objects = this->background_process.m_fff_print->objects().vector(); - // } - // else { - // objects = this->background_process.m_sla_print->objects(); - // } + //PrintObjectPtrs objects; + //if (this->printer_technology == ptFFF) { + // objects = this->background_process.m_fff_print->objects().vector(); + //} + //else { + // objects = this->background_process.m_sla_print->objects(); + //} auto objects = print->objects(); - auto orig = this->partplate_list.get_curr_plate()->get_origin(); - Vec2d orig2d = {orig[0], orig[1]}; + auto orig = this->partplate_list.get_curr_plate()->get_origin(); + Vec2d orig2d = { orig[0], orig[1] }; BBoxData data; - for (auto obj : objects) { + for (auto obj : objects) + { auto bb_scaled = obj->get_first_layer_bbox(data.area, data.layer_height, data.name); - auto bb = unscaled(bb_scaled); + auto bb = unscaled(bb_scaled); bb.min -= orig2d; bb.max -= orig2d; bbox_all.merge(bb); data.area *= (SCALING_FACTOR * SCALING_FACTOR); // unscale area - data.id = obj->id().id; - data.bbox = {bb.min.x(), bb.min.y(), bb.max.x(), bb.max.y()}; + data.id = obj->id().id; + data.bbox = { bb.min.x(),bb.min.y(),bb.max.x(),bb.max.y() }; id_bboxes.emplace_back(data); } // add wipe tower bounding box if (print->has_wipe_tower()) { - auto wt_corners = print->first_layer_wipe_tower_corners(); + auto wt_corners = print->first_layer_wipe_tower_corners(); // when loading gcode.3mf, wipe tower info may not be correct if (!wt_corners.empty()) { BoundingBox bb_scaled = {wt_corners[0], wt_corners[2]}; - auto bb = unscaled(bb_scaled); + auto bb = unscaled(bb_scaled); bb.min -= orig2d; bb.max -= orig2d; bbox_all.merge(bb); @@ -13511,7 +13453,7 @@ PlateBBoxData Plater::priv::generate_first_layer_bbox() } } - bboxdata.bbox_all = {bbox_all.min.x(), bbox_all.min.y(), bbox_all.max.x(), bbox_all.max.y()}; + bboxdata.bbox_all = { bbox_all.min.x(),bbox_all.min.y(),bbox_all.max.x(),bbox_all.max.y() }; return bboxdata; } @@ -13540,8 +13482,7 @@ wxString Plater::priv::get_export_gcode_filename(const wxString& extension, bool auto full_filename = m_project_folder / std::string((m_project_name + extension).mb_str(wxConvUTF8)); return from_path(full_filename); } else { - auto full_filename = m_project_folder / - std::string((m_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); + auto full_filename = m_project_folder / std::string((m_project_name + from_u8(plate_index_str) + extension).mb_str(wxConvUTF8)); return from_path(full_filename); } } else { @@ -13552,26 +13493,30 @@ wxString Plater::priv::get_export_gcode_filename(const wxString& extension, bool } } else { if (only_filename) { - if (!model.objects.empty() && m_project_name == _L("Untitled")) + if(!model.objects.empty() && m_project_name == _L("Untitled")) return wxString(fs::path(model.objects.front()->name).replace_extension().c_str()) + from_u8(plate_index_str) + extension; if (export_all) return m_project_name + extension; else return m_project_name + from_u8(plate_index_str) + extension; - } else + } + else return ""; } } -wxString Plater::priv::get_project_name() { return m_project_name; } +wxString Plater::priv::get_project_name() +{ + return m_project_name; +} -// BBS +//BBS void Plater::priv::set_project_name(const wxString& project_name) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project is:" << project_name; m_project_name = project_name; - // update topbar title + //update topbar title #ifdef __APPLE__ wxGetApp().mainframe->SetTitle(m_project_name); if (!m_project_name.IsEmpty()) @@ -13599,30 +13544,30 @@ void Plater::priv::update_title_dirty_status() #else wxGetApp().mainframe->SetTitle(title + " - OrcaSlicer"); wxGetApp().mainframe->topbar()->SetTitle(title); -#endif +#endif } void Plater::priv::set_project_filename(const wxString& filename) { boost::filesystem::path full_path = into_path(filename); - boost::filesystem::path ext = full_path.extension(); - // if (boost::iequals(ext.string(), ".amf")) { - // // Remove the first extension. - // full_path.replace_extension(""); - // // It may be ".zip.amf". - // if (boost::iequals(full_path.extension().string(), ".zip")) - // // Remove the 2nd extension. - // full_path.replace_extension(""); - // } else { - // // Remove just one extension. - // full_path.replace_extension(""); - // } + boost::filesystem::path ext = full_path.extension(); + //if (boost::iequals(ext.string(), ".amf")) { + // // Remove the first extension. + // full_path.replace_extension(""); + // // It may be ".zip.amf". + // if (boost::iequals(full_path.extension().string(), ".zip")) + // // Remove the 2nd extension. + // full_path.replace_extension(""); + //} else { + // // Remove just one extension. + // full_path.replace_extension(""); + //} full_path.replace_extension(""); m_project_folder = full_path.parent_path(); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " project folder is:" << m_project_folder.string(); - // BBS + //BBS wxString project_name = from_u8(full_path.filename().string()); set_project_name(project_name); // record filename for hint when open exported file/.gcode @@ -13654,14 +13599,14 @@ void Plater::priv::init_notification_manager() notification_manager->init_progress_indicator(); } -void Plater::priv::update_objects_position_when_select_preset(const std::function& select_prest) +void Plater::priv::update_objects_position_when_select_preset(const std::function &select_prest) { select_prest(); wxGetApp().obj_list()->update_object_list_by_printer_technology(); // Re-clamp wipe tower positions to new bed boundaries after preset change - PartPlateList& cur_plate_list = this->partplate_list; + PartPlateList &cur_plate_list = this->partplate_list; for (size_t plate_id = 0; plate_id < cur_plate_list.get_plate_list().size(); ++plate_id) { cur_plate_list.set_default_wipe_tower_pos_for_plate(plate_id); } @@ -13671,23 +13616,29 @@ void Plater::priv::update_objects_position_when_select_preset(const std::functio void Plater::orient() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Orient")); replace_job(w, std::make_unique()); } } -// BBS: add job state related functions -void Plater::set_prepare_state(int state) { p->m_job_prepare_state = state; } +//BBS: add job state related functions +void Plater::set_prepare_state(int state) +{ + p->m_job_prepare_state = state; +} -int Plater::get_prepare_state() { return p->m_job_prepare_state; } +int Plater::get_prepare_state() +{ + return p->m_job_prepare_state; +} void Plater::get_print_job_data(PrintPrepareData* data) { if (data) { - data->plate_idx = p->m_print_job_data.plate_idx; - data->_3mf_path = p->m_print_job_data._3mf_path; + data->plate_idx = p->m_print_job_data.plate_idx; + data->_3mf_path = p->m_print_job_data._3mf_path; data->_3mf_config_path = p->m_print_job_data._3mf_config_path; } } @@ -13696,18 +13647,32 @@ void Plater::set_print_job_plate_idx(int plate_idx) { if (plate_idx == PLATE_CURRENT_IDX) { p->m_print_job_data.plate_idx = get_partplate_list().get_curr_plate_index(); - } else { + } + else { p->m_print_job_data.plate_idx = plate_idx; } } -int Plater::get_send_calibration_finished_event() { return EVT_SEND_CALIBRATION_FINISHED; } -int Plater::get_print_finished_event() { return EVT_PRINT_FINISHED; } +int Plater::get_send_calibration_finished_event() +{ + return EVT_SEND_CALIBRATION_FINISHED; +} -int Plater::get_send_finished_event() { return EVT_SEND_FINISHED; } +int Plater::get_print_finished_event() +{ + return EVT_PRINT_FINISHED; +} -int Plater::get_publish_finished_event() { return EVT_PUBLISH_FINISHED; } +int Plater::get_send_finished_event() +{ + return EVT_SEND_FINISHED; +} + +int Plater::get_publish_finished_event() +{ + return EVT_PUBLISH_FINISHED; +} void Plater::priv::set_current_canvas_as_dirty() { @@ -13727,10 +13692,10 @@ GLCanvas3D* Plater::priv::get_current_canvas3D(bool exclude_preview) return preview->get_canvas3d(); else if (current_panel == assemble_view) return assemble_view->get_canvas3d(); - else // BBS default set to view3D + else //BBS default set to view3D return view3D->get_canvas3d(); - // return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr); + //return (current_panel == view3D) ? view3D->get_canvas3d() : ((current_panel == preview) ? preview->get_canvas3d() : nullptr); } void Plater::priv::unbind_canvas_event_handlers() @@ -13756,7 +13721,7 @@ void Plater::priv::reset_canvas_volumes() bool Plater::priv::check_ams_status_impl(bool is_slice_all) { - Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return true; @@ -13766,9 +13731,9 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) if (q->is_gcode_3mf() || q->only_gcode_mode() || q->get_partplate_list().get_curr_plate()->get_objects().empty()) { return true; } - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type()) { - bool is_same_as_printer = true; + bool is_same_as_printer = true; auto nozzle_volumes_values = preset_bundle->project_config.option("nozzle_volume_type")->values; assert(obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2); if (obj->GetExtderSystem()->GetTotalExtderCount() == 2 && nozzle_volumes_values.size() == 2) { @@ -13778,14 +13743,13 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) // After device sync, extruder_nozzle_stats matches printer → dialog suppressed. // Reference to BBS: BambuStudio/src/slic3r/GUI/Plater.cpp is_extruder_stat_synced() using namespace MultiNozzleUtils; - auto nozzle_diameter_values = - preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")->values; + auto nozzle_diameter_values = preset_bundle->printers.get_edited_preset().config.option("nozzle_diameter")->values; // Build preset nozzle groups from extruder_nozzle_stats config std::vector> preset_nozzle_infos(nozzle_diameter_values.size()); for (size_t extruder_id = 0; extruder_id < nozzle_diameter_values.size(); ++extruder_id) { NozzleVolumeType preset_volume_type = NozzleVolumeType(nozzle_volumes_values[extruder_id]); - std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]); + std::string preset_diameter = format_diameter_to_str(nozzle_diameter_values[extruder_id]); if (preset_volume_type == nvtHybrid) { // Hybrid: expand into separate groups for each nozzle type from stats @@ -13808,9 +13772,9 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) for (const auto& preset_group : preset_groups) { if (preset_group.nozzle_count == 0) { // Never synced: if printer has nozzles of this type → needs sync - if (std::find_if(printer_groups.begin(), printer_groups.end(), [&preset_group](const NozzleGroupInfo& elem) { - return preset_group.is_same_type(elem); - }) != printer_groups.end()) { + if (std::find_if(printer_groups.begin(), printer_groups.end(), + [&preset_group](const NozzleGroupInfo& elem) { return preset_group.is_same_type(elem); }) + != printer_groups.end()) { is_same_as_printer = false; break; } @@ -13852,22 +13816,21 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) } if (!preset_bundle->extruder_ams_counts.empty() && !preset_bundle->extruder_ams_counts.front().empty()) { - is_same_as_printer &= preset_bundle->extruder_ams_counts[0][4] == left_4 && - preset_bundle->extruder_ams_counts[0][1] == left_1 && - preset_bundle->extruder_ams_counts[1][4] == right_4 && - preset_bundle->extruder_ams_counts[1][1] == right_1; + is_same_as_printer &= preset_bundle->extruder_ams_counts[0][4] == left_4 + && preset_bundle->extruder_ams_counts[0][1] == left_1 + && preset_bundle->extruder_ams_counts[1][4] == right_4 + && preset_bundle->extruder_ams_counts[1][1] == right_1; } if (!is_same_as_printer) { struct SyncInfoDialog : MessageDialog { - SyncInfoDialog(wxWindow* parent) + SyncInfoDialog(wxWindow *parent) : MessageDialog(parent, _L("The nozzle type and AMS quantity information has not been synced from the connected printer.\n" "After syncing, software can optimize printing time and filament usage when slicing.\n" "Would you like to sync now?"), - _L("Warning"), - 0) + _L("Warning"), 0) { add_button(wxID_YES, true, _L("Sync now")); add_button(wxID_NO, true, _L("Later")); @@ -13892,7 +13855,7 @@ bool Plater::priv::check_ams_status_impl(bool is_slice_all) bool Plater::priv::get_machine_sync_status() { - Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); + Slic3r::DeviceManager *dev = Slic3r::GUI::wxGetApp().getDeviceManager(); if (!dev) return false; @@ -13900,7 +13863,7 @@ bool Plater::priv::get_machine_sync_status() if (!obj) return false; - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; return preset_bundle && preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle) == obj->get_show_printer_type(); } @@ -13915,10 +13878,10 @@ bool Plater::priv::init_collapse_toolbar() BackgroundTexture::Metadata background_data; background_data.filename = m_is_dark ? "toolbar_background_dark.png" : "toolbar_background.png"; - background_data.left = 16; - background_data.top = 16; - background_data.right = 16; - background_data.bottom = 16; + background_data.left = 16; + background_data.top = 16; + background_data.right = 16; + background_data.bottom = 16; if (!collapse_toolbar.init(background_data)) return false; @@ -13936,9 +13899,11 @@ bool Plater::priv::init_collapse_toolbar() item.name = "collapse_sidebar"; // set collapse svg name - item.icon_filename = "collapse.svg"; - item.sprite_id = 0; - item.left.action_callback = []() { wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed()); }; + item.icon_filename = "collapse.svg"; + item.sprite_id = 0; + item.left.action_callback = []() { + wxGetApp().plater()->collapse_sidebar(!wxGetApp().plater()->is_sidebar_collapsed()); + }; if (!collapse_toolbar.add_item(item)) return false; @@ -13949,7 +13914,10 @@ bool Plater::priv::init_collapse_toolbar() return true; } -void Plater::priv::update_preview_bottom_toolbar() { ; } +void Plater::priv::update_preview_bottom_toolbar() +{ + ; +} #if 0 void Plater::update_partplate() @@ -13958,25 +13926,32 @@ void Plater::update_partplate() } #endif -void Plater::priv::reset_gcode_toolpaths() { preview->get_canvas3d()->reset_gcode_toolpaths(); } +void Plater::priv::reset_gcode_toolpaths() +{ + preview->get_canvas3d()->reset_gcode_toolpaths(); +} bool Plater::priv::can_set_instance_to_object() const { const int obj_idx = get_selected_object_idx(); - return 0 <= obj_idx && obj_idx < (int) model.objects.size() && model.objects[obj_idx]->instances.size() > 1; + return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->instances.size() > 1; } -bool Plater::priv::can_split(bool to_objects) const { return sidebar->obj_list()->is_splittable(to_objects); } +bool Plater::priv::can_split(bool to_objects) const +{ + return sidebar->obj_list()->is_splittable(to_objects); +} bool Plater::priv::can_fillcolor() const { - // BBS TODO + //BBS TODO return true; } bool Plater::priv::has_assemble_view() const { - for (auto object : model.objects) { + for (auto object: model.objects) + { for (auto instance : object->instances) if (instance->is_assemble_initialized()) return true; @@ -13997,19 +13972,29 @@ bool Plater::priv::has_assemble_view() const bool Plater::priv::can_scale_to_print_volume() const { const BuildVolume_Type type = this->bed.build_volume().type(); - return !sidebar->obj_list()->has_selected_cut_object() && !view3D->get_canvas3d()->get_selection().is_empty() && - (type == BuildVolume_Type::Rectangle || type == BuildVolume_Type::Circle); + return !sidebar->obj_list()->has_selected_cut_object() + && !view3D->get_canvas3d()->get_selection().is_empty() + && (type == BuildVolume_Type::Rectangle || type == BuildVolume_Type::Circle); } #endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT bool Plater::priv::can_mirror() const -{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().is_from_single_instance(); } +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().is_from_single_instance(); +} bool Plater::priv::can_replace_with_stl() const -{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() == 1; } +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().get_volume_idxs().size() == 1; +} bool Plater::priv::can_replace_all_with_stl() const -{ return !sidebar->obj_list()->has_selected_cut_object() && get_selection().get_volume_idxs().size() != 1; } +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().get_volume_idxs().size() != 1; +} bool Plater::priv::can_reload_from_disk() const { @@ -14030,9 +14015,8 @@ bool Plater::priv::can_reload_from_disk() const int volume_idx; // operators needed by std::algorithms - bool operator<(const SelectedVolume& other) const - { return (object_idx < other.object_idx) || ((object_idx == other.object_idx) && (volume_idx < other.volume_idx)); } - bool operator==(const SelectedVolume& other) const { return (object_idx == other.object_idx) && (volume_idx == other.volume_idx); } + bool operator < (const SelectedVolume& other) const { return (object_idx < other.object_idx) || ((object_idx == other.object_idx) && (volume_idx < other.volume_idx)); } + bool operator == (const SelectedVolume& other) const { return (object_idx == other.object_idx) && (volume_idx == other.volume_idx); } }; std::vector selected_volumes; @@ -14042,24 +14026,22 @@ bool Plater::priv::can_reload_from_disk() const const std::set& selected_volumes_idxs = selection.get_volume_idxs(); for (unsigned int idx : selected_volumes_idxs) { const GLVolume* v = selection.get_volume(idx); - int v_idx = v->volume_idx(); + int v_idx = v->volume_idx(); if (v_idx >= 0) { int o_idx = v->object_idx(); - if (0 <= o_idx && o_idx < (int) model.objects.size()) - selected_volumes.push_back({o_idx, v_idx}); + if (0 <= o_idx && o_idx < (int)model.objects.size()) + selected_volumes.push_back({ o_idx, v_idx }); } } #endif // ENABLE_RELOAD_FROM_DISK_REWORK #if ENABLE_RELOAD_FROM_DISK_REWORK - std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair& v1, const std::pair& v2) { + std::sort(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { return (v1.first < v2.first) || (v1.first == v2.first && v1.second < v2.second); - }); - selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), - [](const std::pair& v1, const std::pair& v2) { - return (v1.first == v2.first) && (v1.second == v2.second); - }), - selected_volumes.end()); + }); + selected_volumes.erase(std::unique(selected_volumes.begin(), selected_volumes.end(), [](const std::pair &v1, const std::pair &v2) { + return (v1.first == v2.first) && (v1.second == v2.second); + }), selected_volumes.end()); // collects paths of files to load std::vector paths; @@ -14087,7 +14069,7 @@ bool Plater::priv::can_reload_from_disk() const return !paths.empty(); } -void Plater::priv::update_publish_dialog_status(wxString& msg, int percent) +void Plater::priv::update_publish_dialog_status(wxString &msg, int percent) { if (m_publish_dlg) m_publish_dlg->UpdateStatus(msg, percent); @@ -14095,45 +14077,42 @@ void Plater::priv::update_publish_dialog_status(wxString& msg, int percent) bool Plater::priv::show_publish_dlg(bool show) { - if (q != nullptr) { - BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":recevied publish event\n"; - } + if (q != nullptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ":recevied publish event\n"; } - if (!m_publish_dlg) - m_publish_dlg = new PublishDialog(q); + if (!m_publish_dlg) m_publish_dlg = new PublishDialog(q); if (show) { m_publish_dlg->reset(); m_publish_dlg->start_slicing(); - // m_publish_dlg->Show(); + //m_publish_dlg->Show(); m_publish_dlg->ShowModal(); } else { m_publish_dlg->EndModal(wxID_OK); - // cancel the slicing + //cancel the slicing if (this->background_process.running()) this->background_process.stop(); } return true; } -// BBS: add bed exclude area -void Plater::priv::set_bed_shape(const Pointfs& shape, - const Pointfs& exclude_areas, - const Pointfs& wrapping_exclude_areas, - const double printable_height, +//BBS: add bed exclude area +void Plater::priv::set_bed_shape(const Pointfs &shape, + const Pointfs &exclude_areas, + const Pointfs &wrapping_exclude_areas, + const double printable_height, std::vector extruder_areas, - std::vector extruder_heights, - const std::string& custom_texture, - const std::string& custom_model, - bool force_as_custom) + std::vector extruder_heights, + const std::string &custom_texture, + const std::string &custom_model, + bool force_as_custom) { - // Orca: reduce resolution for large bed printer + //Orca: reduce resolution for large bed printer BoundingBoxf bed_size = get_extents(shape); if (bed_size.size().maxCoeff() <= LARGE_BED_THRESHOLD) SCALING_FACTOR = SCALING_FACTOR_INTERNAL; else SCALING_FACTOR = SCALING_FACTOR_INTERNAL_LARGE_PRINTER; - // BBS: add shape position + //BBS: add shape position Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); @@ -14142,28 +14121,25 @@ void Plater::priv::set_bed_shape(const Pointfs& shape, double height_to_lid = config->opt_float("extruder_clearance_height_to_lid"); double height_to_rod = config->opt_float("extruder_clearance_height_to_rod"); - Pointfs prev_exclude_areas = partplate_list.get_exclude_area(); + Pointfs prev_exclude_areas = partplate_list.get_exclude_area(); Pointfs prev_wrapping_exclude_areas = partplate_list.get_wrapping_exclude_area(); - new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != exclude_areas) || - (prev_wrapping_exclude_areas != wrapping_exclude_areas); + new_shape |= (height_to_lid != prev_height_lid) || (height_to_rod != prev_height_rod) || (prev_exclude_areas != exclude_areas) + || (prev_wrapping_exclude_areas != wrapping_exclude_areas); if (!new_shape && partplate_list.get_logo_texture_filename() != custom_texture) { partplate_list.update_logo_texture_filename(custom_texture); } if (new_shape) { - if (view3D) - view3D->bed_shape_changed(); - if (preview) - preview->bed_shape_changed(); + if (view3D) view3D->bed_shape_changed(); + if (preview) preview->bed_shape_changed(); - // BBS: update part plate's size - // BBS: to be checked + //BBS: update part plate's size + // BBS: to be checked Vec3d max = bed.printable_bounding_box().max; Vec3d min = bed.printable_bounding_box().min; - double z = config->opt_float("printable_height"); + double z = config->opt_float("printable_height"); partplate_list.reset_size(max.x() - min.x() - Bed3D::Axes::DefaultTipRadius, max.y() - min.y() - Bed3D::Axes::DefaultTipRadius, z); - partplate_list.set_shapes(shape, exclude_areas, wrapping_exclude_areas, extruder_areas, extruder_heights, custom_texture, - height_to_lid, height_to_rod); + partplate_list.set_shapes(shape, exclude_areas, wrapping_exclude_areas, extruder_areas, extruder_heights, custom_texture, height_to_lid, height_to_rod); Vec2d new_shape_position = partplate_list.get_current_shape_position(); if (shape_position != new_shape_position) @@ -14171,13 +14147,25 @@ void Plater::priv::set_bed_shape(const Pointfs& shape, } } -bool Plater::priv::can_delete() const { return !get_selection().is_empty() && !get_selection().is_wipe_tower(); } +bool Plater::priv::can_delete() const +{ + return !get_selection().is_empty() && !get_selection().is_wipe_tower(); +} -bool Plater::priv::can_delete_all() const { return !model.objects.empty(); } +bool Plater::priv::can_delete_all() const +{ + return !model.objects.empty(); +} -bool Plater::priv::can_add_plate() const { return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; } +bool Plater::priv::can_add_plate() const +{ + return q->get_partplate_list().get_plate_count() < PartPlateList::MAX_PLATES_COUNT; +} -bool Plater::priv::can_delete_plate() const { return q->get_partplate_list().get_plate_count() > 1; } +bool Plater::priv::can_delete_plate() const +{ + return q->get_partplate_list().get_plate_count() > 1; +} bool Plater::priv::can_fix_through_cgal() const { @@ -14186,8 +14174,8 @@ bool Plater::priv::can_fix_through_cgal() const #if FIX_THROUGH_CGAL_ALWAYS // Fixing always. - return !obj_idxs.empty() || !vol_idxs.empty(); -#else // FIX_THROUGH_CGAL_ALWAYS + return ! obj_idxs.empty() || ! vol_idxs.empty(); +#else // FIX_THROUGH_CGAL_ALWAYS // Fixing only if the model is not manifold. if (vol_idxs.empty()) { for (auto obj_idx : obj_idxs) @@ -14207,10 +14195,10 @@ bool Plater::priv::can_fix_through_cgal() const bool Plater::priv::can_simplify() const { // is object for simplification selected - if (get_selected_object_idx() < 0) - return false; + if (get_selected_object_idx() < 0) return false; // is already opened? - if (q->get_view3D_canvas3D()->get_gizmos_manager().get_current_type() == GLGizmosManager::EType::Simplify) + if (q->get_view3D_canvas3D()->get_gizmos_manager().get_current_type() == + GLGizmosManager::EType::Simplify) return false; return true; } @@ -14235,30 +14223,41 @@ bool Plater::priv::can_smooth_mesh() const bool Plater::priv::can_increase_instances() const { - if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) - return false; + if (!m_worker.is_idle() + || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int) model.objects.size()) && !sidebar->obj_list()->has_selected_cut_object() && - std::all_of(model.objects[obj_idx]->instances.begin(), model.objects[obj_idx]->instances.end(), - [](auto& inst) { return inst->printable; }); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) + && !sidebar->obj_list()->has_selected_cut_object() + && std::all_of(model.objects[obj_idx]->instances.begin(), model.objects[obj_idx]->instances.end(), [](auto& inst) {return inst->printable; }); } bool Plater::priv::can_decrease_instances() const { - if (!m_worker.is_idle() || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) - return false; + if (!m_worker.is_idle() + || q->get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode()) + return false; int obj_idx = get_selected_object_idx(); - return (0 <= obj_idx) && (obj_idx < (int) model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1) && - !sidebar->obj_list()->has_selected_cut_object(); + return (0 <= obj_idx) && (obj_idx < (int)model.objects.size()) && (model.objects[obj_idx]->instances.size() > 1) + && !sidebar->obj_list()->has_selected_cut_object(); } -bool Plater::priv::can_split_to_objects() const { return q->can_split(true); } +bool Plater::priv::can_split_to_objects() const +{ + return q->can_split(true); +} -bool Plater::priv::can_split_to_volumes() const { return (printer_technology != ptSLA) && q->can_split(false); } +bool Plater::priv::can_split_to_volumes() const +{ + return (printer_technology != ptSLA) && q->can_split(false); +} -bool Plater::priv::can_arrange() const { return !model.objects.empty() && m_worker.is_idle(); } +bool Plater::priv::can_arrange() const +{ + return !model.objects.empty() && m_worker.is_idle(); +} bool Plater::priv::layers_height_allowed() const { @@ -14266,11 +14265,13 @@ bool Plater::priv::layers_height_allowed() const return false; int obj_idx = get_selected_object_idx(); - return 0 <= obj_idx && obj_idx < (int) model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && - view3D->is_layers_editing_allowed(); + return 0 <= obj_idx && obj_idx < (int)model.objects.size() && model.objects[obj_idx]->max_z() > SINKING_Z_THRESHOLD && view3D->is_layers_editing_allowed(); } -bool Plater::priv::can_layers_editing() const { return layers_height_allowed(); } +bool Plater::priv::can_layers_editing() const +{ + return layers_height_allowed(); +} void Plater::priv::on_action_layersediting(SimpleEvent&) { @@ -14281,9 +14282,9 @@ void Plater::priv::on_action_layersediting(SimpleEvent&) const auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; if (print_config.opt_bool("enable_mixed_color_sublayer")) { if (wxGetApp().app_config->get("no_warn_mixed_sublayer_variable_layer") != "1") { - MessageDialog - dlg(q, _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), - _L("Warning"), wxICON_WARNING | wxOK); + MessageDialog dlg(q, + _L("Using variable layer height together with mixed color sublayer may result in poor color mixing quality."), + _L("Warning"), wxICON_WARNING | wxOK); dlg.show_dsa_button(); dlg.ShowModal(); if (dlg.get_checkbox_state()) @@ -14295,7 +14296,7 @@ void Plater::priv::on_action_layersediting(SimpleEvent&) notification_manager->set_move_from_overlay(view3D->is_layers_editing_enabled()); } -void Plater::priv::on_create_filament(SimpleEvent&) +void Plater::priv::on_create_filament(SimpleEvent &) { CreateFilamentPresetDialog dlg(wxGetApp().mainframe); int res = dlg.ShowModal(); @@ -14304,48 +14305,51 @@ void Plater::priv::on_create_filament(SimpleEvent&) update_ui_from_settings(); sidebar->update_all_preset_comboboxes(); CreatePresetSuccessfulDialog success_dlg(wxGetApp().mainframe, SuccessType::FILAMENT); - int res = success_dlg.ShowModal(); + int res = success_dlg.ShowModal(); } } -void Plater::priv::on_modify_filament(SimpleEvent& evt) +void Plater::priv::on_modify_filament(SimpleEvent &evt) { - Filamentinformation* filament_info = static_cast(evt.GetEventObject()); - int res; + Filamentinformation *filament_info = static_cast(evt.GetEventObject()); + int res; std::shared_ptr need_edit_preset; { EditFilamentPresetDialog dlg(wxGetApp().mainframe, filament_info); - res = dlg.ShowModal(); + res = dlg.ShowModal(); need_edit_preset = dlg.get_need_edit_preset(); } wxGetApp().mainframe->update_side_preset_ui(); update_ui_from_settings(); sidebar->update_all_preset_comboboxes(); if (wxID_EDIT == res) { - Tab* tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT); - // tab->restore_last_select_item(); - if (tab == nullptr) { - return; - } + Tab *tab = wxGetApp().get_tab(Preset::Type::TYPE_FILAMENT); + //tab->restore_last_select_item(); + if (tab == nullptr) { return; } // Popup needs to be called before "restore_last_select_item", otherwise the page may not be updated wxGetApp().params_dialog()->Popup(); tab->restore_last_select_item(); - // Opening Studio and directly accessing the Filament settings interface through the edit preset button will not take effect and - // requires manual settings. + // Opening Studio and directly accessing the Filament settings interface through the edit preset button will not take effect and requires manual settings. tab->set_just_edit(true); tab->select_preset(need_edit_preset->name); - // when some preset have modified, if the printer is not need_edit_preset_name compatible printer, the preset will jump to other - // preset, need select again - if (!need_edit_preset->is_compatible) - tab->select_preset(need_edit_preset->name); + // when some preset have modified, if the printer is not need_edit_preset_name compatible printer, the preset will jump to other preset, need select again + if (!need_edit_preset->is_compatible) tab->select_preset(need_edit_preset->name); } + } -void Plater::priv::on_add_filament(SimpleEvent& evt) { sidebar->add_filament(); } +void Plater::priv::on_add_filament(SimpleEvent &evt) { + sidebar->add_filament(); +} -void Plater::priv::on_delete_filament(SimpleEvent& evt) { sidebar->delete_filament(); } +void Plater::priv::on_delete_filament(SimpleEvent &evt) { + sidebar->delete_filament(); +} -void Plater::priv::on_add_custom_filament(ColorEvent& evt) { sidebar->add_custom_filament(evt.data); } +void Plater::priv::on_add_custom_filament(ColorEvent &evt) +{ + sidebar->add_custom_filament(evt.data); +} void Plater::priv::enter_gizmos_stack() { @@ -14364,7 +14368,7 @@ bool Plater::priv::leave_gizmos_stack() bool changed = false; assert(m_undo_redo_stack_active == &m_undo_redo_stack_gizmos); if (m_undo_redo_stack_active == &m_undo_redo_stack_gizmos) { - assert(!m_undo_redo_stack_active->empty()); + assert(! m_undo_redo_stack_active->empty()); changed = m_undo_redo_stack_gizmos.has_undo_snapshot(); m_undo_redo_stack_active->clear(); m_undo_redo_stack_active = &m_undo_redo_stack_main; @@ -14374,7 +14378,7 @@ bool Plater::priv::leave_gizmos_stack() int Plater::priv::get_active_snapshot_index() { - const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time(); + const size_t active_snapshot_time = this->undo_redo_stack().active_snapshot_time(); const std::vector& ss_stack = this->undo_redo_stack().snapshots(); const auto it = std::lower_bound(ss_stack.begin(), ss_stack.end(), UndoRedo::Snapshot(active_snapshot_time)); return it - ss_stack.begin(); @@ -14396,10 +14400,12 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed if (this->sidebar->obj_list()->is_selected(itSettings)) { snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } else if (this->sidebar->obj_list()->is_selected(itLayer)) { + } + else if (this->sidebar->obj_list()->is_selected(itLayer)) { snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + } + else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; // If SLA gizmo is active, ask it if it wants to trigger support generation @@ -14407,12 +14413,12 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed if (view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; - // FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print - // config. BBS: add partplate logic + //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); assert(tower_x_opt->values.size() == tower_y_opt->values.size()); @@ -14422,24 +14428,18 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = config.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } - const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_gizmos_manager() : - view3D->get_canvas3d()->get_gizmos_manager(); + const GLGizmosManager& gizmos = get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : view3D->get_canvas3d()->get_gizmos_manager(); if (snapshot_type == UndoRedo::SnapshotType::ProjectSeparator) this->undo_redo_stack().clear(); - this->undo_redo_stack().take_snapshot(snapshot_name, model, - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_selection() : - view3D->get_canvas3d()->get_selection(), - gizmos, partplate_list, snapshot_data); + this->undo_redo_stack().take_snapshot(snapshot_name, model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : view3D->get_canvas3d()->get_selection(), gizmos, partplate_list, snapshot_data); if (snapshot_type == UndoRedo::SnapshotType::LeavingGizmoWithAction) { - // Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and - // UndoRedo::SnapshotType::LeavingGizmoWithAction. The remaining snapshot will be renamed to a more generic name, depending on what - // gizmo is being left. + // Filter all but the last UndoRedo::SnapshotType::GizmoAction in a row between the last UndoRedo::SnapshotType::EnteringGizmo and UndoRedo::SnapshotType::LeavingGizmoWithAction. + // The remaining snapshot will be renamed to a more generic name, + // depending on what gizmo is being left. if (gizmos.get_current() != nullptr) { std::string new_name = gizmos.get_current()->get_action_snapshot_name(); this->undo_redo_stack().reduce_noisy_snapshots(new_name); @@ -14448,29 +14448,23 @@ void Plater::priv::take_snapshot(const std::string& snapshot_name, const UndoRed // Reset the "dirty project" flag. m_undo_redo_stack_main.mark_current_as_saved(); } - // BBS: add PartPlateList as the paremeter for take_snapshot + //BBS: add PartPlateList as the paremeter for take_snapshot this->undo_redo_stack().release_least_recently_used(); dirty_state.update_from_undo_redo_stack(m_undo_redo_stack_main.project_modified()); // Save the last active preset name of a particular printer technology. - ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : - m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); - BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name - << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) - << log_memory_info(); + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot taken: " << snapshot_name << ", Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); } void Plater::priv::undo() { - const std::vector& snapshots = this->undo_redo_stack().snapshots(); - auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), - UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); + const std::vector &snapshots = this->undo_redo_stack().snapshots(); + auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record - while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current)) - ; - if (it_current == snapshots.begin()) - return; + while (--it_current != snapshots.begin() && !snapshot_modifies_project(*it_current)); + if (it_current == snapshots.begin()) return; if (get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView) { if (it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::GizmoAction && it_current->snapshot_data.snapshot_type != UndoRedo::SnapshotType::EnteringGizmo && @@ -14483,22 +14477,19 @@ void Plater::priv::undo() void Plater::priv::redo() { - const std::vector& snapshots = this->undo_redo_stack().snapshots(); - auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), - UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); + const std::vector &snapshots = this->undo_redo_stack().snapshots(); + auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(this->undo_redo_stack().active_snapshot_time())); // BBS: undo-redo until modify record - while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)) - ; + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); if (it_current != snapshots.end()) { - while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)) - ; + while (it_current != snapshots.end() && !snapshot_modifies_project(*it_current++)); this->undo_redo_to(--it_current); } } void Plater::priv::undo_redo_to(size_t time_to_load) { - const std::vector& snapshots = this->undo_redo_stack().snapshots(); + const std::vector &snapshots = this->undo_redo_stack().snapshots(); auto it_current = std::lower_bound(snapshots.begin(), snapshots.end(), UndoRedo::Snapshot(time_to_load)); assert(it_current != snapshots.end()); this->undo_redo_to(it_current); @@ -14513,7 +14504,8 @@ bool Plater::priv::up_to_date(bool saved, bool backup) if (!backup) undo_redo_stack_main().mark_current_as_saved(); return true; - } else { + } + else { return !undo_redo_stack_main().has_real_change_from(last_time); } } @@ -14523,21 +14515,20 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator // Make sure that no updating function calls take_snapshot until we are done. SuppressSnapshots snapshot_supressor(q); - bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); - PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; - bool printer_technology_changed = this->printer_technology != new_printer_technology; + bool temp_snapshot_was_taken = this->undo_redo_stack().temp_snapshot_active(); + PrinterTechnology new_printer_technology = it_snapshot->snapshot_data.printer_technology; + bool printer_technology_changed = this->printer_technology != new_printer_technology; if (printer_technology_changed) { - // BBS do not support SLA + //BBS do not support SLA } // Save the last active preset name of a particular printer technology. - ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : - m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); - // FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print - // config. BBS: add partplate logic + ((this->printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name) = wxGetApp().preset_bundle->printers.get_selected_preset_name(); + //FIXME updating the Wipe tower config values at the ModelWipeTower from the Print config. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; const ConfigOptionFloats* tower_x_opt = proj_cfg.option("wipe_tower_x"); const ConfigOptionFloats* tower_y_opt = proj_cfg.option("wipe_tower_y"); assert(tower_x_opt->values.size() == tower_y_opt->values.size()); @@ -14547,7 +14538,7 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator ModelWipeTower& tower = model.wipe_tower; tower.positions[plate_idx] = Vec2d(tower_x_opt->get_at(plate_idx), tower_y_opt->get_at(plate_idx)); - tower.rotation = config.opt_float("wipe_tower_rotation_angle"); + tower.rotation = config.opt_float("wipe_tower_rotation_angle"); } } const int layer_range_idx = it_snapshot->snapshot_data.layer_range_idx; @@ -14560,15 +14551,17 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator if (this->sidebar->obj_list()->is_selected(itSettings)) { top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR; top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } else if (this->sidebar->obj_list()->is_selected(itLayer)) { + } + else if (this->sidebar->obj_list()->is_selected(itLayer)) { top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR; top_snapshot_data.layer_range_idx = this->sidebar->obj_list()->get_selected_layers_range_idx(); - } else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) + } + else if (this->sidebar->obj_list()->is_selected(itLayerRoot)) top_snapshot_data.flags |= UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR; - bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0; - bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0; - bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0; - bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0; + bool new_variable_layer_editing_active = (new_flags & UndoRedo::SnapshotData::VARIABLE_LAYER_EDITING_ACTIVE) != 0; + bool new_selected_settings_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_SETTINGS_ON_SIDEBAR) != 0; + bool new_selected_layer_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYER_ON_SIDEBAR) != 0; + bool new_selected_layerroot_on_sidebar = (new_flags & UndoRedo::SnapshotData::SELECTED_LAYERROOT_ON_SIDEBAR) != 0; if (this->view3D->get_canvas3d()->get_gizmos_manager().wants_reslice_supports_on_undo()) top_snapshot_data.flags |= UndoRedo::SnapshotData::RECALCULATE_SLA_SUPPORTS; @@ -14581,45 +14574,32 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator const UndoRedo::Snapshot snapshot_copy = *it_snapshot; // Do the jump in time. if (it_snapshot->timestamp < this->undo_redo_stack().active_snapshot_time() ? - this->undo_redo_stack().undo(model, - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_selection() : - this->view3D->get_canvas3d()->get_selection(), - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_gizmos_manager() : - this->view3D->get_canvas3d()->get_gizmos_manager(), - this->partplate_list, top_snapshot_data, it_snapshot->timestamp) : - this->undo_redo_stack().redo(model, - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d()->get_gizmos_manager() : - this->view3D->get_canvas3d()->get_gizmos_manager(), - this->partplate_list, it_snapshot->timestamp)) { + this->undo_redo_stack().undo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection() : this->view3D->get_canvas3d()->get_selection(), get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, top_snapshot_data, it_snapshot->timestamp) : + this->undo_redo_stack().redo(model, get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager() : this->view3D->get_canvas3d()->get_gizmos_manager(), this->partplate_list, it_snapshot->timestamp)) { if (printer_technology_changed) { // Switch to the other printer technology. Switch to the last printer active for that particular technology. - AppConfig* app_config = wxGetApp().app_config; - app_config->set("presets", PRESET_PRINTER_NAME, - (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); - // FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. - // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. + AppConfig *app_config = wxGetApp().app_config; + app_config->set("presets", PRESET_PRINTER_NAME, (new_printer_technology == ptFFF) ? m_last_fff_printer_profile_name : m_last_sla_printer_profile_name); + //FIXME Why are we reloading the whole preset bundle here? Please document. This is fishy and it is unnecessarily expensive. + // Anyways, don't report any config value substitutions, they have been already reported to the user at application start up. wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilent); - // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> - // Object_list::update_and_show_object_settings_item(), but the Object list still keeps pointer to the old Model. Avoid a crash - // by removing selection first. + // load_current_presets() calls Tab::load_current_preset() -> TabPrint::update() -> Object_list::update_and_show_object_settings_item(), + // but the Object list still keeps pointer to the old Model. Avoid a crash by removing selection first. this->sidebar->obj_list()->unselect_objects(); // Load the currently selected preset into the GUI, update the preset selection box. // This also switches the printer technology based on the printer technology of the active printer profile. wxGetApp().load_current_presets(); } - // FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. - // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print - // config. BBS: add partplate logic + //FIXME updating the Print config from the Wipe tower config values at the ModelWipeTower. + // This is a workaround until we refactor the Wipe Tower position / orientation to live solely inside the Model, not in the Print config. + // BBS: add partplate logic if (this->printer_technology == ptFFF) { - const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + const DynamicPrintConfig& config = wxGetApp().preset_bundle->prints.get_edited_preset().config; const DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; - ConfigOptionFloats* tower_x_opt = const_cast(proj_cfg.option("wipe_tower_x")); - ConfigOptionFloats* tower_y_opt = const_cast(proj_cfg.option("wipe_tower_y")); + ConfigOptionFloats* tower_x_opt = const_cast(proj_cfg.option("wipe_tower_x")); + ConfigOptionFloats* tower_y_opt = const_cast(proj_cfg.option("wipe_tower_y")); // BBS: don't support wipe tower rotation - // double current_rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); + //double current_rotation = proj_cfg.opt_float("wipe_tower_rotation_angle"); bool need_update = false; if (tower_x_opt->values.size() != model.wipe_tower.positions.size()) { tower_x_opt->clear(); @@ -14673,28 +14653,18 @@ void Plater::priv::undo_redo_to(std::vector::const_iterator void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bool /* temp_snapshot_was_taken */) { - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection().clear() : - this->view3D->get_canvas3d()->get_selection().clear(); + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_selection().clear() : this->view3D->get_canvas3d()->get_selection().clear(); // Update volumes from the deserializd model, always stop / update the background processing (for both the SLA and FFF technologies). - this->update((unsigned int) UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | - (unsigned int) UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); - // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first - // snapshot. - // if (temp_snapshot_was_taken) - // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while - // some triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the - // Undo / Redo stack size. - this->undo_redo_stack().release_least_recently_used(); - // YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) - get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? - assemble_view->get_canvas3d() - ->get_selection() - .set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), - this->undo_redo_stack().selection_deserialized().volumes_and_instances) : - this->view3D->get_canvas3d() - ->get_selection() - .set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), - this->undo_redo_stack().selection_deserialized().volumes_and_instances); + this->update((unsigned int)UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE | (unsigned int)UpdateParams::POSTPONE_VALIDATION_ERROR_MESSAGE); + // Release old snapshots if the memory allocated is excessive. This may remove the top most snapshot if jumping to the very first snapshot. + //if (temp_snapshot_was_taken) + // Release the old snapshots always, as it may have happened, that some of the triangle meshes got deserialized from the snapshot, while some + // triangle meshes may have gotten released from the scene or the background processing, therefore now being calculated into the Undo / Redo stack size. + this->undo_redo_stack().release_least_recently_used(); + //YS_FIXME update obj_list from the deserialized model (maybe store ObjectIDs into the tree?) (no selections at this point of time) + get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? + assemble_view->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances) : + this->view3D->get_canvas3d()->get_selection().set_deserialized(GUI::Selection::EMode(this->undo_redo_stack().selection_deserialized().mode), this->undo_redo_stack().selection_deserialized().volumes_and_instances); get_current_canvas3D()->get_canvas_type() == GLCanvas3D::CanvasAssembleView ? assemble_view->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot) : this->view3D->get_canvas3d()->get_gizmos_manager().update_after_undo_redo(snapshot); @@ -14703,23 +14673,20 @@ void Plater::priv::update_after_undo_redo(const UndoRedo::Snapshot& snapshot, bo if (wxGetApp().get_mode() == comSimple && model_has_advanced_features(this->model)) { // If the user jumped to a snapshot that require user interface with advanced features, switch to the advanced mode without asking. - // There is a little risk of surprising the user, as he already must have had the advanced or advanced mode active for such a - // snapshot to be taken. + // There is a little risk of surprising the user, as he already must have had the advanced or advanced mode active for such a snapshot to be taken. Slic3r::GUI::wxGetApp().save_mode(comAdvanced); view3D->set_as_dirty(); } - // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the - // back end, it would not open immediately, but it would be saved to be show later. Let's do it now. We do not want to display the - // message box earlier, because on Windows & OSX the message box takes over the message queue pump, which in turn executes the rendering - // function before a full update after the Undo / Redo jump. + // this->update() above was called with POSTPONE_VALIDATION_ERROR_MESSAGE, so that if an error message was generated when updating the back end, it would not open immediately, + // but it would be saved to be show later. Let's do it now. We do not want to display the message box earlier, because on Windows & OSX the message box takes over the message + // queue pump, which in turn executes the rendering function before a full update after the Undo / Redo jump. this->show_delayed_error_message(); - // FIXME what about the state of the manipulators? - // FIXME what about the focus? Cursor in the side panel? + //FIXME what about the state of the manipulators? + //FIXME what about the focus? Cursor in the side panel? - BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " - << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); + BOOST_LOG_TRIVIAL(info) << "Undo / Redo snapshot reloaded. Undo / Redo stack memory: " << Slic3r::format_memsize_MB(this->undo_redo_stack().memsize()) << log_memory_info(); } void Plater::priv::bring_instance_forward() const @@ -14733,7 +14700,7 @@ void Plater::priv::bring_instance_forward() const return; } BOOST_LOG_TRIVIAL(debug) << "Orca Slicer window going forward"; - // this code maximize app window on Fedora + //this code maximize app window on Fedora { main_frame->Iconize(false); if (main_frame->IsMaximized()) @@ -14741,25 +14708,25 @@ void Plater::priv::bring_instance_forward() const else main_frame->Maximize(false); } - // this code maximize window on Ubuntu + //this code maximize window on Ubuntu { main_frame->Restore(); - wxGetApp().GetTopWindow()->SetFocus(); // focus on my window + wxGetApp().GetTopWindow()->SetFocus(); // focus on my window wxGetApp().GetTopWindow()->Show(true); // show the window - wxGetApp().GetTopWindow()->Raise(); // bring window to front + wxGetApp().GetTopWindow()->Raise(); // bring window to front } } -// BBS: popup object table +//BBS: popup object table bool Plater::priv::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, create ObjectTableDialog"); int max_width{1920}, max_height{1080}; - max_width = q->GetMaxWidth(); + max_width = q->GetMaxWidth(); max_height = q->GetMaxHeight(); ObjectTableDialog table_dialog(q, q, &model, wxSize(max_width, max_height)); - // m_popup_table = new ObjectTableDialog(q, q, &model); + //m_popup_table = new ObjectTableDialog(q, q, &model); wxRect rect = sidebar->GetRect(); wxPoint pos = sidebar->ClientToScreen(wxPoint(rect.x, rect.y)); @@ -14773,17 +14740,19 @@ bool Plater::priv::PopupObjectTable(int object_id, int volume_id, const wxPoint& void Sidebar::set_btn_label(const ActionButtonType btn_type, const wxString& label) const { - switch (btn_type) { - case ActionButtonType::abReslice: p->btn_reslice->SetLabelText(label); break; - case ActionButtonType::abExport: p->btn_export_gcode->SetLabelText(label); break; - case ActionButtonType::abSendGCode: /*p->btn_send_gcode->SetLabelText(label);*/ break; + switch (btn_type) + { + case ActionButtonType::abReslice: p->btn_reslice->SetLabelText(label); break; + case ActionButtonType::abExport: p->btn_export_gcode->SetLabelText(label); break; + case ActionButtonType::abSendGCode: /*p->btn_send_gcode->SetLabelText(label);*/ break; } } // Plater / Public -Plater::Plater(wxWindow* parent, MainFrame* main_frame) - : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()), p(new priv(this, main_frame)) +Plater::Plater(wxWindow *parent, MainFrame *main_frame) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxGetApp().get_min_size()) + , p(new priv(this, main_frame)) { // Initialization performed in the private c-tor enable_wireframe(true); @@ -14801,7 +14770,7 @@ bool Plater::is_project_dirty() const { return p->is_project_dirty(); } bool Plater::is_presets_dirty() const { return p->is_presets_dirty(); } void Plater::set_plater_dirty(bool is_dirty) { p->set_plater_dirty(is_dirty); } void Plater::update_project_dirty_from_presets() { p->update_project_dirty_from_presets(); } -int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); } +int Plater::save_project_if_dirty(const wxString& reason) { return p->save_project_if_dirty(reason); } void Plater::reset_project_dirty_after_save() { p->reset_project_dirty_after_save(); } void Plater::reset_project_dirty_initial_presets() { p->reset_project_dirty_initial_presets(); } #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW @@ -14812,12 +14781,10 @@ std::vector Plater::mixed_filament_config_indices() const { std::vector indices; auto& config = wxGetApp().preset_bundle->project_config; - auto* opt = config.option("filament_is_mixed"); - if (!opt) - return indices; + auto* opt = config.option("filament_is_mixed"); + if (!opt) return indices; for (size_t i = 0; i < opt->values.size(); ++i) - if (opt->values[i]) - indices.push_back(i); + if (opt->values[i]) indices.push_back(i); return indices; } @@ -14825,7 +14792,7 @@ std::vector Plater::physical_filament_config_indices() const { std::vector indices; auto& config = wxGetApp().preset_bundle->project_config; - auto* opt = config.option("filament_is_mixed"); + auto* opt = config.option("filament_is_mixed"); size_t total = wxGetApp().preset_bundle->filament_presets.size(); for (size_t i = 0; i < total; ++i) { if (!opt || i >= opt->values.size() || !opt->values[i]) @@ -14834,13 +14801,11 @@ std::vector Plater::physical_filament_config_indices() const return indices; } -bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, - TextureImportResult& result, +bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, TextureImportResult& result, std::function cancel_callback, std::function progress_callback) { - if (!loaded_model.texture_mesh || !has_importable_texture(*loaded_model.texture_mesh)) - return false; + if (!loaded_model.texture_mesh || !has_importable_texture(*loaded_model.texture_mesh)) return false; // Defense in depth: if all geometry got dropped earlier (e.g. by a future // regression of the zero-volume cleanup) but the textured mesh is still @@ -14854,31 +14819,29 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, return true; } - const wxString fallback_warning = _L("Texture import failed. The model appears to contain texture data, but the texture import process " - "could not be completed. The model will be imported as geometry only."); + const wxString fallback_warning = _L("Texture import failed. The model appears to contain texture data, but the texture import process could not be completed. The model will be imported as geometry only."); BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: opening texture import dialog"; std::vector filament_entries; { - auto& preset_bundle = *wxGetApp().preset_bundle; + auto& preset_bundle = *wxGetApp().preset_bundle; auto& project_config = preset_bundle.project_config; - auto* colours_opt = project_config.option("filament_colour"); - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - auto* type_opt = project_config.option("filament_type"); + auto* colours_opt = project_config.option("filament_colour"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + auto* type_opt = project_config.option("filament_type"); auto* components_opt = project_config.option("filament_mixed_components"); - auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); - const size_t total = preset_bundle.filament_presets.size(); + auto* ratios_opt = project_config.option("filament_mixed_sublayer_ratios"); + const size_t total = preset_bundle.filament_presets.size(); filament_entries.reserve(total); for (size_t i = 0; i < total; ++i) { TextureFilamentEntry entry; - entry.kind = (is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]) ? - TextureFilamentKind::ExistingMixed : - TextureFilamentKind::ExistingPhysical; - entry.dialog_index = (int) filament_entries.size(); + entry.kind = (is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]) ? + TextureFilamentKind::ExistingMixed : TextureFilamentKind::ExistingPhysical; + entry.dialog_index = (int)filament_entries.size(); entry.project_config_index = i; - entry.color_hex = (colours_opt && i < colours_opt->values.size()) ? colours_opt->values[i] : "#808080"; - entry.type = (type_opt && i < type_opt->values.size()) ? type_opt->values[i] : ""; + entry.color_hex = (colours_opt && i < colours_opt->values.size()) ? colours_opt->values[i] : "#808080"; + entry.type = (type_opt && i < type_opt->values.size()) ? type_opt->values[i] : ""; std::string name; if (i < preset_bundle.filament_presets.size()) { @@ -14893,19 +14856,19 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, if (entry.kind == TextureFilamentKind::ExistingMixed) { if (components_opt && i < components_opt->values.size()) entry.mixed_components = Slic3r::parse_mixed_components(components_opt->values[i]); - std::vector ratios = Slic3r::parse_mixed_ratios(ratios_opt && i < ratios_opt->values.size() ? - ratios_opt->values[i] : - "", - entry.mixed_components.size()); + std::vector ratios = Slic3r::parse_mixed_ratios( + ratios_opt && i < ratios_opt->values.size() ? ratios_opt->values[i] : "", + entry.mixed_components.size()); entry.mixed_ratios.reserve(ratios.size()); for (double ratio : ratios) - entry.mixed_ratios.push_back((int) std::lround(ratio * 100.0)); + entry.mixed_ratios.push_back((int)std::lround(ratio * 100.0)); } filament_entries.push_back(std::move(entry)); } } - TextureImportDialog dlg(q, *loaded_model.texture_mesh, filament_entries, std::move(cancel_callback), std::move(progress_callback)); + TextureImportDialog dlg(q, *loaded_model.texture_mesh, filament_entries, + std::move(cancel_callback), std::move(progress_callback)); if (dlg.ShowModal() != wxID_OK) { if (dlg.was_skipped()) { BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: user skipped texture matching"; @@ -14916,7 +14879,7 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, if (dlg.fallback_to_geometry_only()) { BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: texture import failed, falling back to geometry-only import"; result.fallback_to_geometry_only = true; - result.fallback_warning = fallback_warning; + result.fallback_warning = fallback_warning; loaded_model.texture_mesh.reset(); return true; } @@ -14925,13 +14888,13 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, return false; } - auto painted = dlg.get_painted_mesh(); + auto painted = dlg.get_painted_mesh(); auto final_matches = dlg.get_matches(); if (painted.face_colors.empty() || final_matches.empty()) { BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: no painting result"; result.fallback_to_geometry_only = true; - result.fallback_warning = fallback_warning; + result.fallback_warning = fallback_warning; loaded_model.texture_mesh.reset(); return true; } @@ -14939,28 +14902,26 @@ bool Plater::priv::run_textured_mesh_import_dialog(Slic3r::Model& loaded_model, BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: got " << painted.cluster_colors.size() << " clusters, skipped=" << dlg.was_skipped(); - result.painted = std::move(painted); - result.matches = std::move(final_matches); - result.new_filament_colors = dlg.get_new_filament_colors(); + result.painted = std::move(painted); + result.matches = std::move(final_matches); + result.new_filament_colors = dlg.get_new_filament_colors(); result.new_filament_preset_names = dlg.get_new_filament_preset_names(); - result.new_mixed_filaments = dlg.get_new_mixed_filaments(); - result.filament_entries = dlg.get_filament_entries(); - result.existing_filament_count = dlg.get_existing_filament_count(); - result.skipped = dlg.was_skipped(); + result.new_mixed_filaments = dlg.get_new_mixed_filaments(); + result.filament_entries = dlg.get_filament_entries(); + result.existing_filament_count = dlg.get_existing_filament_count(); + result.skipped = dlg.was_skipped(); return true; } -void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model, - const std::vector& obj_idxs, +void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model, const std::vector& obj_idxs, const TextureImportResult& result, - LoadProgressCallback progress_callback, - bool update_scene) + LoadProgressCallback progress_callback, bool update_scene) { auto update_apply_progress = [&progress_callback](int percent, const wxString& message) { return !progress_callback || progress_callback(std::clamp(percent, 0, 100), message); }; - const auto& painted = result.painted; + const auto& painted = result.painted; const auto& final_matches = result.matches; if (painted.face_colors.empty() || final_matches.empty()) { @@ -14977,9 +14938,9 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model auto collect_physical_color_strs = []() { std::vector colors; auto& project_config = wxGetApp().preset_bundle->project_config; - auto* colours_opt = project_config.option("filament_colour"); - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - const size_t total = wxGetApp().preset_bundle->filament_presets.size(); + auto* colours_opt = project_config.option("filament_colour"); + auto* is_mixed_opt = project_config.option("filament_is_mixed"); + const size_t total = wxGetApp().preset_bundle->filament_presets.size(); for (size_t i = 0; i < total; ++i) { const bool is_mixed = is_mixed_opt && i < is_mixed_opt->values.size() && is_mixed_opt->values[i]; if (!is_mixed) @@ -14991,7 +14952,7 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model const auto& entries = result.filament_entries; std::vector filament_index_remap(entries.size(), -1); size_t existing_physical_count = 0; - size_t new_physical_count = 0; + size_t new_physical_count = 0; for (const auto& entry : entries) { if (entry.kind == TextureFilamentKind::ExistingPhysical) ++existing_physical_count; @@ -15000,12 +14961,12 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model } for (const auto& entry : entries) { - if (entry.dialog_index < 0 || entry.dialog_index >= (int) filament_index_remap.size()) + if (entry.dialog_index < 0 || entry.dialog_index >= (int)filament_index_remap.size()) continue; if (entry.kind == TextureFilamentKind::ExistingPhysical) { - filament_index_remap[entry.dialog_index] = (int) entry.project_config_index; + filament_index_remap[entry.dialog_index] = (int)entry.project_config_index; } else if (entry.kind == TextureFilamentKind::ExistingMixed) { - filament_index_remap[entry.dialog_index] = (int) (entry.project_config_index + new_physical_count); + filament_index_remap[entry.dialog_index] = (int)(entry.project_config_index + new_physical_count); } } @@ -15016,10 +14977,12 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model wxColour new_col(entry.color_hex); const size_t final_idx = existing_physical_count + new_physical_order; sidebar->add_custom_filament(new_col, entry.preset_name); - if (entry.dialog_index >= 0 && entry.dialog_index < (int) filament_index_remap.size()) - filament_index_remap[entry.dialog_index] = (int) final_idx; - BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending physical filament dialog=" << entry.dialog_index - << " final=" << final_idx << " color=" << entry.color_hex << " preset=" << entry.preset_name; + if (entry.dialog_index >= 0 && entry.dialog_index < (int)filament_index_remap.size()) + filament_index_remap[entry.dialog_index] = (int)final_idx; + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending physical filament dialog=" + << entry.dialog_index << " final=" << final_idx + << " color=" << entry.color_hex + << " preset=" << entry.preset_name; ++new_physical_order; } @@ -15030,25 +14993,27 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model mixed_result.components.reserve(mixed.component_dialog_indices.size()); bool valid_components = true; for (int component_dialog_idx : mixed.component_dialog_indices) { - if (component_dialog_idx < 0 || component_dialog_idx >= (int) filament_index_remap.size() || + if (component_dialog_idx < 0 || component_dialog_idx >= (int)filament_index_remap.size() || filament_index_remap[component_dialog_idx] < 0) { valid_components = false; break; } - mixed_result.components.push_back((unsigned int) (filament_index_remap[component_dialog_idx] + 1)); + mixed_result.components.push_back((unsigned int)(filament_index_remap[component_dialog_idx] + 1)); } - if (!valid_components || mixed_result.components.size() < 2 || mixed_result.components.size() != mixed_result.ratios.size()) { - BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid pending mixed filament dialog=" << mixed.dialog_index; + if (!valid_components || mixed_result.components.size() < 2 || + mixed_result.components.size() != mixed_result.ratios.size()) { + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid pending mixed filament dialog=" + << mixed.dialog_index; continue; } - const int final_idx = (int) wxGetApp().preset_bundle->filament_presets.size(); + const int final_idx = (int)wxGetApp().preset_bundle->filament_presets.size(); if (create_mixed_filament_from_result(sidebar, mixed_result, physical_colors_for_mixing)) { - if (mixed.dialog_index >= 0 && mixed.dialog_index < (int) filament_index_remap.size()) + if (mixed.dialog_index >= 0 && mixed.dialog_index < (int)filament_index_remap.size()) filament_index_remap[mixed.dialog_index] = final_idx; physical_colors_for_mixing = collect_physical_color_strs(); - BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending mixed filament dialog=" << mixed.dialog_index - << " final=" << final_idx; + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: created pending mixed filament dialog=" + << mixed.dialog_index << " final=" << final_idx; } } @@ -15056,11 +15021,11 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model for (auto& m : remapped_matches) { if (m.filament_index < 0) continue; - if (m.filament_index < (int) filament_index_remap.size() && filament_index_remap[m.filament_index] >= 0) { + if (m.filament_index < (int)filament_index_remap.size() && filament_index_remap[m.filament_index] >= 0) { m.filament_index = filament_index_remap[m.filament_index]; } else { - BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid filament index " << m.filament_index - << " in texture mapping"; + BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: invalid filament index " + << m.filament_index << " in texture mapping"; m.filament_index = -1; } } @@ -15069,7 +15034,7 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model { std::map, int> color_to_filament; for (const auto& m : remapped_matches) { - if (m.cluster_index >= 0 && m.cluster_index < (int) painted.cluster_colors.size() && m.filament_index >= 0) + if (m.cluster_index >= 0 && m.cluster_index < (int)painted.cluster_colors.size() && m.filament_index >= 0) color_to_filament[painted.cluster_colors[m.cluster_index]] = m.filament_index + 1; } for (const auto& face_color : painted.face_colors) { @@ -15088,11 +15053,9 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model for (size_t obj_order = 0; obj_order < obj_idxs.size(); ++obj_order) { size_t idx = obj_idxs[obj_order]; - if (idx >= loaded_model.objects.size()) - continue; + if (idx >= loaded_model.objects.size()) continue; ModelObject* obj = loaded_model.objects[idx]; - if (!obj) - continue; + if (!obj) continue; // painted is derived from the whole textured mesh and is meaningful // only against a single MODEL_PART volume. Applying it to every @@ -15100,37 +15063,38 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model // volume with the same painted geometry. Restrict to the first // model_part and warn when the object holds more than one. ModelVolume* target = nullptr; - int part_count = 0; + int part_count = 0; for (ModelVolume* vol : obj->volumes) { if (vol && vol->is_model_part()) { ++part_count; - if (!target) - target = vol; + if (!target) target = vol; } } - if (!target) - continue; + if (!target) continue; if (part_count > 1) { - BOOST_LOG_TRIVIAL(warning) << "handle_textured_mesh_import: object has " << part_count - << " model parts; painting only applied to the first part."; + BOOST_LOG_TRIVIAL(warning) + << "handle_textured_mesh_import: object has " << part_count + << " model parts; painting only applied to the first part."; } - if (Slic3r::apply_painted_mesh_to_volume(painted, remapped_matches, *target) && min_used_filament_1based > 0) { + if (Slic3r::apply_painted_mesh_to_volume(painted, remapped_matches, *target) + && min_used_filament_1based > 0) { target->config.set("extruder", min_used_filament_1based); obj->config.set("extruder", min_used_filament_1based); if (update_scene) { if (auto* obj_list = wxGetApp().obj_list()) { - obj_list->update_objects_list_filament_column( - std::max(wxGetApp().filaments_cnt(), (size_t) min_used_filament_1based)); + obj_list->update_objects_list_filament_column(std::max( + wxGetApp().filaments_cnt(), (size_t)min_used_filament_1based)); obj_list->update_info_items(idx); } } - BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: set base filament to " << min_used_filament_1based - << " for object index " << idx << ", object extruder=" << obj->config.extruder() + BOOST_LOG_TRIVIAL(info) << "handle_textured_mesh_import: set base filament to " + << min_used_filament_1based << " for object index " << idx + << ", object extruder=" << obj->config.extruder() << ", volume extruder=" << target->config.extruder(); } // bbox invalidation is performed inside apply_painted_mesh_to_volume. obj->ensure_on_bed(); - const int object_percent = 25 + (int) (60 * (obj_order + 1) / std::max(obj_idxs.size(), 1)); + const int object_percent = 25 + (int)(60 * (obj_order + 1) / std::max(obj_idxs.size(), 1)); if (!update_apply_progress(object_percent, _L("Applying texture colors..."))) return; } @@ -15145,8 +15109,7 @@ void Plater::priv::apply_textured_mesh_import_result(Slic3r::Model& loaded_model update_apply_progress(100, _L("Texture colors applied.")); } -void Plater::priv::handle_textured_mesh_import(Slic3r::Model& loaded_model, - const std::vector& obj_idxs, +void Plater::priv::handle_textured_mesh_import(Slic3r::Model& loaded_model, const std::vector& obj_idxs, std::function cancel_callback) { TextureImportResult result; @@ -15156,13 +15119,13 @@ void Plater::priv::handle_textured_mesh_import(Slic3r::Model& loaded_model, apply_textured_mesh_import_result(loaded_model, obj_idxs, result); } -Sidebar& Plater::sidebar() { return *p->sidebar; } -const Model& Plater::model() const { return p->model; } -Model& Plater::model() { return p->model; } -const Print& Plater::fff_print() const { return p->fff_print; } -Print& Plater::fff_print() { return p->fff_print; } -const SLAPrint& Plater::sla_print() const { return p->sla_print; } -SLAPrint& Plater::sla_print() { return p->sla_print; } +Sidebar& Plater::sidebar() { return *p->sidebar; } +const Model& Plater::model() const { return p->model; } +Model& Plater::model() { return p->model; } +const Print& Plater::fff_print() const { return p->fff_print; } +Print& Plater::fff_print() { return p->fff_print; } +const SLAPrint& Plater::sla_print() const { return p->sla_print; } +SLAPrint& Plater::sla_print() { return p->sla_print; } int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_name) { @@ -15171,20 +15134,18 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ bool transfer_preset_changes = false; // BBS: save confirm - auto check = [this, &transfer_preset_changes](bool yes_or_no) { + auto check = [this,&transfer_preset_changes](bool yes_or_no) { m_new_project_and_check_state = true; wxString header = _L("Some presets are modified.") + "\n" + - (yes_or_no ? - _L("You can keep the modified presets for the new project or discard them") : - _L("You can keep the modified presets for the new project, discard, or save changes as new presets.")); + (yes_or_no ? _L("You can keep the modified presets for the new project or discard them") : + _L("You can keep the modified presets for the new project, discard, or save changes as new presets.")); int act_buttons = ActionButtons::KEEP | ActionButtons::REMEMBER_CHOISE; if (!yes_or_no) act_buttons |= ActionButtons::SAVE; if (m_exported_file) { //.gcode.3mf ignore presets modify m_exported_file = false; } - bool result = wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, - &transfer_preset_changes); + bool result = wxGetApp().check_and_keep_current_preset_changes(_L("Creating a new project"), header, act_buttons, &transfer_preset_changes); m_new_project_and_check_state = false; return result; }; @@ -15192,26 +15153,26 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ if (!skip_confirm && (result = close_with_confirm(check)) == wxID_CANCEL) return wxID_CANCEL; - m_only_gcode = false; - m_exported_file = false; + m_only_gcode = false; + m_exported_file = false; m_loading_project = false; get_notification_manager()->clear_all(); if (!silent) wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - // get_partplate_list().reinit(); - // get_partplate_list().update_slice_context_to_current_plate(p->background_process); - // p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); + //get_partplate_list().reinit(); + //get_partplate_list().update_slice_context_to_current_plate(p->background_process); + //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); reset(transfer_preset_changes); reset_project_dirty_after_save(); reset_project_dirty_initial_presets(); wxGetApp().update_saved_preset_from_current_preset(); update_project_dirty_from_presets(); - // reset project + //reset project p->project.reset(); - // set project name + //set project name if (project_name.empty()) p->set_project_name(_L("Untitled")); else @@ -15222,7 +15183,7 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ Model m; model().load_from(m); // new id avoid same path name - // select first plate + //select first plate get_partplate_list().select_plate(0); SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); @@ -15244,7 +15205,8 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ LoadType determine_load_type(std::string filename, std::string override_setting = ""); // BBS: FIXME, missing resotre logic -void Plater::load_project(wxString const& filename2, wxString const& originfile) +void Plater::load_project(wxString const& filename2, + wxString const& originfile) { model().calib_pa_pattern.reset(nullptr); model().plates_custom_gcodes.clear(); @@ -15252,8 +15214,9 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "filename is: " << filename2 << "and originfile is: " << originfile; BOOST_LOG_TRIVIAL(info) << __FUNCTION__; auto filename = filename2; - auto check = [&filename, this](bool yes_or_no) { - if (!yes_or_no && !wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified."))) + auto check = [&filename, this] (bool yes_or_no) { + if (!yes_or_no && !wxGetApp().check_and_save_current_preset_changes(_L("Load project"), + _L("Some presets are modified."))) return false; if (filename.empty()) { // Ask user for a project file name. @@ -15270,8 +15233,8 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) // BBS if (m_loading_project) { - // some error cases happens - // return directly + //some error cases happens + //return directly BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": current loading other project, return directly"); return; } @@ -15279,7 +15242,7 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) m_loading_project = true; ScopeGuard loading_project_sc([this]() { m_loading_project = false; }); // Make sure state restored on any early return - m_only_gcode = false; + m_only_gcode = false; m_exported_file = false; get_notification_manager()->bbl_close_plateinfo_notification(); get_notification_manager()->bbl_close_preview_only_notification(); @@ -15290,7 +15253,7 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) get_notification_manager()->close_notification_of_type(NotificationType::SlicingSeriousWarning); get_notification_manager()->close_notification_of_type(NotificationType::SlicingWarning); - auto path = into_path(filename); + auto path = into_path(filename); auto strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig; if (originfile == "") { @@ -15301,9 +15264,9 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) strategy = strategy | LoadStrategy::Restore; } else { switch (determine_load_type(filename.ToStdString())) { - case LoadType::OpenProject: break; // Do nothing - case LoadType::LoadGeometry:; strategy = LoadStrategy::LoadModel; break; - default: return; // User cancelled + case LoadType::OpenProject: break; // Do nothing + case LoadType::LoadGeometry:; strategy = LoadStrategy::LoadModel; break; + default: return; // User cancelled } } bool load_restore = strategy & LoadStrategy::Restore; @@ -15330,7 +15293,7 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) 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()) { - p->set_project_name(_L("Untitled")); + p->set_project_name(_L("Untitled")); } } else { @@ -15346,15 +15309,17 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) if (!filename.IsEmpty()) wxGetApp().mainframe->add_to_recent_projects(filename); } + } // BBS set default 3D view and direction after loading project - // p->select_view_3D("3D"); + //p->select_view_3D("3D"); if (!m_exported_file) { p->select_view("topfront"); p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - } else { + } + else { p->partplate_list.select_plate_view(); } @@ -15374,7 +15339,7 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) wxGetApp().params_panel()->switch_to_object_if_has_object_configs(); - auto has_modify = is_flush_config_modified(); + auto has_modify = is_flush_config_modified(); sidebar().set_flushing_volume_warning(has_modify); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load project done"; @@ -15383,8 +15348,8 @@ void Plater::load_project(wxString const& filename2, wxString const& originfile) // BBS: save logic int Plater::save_project(bool saveAs) { - // if (up_to_date(false, false)) // should we always save - // return; + //if (up_to_date(false, false)) // should we always save + // return; auto filename = get_project_filename(".3mf"); if (!saveAs && filename.IsEmpty()) saveAs = true; @@ -15395,18 +15360,15 @@ int Plater::save_project(bool saveAs) if (filename == "") return wxID_CANCEL; - // BBS export 3mf without gcode - auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh; + //BBS export 3mf without gcode + auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh; bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames"); if (full_pathnames) { save_strategy = save_strategy | SaveStrategy::FullPathSources; } if (export_3mf(into_path(filename), save_strategy) < 0) { - MessageDialog(this, - _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project " - "file open."), - _L("Save project"), wxOK | wxICON_WARNING) - .ShowModal(); + MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), + _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } @@ -15423,17 +15385,18 @@ int Plater::save_project(bool saveAs) try { json j; boost::uintmax_t size = boost::filesystem::file_size(into_path(filename)); - j["file_size"] = size; - j["file_name"] = std::string(filename.mb_str()); + j["file_size"] = size; + j["file_name"] = std::string(filename.mb_str()); NetworkAgent* agent = wxGetApp().getAgent(); - } catch (...) {} + } + catch (...) {} update_title_dirty_status(); return wxID_YES; } -// BBS import model by model id +//BBS import model by model id void Plater::import_model_id(wxString download_info) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " download info: " << download_info; @@ -15443,135 +15406,155 @@ void Plater::import_model_id(wxString download_info) wxString filename; wxString separator = "&name="; - try { + try + { size_t namePos = download_info.Find(separator); if (namePos != wxString::npos) { download_url = download_info.Mid(0, namePos); - filename = download_info.Mid(namePos + separator.Length()); + filename = download_info.Mid(namePos + separator.Length()); - } else { + } + else { fs::path download_path = fs::path(download_origin_url.wx_str()); - download_url = download_origin_url; - filename = download_path.filename().string(); + download_url = download_origin_url; + filename = download_path.filename().string(); } - } catch (const std::exception&) { - // wxString sError = error.what(); + } + catch (const std::exception&) + { + //wxString sError = error.what(); } - bool download_ok = false; - int retry_count = 0; + bool download_ok = false; + int retry_count = 0; const int max_retries = 3; /* jump to 3D eidtor */ wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); /* prepare progress dialog */ - bool cont = true; + bool cont = true; bool cont_dlg = true; - bool cancel = false; + bool cancel = false; wxString msg; wxString dlg_title = _L("Importing Model"); int percent = 0; - ProgressDialog dlg(dlg_title, wxString(' ', 100) + "\n\n\n\n", - 100, // range - this, // parent - wxPD_CAN_ABORT | wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH); + ProgressDialog dlg(dlg_title, + wxString(' ', 100) + "\n\n\n\n", + 100, // range + this, // parent + wxPD_CAN_ABORT | + wxPD_APP_MODAL | + wxPD_AUTO_HIDE | + wxPD_SMOOTH); boost::filesystem::path target_path; - // reset params + //reset params p->project.reset(); /* prepare project and profile */ boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &retry_count, &msg, &target_path, &download_ok, download_url, &filename] { - int res = 0; - std::string http_body; + // Orca: NetworkAgent is not needed and only prevents this from running +// NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent(); +// if (!m_agent) return; - msg = _L("Preparing 3MF file..."); + int res = 0; + std::string http_body; - // gets the number of files with the same name - std::vector vecFiles; - bool is_already_exist = false; + msg = _L("Preparing 3MF file..."); - target_path = fs::path(wxGetApp().app_config->get("download_path")); + //gets the number of files with the same name + std::vector vecFiles; + bool is_already_exist = false; - try { - vecFiles.clear(); - wxString extension = fs::path(filename.wx_str()).extension().c_str(); - // check file suffix - if (!extension.Contains(".3mf")) { - msg = _L("Download failed; unknown file format."); - return; + target_path = fs::path(wxGetApp().app_config->get("download_path")); + + try + { + vecFiles.clear(); + wxString extension = fs::path(filename.wx_str()).extension().c_str(); + + + //check file suffix + if (!extension.Contains(".3mf")) { + msg = _L("Download failed; unknown file format."); + return; + } + + auto name = filename.substr(0, filename.length() - extension.length() - 1); + + for (const auto& iter : boost::filesystem::directory_iterator(target_path)) + { + if (boost::filesystem::is_directory(iter.path())) + continue; + + wxString sFile = iter.path().filename().string().c_str(); + if (strstr(sFile.c_str(), name.c_str()) != NULL) { + vecFiles.push_back(sFile); } - auto name = filename.substr(0, filename.length() - extension.length() - 1); - - for (const auto& iter : boost::filesystem::directory_iterator(target_path)) { - if (boost::filesystem::is_directory(iter.path())) - continue; - - wxString sFile = iter.path().filename().string().c_str(); - if (strstr(sFile.c_str(), name.c_str()) != NULL) { - vecFiles.push_back(sFile); - } - - if (sFile == filename) - is_already_exist = true; - } - } catch (const std::exception&) { - // wxString sError = error.what(); + if (sFile == filename) is_already_exist = true; } + } + catch (const std::exception&) + { + //wxString sError = error.what(); + } - // update filename - if (is_already_exist && vecFiles.size() >= 1) { - wxString extension = fs::path(filename.wx_str()).extension().c_str(); - wxString name = filename.substr(0, filename.length() - extension.length()); - filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString(); - } + //update filename + if (is_already_exist && vecFiles.size() >= 1) { + wxString extension = fs::path(filename.wx_str()).extension().c_str(); + wxString name = filename.substr(0, filename.length() - extension.length()); + filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString(); + } - msg = _L("Downloading project..."); - // target_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); + msg = _L("Downloading project..."); - // target_path = wxGetApp().get_local_models_path().c_str(); - boost::uuids::uuid uuid = boost::uuids::random_generator()(); - std::string unique = to_string(uuid).substr(0, 6); + //target_path = wxStandardPaths::Get().GetTempDir().utf8_str().data(); - if (filename.empty()) { - filename = "untitled.3mf"; - } - // target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str(); - target_path /= fs::path(filename.wc_str()); + //target_path = wxGetApp().get_local_models_path().c_str(); + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + std::string unique = to_string(uuid).substr(0, 6); - fs::path tmp_path = target_path; - tmp_path += format(".%1%", ".download"); + if (filename.empty()) { + filename = "untitled.3mf"; + } - auto filesize = 0; - bool size_limit = false; - auto http = Http::get(download_url.ToStdString()); + //target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str(); + target_path /= fs::path(filename.wc_str()); - while (cont && retry_count < max_retries) { - retry_count++; - http.on_progress([&percent, &cont, &msg, &filesize, &size_limit](Http::Progress progress, bool& cancel) { - if (!cont) - cancel = true; - if (progress.dltotal != 0) { - if (filesize == 0) { - filesize = progress.dltotal; - double megabytes = static_cast(progress.dltotal) / (1024 * 1024); - // The maximum size of a 3mf file is 500mb - if (megabytes > 500) { - cont = false; - size_limit = true; - } + fs::path tmp_path = target_path; + tmp_path += format(".%1%", ".download"); + + auto filesize = 0; + bool size_limit = false; + auto http = Http::get(download_url.ToStdString()); + + while (cont && retry_count < max_retries) { + retry_count++; + http.on_progress([&percent, &cont, &msg, &filesize, &size_limit](Http::Progress progress, bool& cancel) { + + if (!cont) cancel = true; + if (progress.dltotal != 0) { + + if (filesize == 0) { + filesize = progress.dltotal; + double megabytes = static_cast(progress.dltotal) / (1024 * 1024); + //The maximum size of a 3mf file is 500mb + if (megabytes > 500) { + cont = false; + size_limit = true; } - percent = progress.dlnow * 100 / progress.dltotal; } + percent = progress.dlnow * 100 / progress.dltotal; + } if (size_limit) { msg = _L("Download failed; File size exception."); @@ -15587,31 +15570,31 @@ void Plater::import_model_id(wxString download_info) http_status, error); - if (retry_count == max_retries) { - msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it."); - cont = false; - } - }) - .on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) { + if (retry_count == max_retries) { + msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it."); + cont = false; + } + }) + .on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) { fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); file.write(body.c_str(), body.size()); file.close(); fs::rename(tmp_path, target_path); - cont = false; + cont = false; download_ok = true; - }) - .perform_sync(); + }).perform_sync(); // for break while - // cont = false; - } - }); + //cont = false; + } + + }); while (cont && cont_dlg) { wxMilliSleep(50); cont_dlg = dlg.Update(percent, msg); if (!cont_dlg) { - cont = cont_dlg; + cont = cont_dlg; cancel = true; } @@ -15632,15 +15615,13 @@ void Plater::import_model_id(wxString download_info) BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string(); /* load project */ // Orca: If download is a zip file, treat it as if file has been drag and dropped on the plater - if (target_path.extension() == ".zip") { - wxArrayString arr; - arr.Add(from_path(target_path)); - this->load_files(arr); - } else + if (target_path.extension() == ".zip") + { wxArrayString arr; arr.Add(from_path(target_path)); this->load_files(arr); } + else this->load_project(from_path(target_path)); /*BBS set project info after load project, project info is reset in load project */ - // p->project.project_model_id = model_id; - // p->project.project_design_id = design_id; + //p->project.project_model_id = model_id; + //p->project.project_design_id = design_id; AppConfig* config = wxGetApp().app_config; if (config) { p->project.project_country_code = config->get_country_code(); @@ -15649,7 +15630,8 @@ void Plater::import_model_id(wxString download_info) // show save new project p->set_project_filename(target_path.wstring()); p->notification_manager->push_import_finished_notification(target_path.string(), target_path.parent_path().string(), false); - } else { + } + else { if (!msg.empty()) { MessageDialog msg_wingow(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK); msg_wingow.SetSize(wxSize(FromDIP(480), -1)); @@ -15658,8 +15640,11 @@ void Plater::import_model_id(wxString download_info) return; } } -// BBS download project by project id -void Plater::download_project(const wxString& project_id) { return; } +//BBS download project by project id +void Plater::download_project(const wxString& project_id) +{ + return; +} void Plater::request_model_download(wxString url) { @@ -15682,7 +15667,8 @@ bool Plater::up_to_date(bool saved, bool backup) Slic3r::clear_other_changes(backup); return p->up_to_date(saved, backup); } - return p->model.objects.empty() || (p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup)); + return p->model.objects.empty() || (p->up_to_date(saved, backup) && + !Slic3r::has_other_changes(backup)); } bool Plater::add_model(bool imperial_units, std::string fname) @@ -15697,12 +15683,13 @@ bool Plater::add_model(bool imperial_units, std::string fname) for (const auto& file : input_files) paths.emplace_back(into_path(file)); - } else { + } + else { paths.emplace_back(fname); } std::string snapshot_label; - assert(!paths.empty()); + assert(! paths.empty()); if (paths.size() == 1) { snapshot_label = "Import Object"; snapshot_label += ": "; @@ -15711,7 +15698,7 @@ bool Plater::add_model(bool imperial_units, std::string fname) snapshot_label = "Import Objects"; snapshot_label += ": "; snapshot_label += paths.front().filename().string().c_str(); - for (size_t i = 1; i < paths.size(); ++i) { + for (size_t i = 1; i < paths.size(); ++ i) { snapshot_label += ", "; snapshot_label += encode_path(paths[i].filename().string().c_str()); } @@ -15723,21 +15710,11 @@ bool Plater::add_model(bool imperial_units, std::string fname) auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(paths); - if (paths.size() > 1 && amf_files_count < paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MFOther; - } - if (paths.size() > 1 && amf_files_count == paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MF; - } - if (paths.size() > 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::MultipleOther; - } - if (paths.size() == 1 && amf_files_count == 1) { - loadfiles_type = LoadFilesType::Single3MF; - }; - if (paths.size() == 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::SingleOther; - }; + if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; bool ask_multi = false; @@ -15745,10 +15722,10 @@ bool Plater::add_model(bool imperial_units, std::string fname) ask_multi = true; auto strategy = LoadStrategy::LoadModel; - if (imperial_units) - strategy = strategy | LoadStrategy::ImperialUnits; + if (imperial_units) strategy = strategy | LoadStrategy::ImperialUnits; const bool loaded = !load_files(paths, strategy, ask_multi).empty(); if (loaded) { + if (get_project_name() == _L("Untitled") && paths.size() > 0) { boost::filesystem::path full_path(paths[0].string()); p->set_project_name(from_u8(full_path.stem().string())); @@ -15764,16 +15741,22 @@ void Plater::calib_pa(const Calib_Params& params) const auto calib_pa_name = wxString::Format(L"Pressure Advance Test"); new_project(false, false, calib_pa_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false)); print_config->set_key_value("precise_z_height", new ConfigOptionBool(false)); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); switch (params.mode) { - case CalibMode::Calib_PA_Line: add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.drc"); break; - case CalibMode::Calib_PA_Pattern: _calib_pa_pattern(params); break; - case CalibMode::Calib_PA_Tower: _calib_pa_tower(params); break; - default: break; + case CalibMode::Calib_PA_Line: + add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/pressure_advance_test.drc"); + break; + case CalibMode::Calib_PA_Pattern: + _calib_pa_pattern(params); + break; + case CalibMode::Calib_PA_Tower: + _calib_pa_tower(params); + break; + default: break; } p->background_process.fff_print()->set_calib_params(params); } @@ -15784,17 +15767,17 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) std::vector accels{params.accelerations}; std::vector object_idxs{}; /* Set common parameters */ - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; DynamicPrintConfig& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); set_config_values(filament_config, "filament_retract_when_changing_layer", false); set_config_values(filament_config, "filament_wipe", false); set_config_values(printer_config, "wipe", false); set_config_values(printer_config, "retract_when_changing_layer", false); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool(false)); - // Orca: find acceleration to use in the test + //Orca: find acceleration to use in the test auto accel = print_config.get_abs_value_at("outer_wall_acceleration", params.extruder_id); // get the outer wall acceleration if (accel == 0) // if outer wall accel isnt defined, fall back to inner wall accel accel = print_config.get_abs_value_at("inner_wall_acceleration", params.extruder_id); @@ -15804,27 +15787,26 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) // is set to the travel accel before printing the pattern. if (accels.empty()) { accels.assign({accel}); - const auto msg{_L("INFO:") + "\n" + _L("No accelerations provided for calibration. Use default acceleration value ") + - std::to_string(long(accel)) + _L(u8"mm/s²")}; + const auto msg{_L("INFO:") + "\n" + + _L("No accelerations provided for calibration. Use default acceleration value ") + std::to_string(long(accel)) + _L(u8"mm/s²")}; get_notification_manager()->push_notification(msg.utf8_string()); } else { // set max acceleration in case of batch mode to get correct test pattern size accel = *std::max_element(accels.begin(), accels.end()); } set_config_values(&print_config, "outer_wall_acceleration", accel); - print_config.set_key_value("print_sequence", new ConfigOptionEnum(PrintSequence::ByLayer)); - - // Orca: find jerk value to use in the test - if (!has_junction_deviation(printer_config) && - print_config.get_abs_value_at("default_jerk", params.extruder_id) > 0) { // we have set a jerk value + print_config.set_key_value( "print_sequence", new ConfigOptionEnum(PrintSequence::ByLayer)); + + //Orca: find jerk value to use in the test + if(!has_junction_deviation(printer_config) && print_config.get_abs_value_at("default_jerk", params.extruder_id) > 0){ // we have set a jerk value auto jerk = print_config.get_abs_value_at("outer_wall_jerk", params.extruder_id); // get outer wall jerk if (jerk == 0) // if outer wall jerk is not defined, get inner wall jerk jerk = print_config.get_abs_value_at("inner_wall_jerk", params.extruder_id); if (jerk == 0) // if inner wall jerk is not defined, get the default jerk jerk = print_config.get_abs_value_at("default_jerk", params.extruder_id); - - // Orca: Set jerk values. Again first layer jerk should not matter as it is reset to the travel jerk before the - // first PA pattern is printed. + + //Orca: Set jerk values. Again first layer jerk should not matter as it is reset to the travel jerk before the + // first PA pattern is printed. set_config_values(&print_config, "default_jerk", jerk); set_config_values(&print_config, "outer_wall_jerk", jerk); set_config_values(&print_config, "inner_wall_jerk", jerk); @@ -15833,38 +15815,45 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) set_config_values(&print_config, "travel_jerk", jerk); } - if (has_junction_deviation(printer_config)) { + if (has_junction_deviation(printer_config)){ set_config_values(&print_config, "default_junction_deviation", 0); } - + for (const auto& opt : SuggestedConfigCalibPAPattern().floats_pairs) { set_config_values(&print_config, opt.first, opt.second[0]); } for (const auto& opt : SuggestedConfigCalibPAPattern().nozzle_ratio_pairs) { - print_config.set_key_value(opt.first, new ConfigOptionFloatOrPercent(nozzle_diameter * opt.second / 100, false)); + print_config.set_key_value( + opt.first, + new ConfigOptionFloatOrPercent(nozzle_diameter * opt.second / 100, false) + ); } for (const auto& opt : SuggestedConfigCalibPAPattern().int_pairs) { - print_config.set_key_value(opt.first, new ConfigOptionInt(opt.second)); + print_config.set_key_value( + opt.first, + new ConfigOptionInt(opt.second) + ); } print_config.set_key_value(SuggestedConfigCalibPAPattern().brim_pair.first, - new ConfigOptionEnum(SuggestedConfigCalibPAPattern().brim_pair.second)); + new ConfigOptionEnum(SuggestedConfigCalibPAPattern().brim_pair.second)); print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); // Orca: Set the outer wall speed to the optimal speed for the test, cap it with max volumetric speed if (speeds.empty()) { // TODO: per-variant cap - double speed = CalibPressureAdvance::find_optimal_PA_speed(wxGetApp().preset_bundle->full_config(), - print_config.get_abs_value("line_width", nozzle_diameter), - print_config.get_abs_value("layer_height"), 0, 0); + double speed = CalibPressureAdvance::find_optimal_PA_speed( + wxGetApp().preset_bundle->full_config(), + print_config.get_abs_value("line_width", nozzle_diameter), + print_config.get_abs_value("layer_height"), 0, 0); set_config_values(&print_config, "outer_wall_speed", speed); speeds.assign({speed}); - const auto msg{_L("INFO:") + "\n" + _L("No speeds provided for calibration. Use default optimal speed ") + - std::to_string(long(speed)) + _L("mm/s")}; + const auto msg{_L("INFO:") + "\n" + + _L("No speeds provided for calibration. Use default optimal speed ") + std::to_string(long(speed)) + _L("mm/s")}; get_notification_manager()->push_notification(msg.utf8_string()); } else if (speeds.size() == 1) { // If we have single value provided, set speed using global configuration. @@ -15880,15 +15869,21 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(); - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - const bool is_bbl_machine = preset_bundle->is_bbl_vendor(); - auto cur_plate = get_partplate_list().get_plate(0); + PresetBundle* preset_bundle = wxGetApp().preset_bundle; + const bool is_bbl_machine = preset_bundle->is_bbl_vendor(); + auto cur_plate = get_partplate_list().get_plate(0); // add "handle" cube sidebar().obj_list()->load_generic_subobject("Cube", ModelVolumeType::INVALID); - auto* cube = model().objects[0]; + auto *cube = model().objects[0]; - CalibPressureAdvancePattern pa_pattern(params, full_config, is_bbl_machine, *cube, cur_plate->get_origin()); + CalibPressureAdvancePattern pa_pattern( + params, + full_config, + is_bbl_machine, + *cube, + cur_plate->get_origin() + ); /* Having PA pattern configured, we could make a set of polygons resembling N test patterns. * We'll arrange this set of polygons, so we would know position of each test pattern and @@ -15898,7 +15893,8 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) * as a reference for objects arrangement. Polygon is slightly oversized to add spaces between patterns. * That arrangement will be used to place 'handle cubes' for each test. */ auto cube_bb = cube->raw_bounding_box(); - cube->scale((pa_pattern.print_size_x() + 4) / cube_bb.size().x(), (pa_pattern.print_size_y() + 4) / cube_bb.size().y(), + cube->scale((pa_pattern.print_size_x() + 4) / cube_bb.size().x(), + (pa_pattern.print_size_y() + 4) / cube_bb.size().y(), pa_pattern.max_layer_z() / cube_bb.size().z()); arrangement::ArrangePolygons arranged_items; @@ -15906,7 +15902,7 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) arrangement::ArrangeParams ap; Points bedpts = arrangement::get_shrink_bedpts(&full_config, ap); - for (size_t i = 0; i < speeds.size() * accels.size(); i++) { + for(size_t i = 0; i < speeds.size() * accels.size(); i++) { arrangement::ArrangePolygon p; cube->instances[0]->get_arrange_polygon(&p); p.bed_idx = 0; @@ -15918,23 +15914,24 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) /* scale cube back to the size of test pattern 'handle' */ cube_bb = cube->raw_bounding_box(); - cube->scale(pa_pattern.handle_xy_size() / cube_bb.size().x(), pa_pattern.handle_xy_size() / cube_bb.size().y(), + cube->scale(pa_pattern.handle_xy_size() / cube_bb.size().x(), + pa_pattern.handle_xy_size() / cube_bb.size().y(), pa_pattern.max_layer_z() / cube_bb.size().z()); /* Set speed and acceleration on per-object basis and arrange anchor object on the plates. * Test gcode will be genecated during plate slicing */ - for (size_t test_idx = 0; test_idx < arranged_items.size(); test_idx++) { - const auto& ai = arranged_items[test_idx]; + for(size_t test_idx = 0; test_idx < arranged_items.size(); test_idx++) { + const auto &ai = arranged_items[test_idx]; size_t plate_idx = arranged_items[test_idx].bed_idx; - auto tspd = speeds[test_idx % speeds.size()]; - auto tacc = accels[test_idx / speeds.size()]; + auto tspd = speeds[test_idx % speeds.size()]; + auto tacc = accels[test_idx / speeds.size()]; /* make an own copy of anchor cube for each test */ - auto obj = test_idx == 0 ? cube : model().add_object(*cube); + auto obj = test_idx == 0 ? cube : model().add_object(*cube); auto obj_idx = std::distance(model().objects.begin(), std::find(model().objects.begin(), model().objects.end(), obj)); obj->name.assign(std::string("pa_pattern_") + std::to_string(int(tspd)) + std::string("_") + std::to_string(int(tacc))); - auto& obj_config = obj->config; + auto &obj_config = obj->config; if (speeds.size() > 1) obj_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable(1, tspd)); if (accels.size() > 1) @@ -15948,7 +15945,9 @@ void Plater::_calib_pa_pattern(const Calib_Params& params) object_idxs.emplace_back(obj_idx); get_partplate_list().add_to_plate(obj_idx, 0, plate_idx); - const Vec3d obj_offset{unscale(ai.translation(X)), unscale(ai.translation(Y)), 0}; + const Vec3d obj_offset{unscale(ai.translation(X)), + unscale(ai.translation(Y)), + 0}; obj->instances[0]->set_offset(cur_plate->get_origin() + obj_offset + pa_pattern.handle_pos_offset()); obj->ensure_on_bed(); @@ -15975,23 +15974,27 @@ void Plater::_calib_pa_pattern_gen_gcode() * We'll store gcode for all tests on a single plate here. Once the plate handling is done, * all the g-codes will be merged into a single one on per-layer basis */ std::vector mgc; - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; /* iterate over all cubes on current plate and generate gcode for them */ for (auto obj : cur_plate->get_objects_on_this_plate()) { - auto gcode = model().calib_pa_pattern->generate_custom_gcodes(preset_bundle->full_config(), preset_bundle->is_bbl_vendor(), *obj, - cur_plate->get_origin()); + auto gcode = model().calib_pa_pattern->generate_custom_gcodes( + preset_bundle->full_config(), + preset_bundle->is_bbl_vendor(), + *obj, + cur_plate->get_origin() + ); mgc.emplace_back(gcode); } // move first item into model custom gcode - auto& pcgc = model().plates_custom_gcodes[get_partplate_list().get_curr_plate_index()]; - pcgc = std::move(mgc[0]); + auto &pcgc = model().plates_custom_gcodes[get_partplate_list().get_curr_plate_index()]; + pcgc = std::move(mgc[0]); mgc.erase(mgc.begin()); // concat layer gcodes for each test for (size_t i = 0; i < pcgc.gcodes.size(); i++) { - for (auto& gc : mgc) { + for (auto &gc : mgc) { pcgc.gcodes[i].extra += gc.gcodes[i].extra; } } @@ -16000,35 +16003,35 @@ void Plater::_calib_pa_pattern_gen_gcode() void Plater::cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes) { wxCHECK_RET(obj_idx < p->model.objects.size(), "obj_idx out of bounds"); - auto* object = p->model.objects[obj_idx]; + auto *object = p->model.objects[obj_idx]; wxCHECK_RET(instance_idx < object->instances.size(), "instance_idx out of bounds"); - if (!attributes.has(ModelObjectCutAttribute::KeepUpper) && !attributes.has(ModelObjectCutAttribute::KeepLower)) + if (! attributes.has(ModelObjectCutAttribute::KeepUpper) && ! attributes.has(ModelObjectCutAttribute::KeepLower)) return; wxBusyCursor wait; const Vec3d instance_offset = object->instances[instance_idx]->get_offset(); - Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ() - instance_offset), attributes); - const auto new_objects = cut.perform_with_plane(); + Cut cut(object, instance_idx, Geometry::translation_transform(z * Vec3d::UnitZ() - instance_offset), attributes); + const auto new_objects = cut.perform_with_plane(); apply_cut_object_to_model(obj_idx, new_objects); } -void Plater::_calib_pa_tower(const Calib_Params& params) -{ +void Plater::_calib_pa_tower(const Calib_Params& params) { if (!add_model(false, Slic3r::resources_dir() + "/calib/pressure_advance/tower_with_seam.drc")) return; - auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto& print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; const double nozzle_diameter = printer_config->option("nozzle_diameter")->get_at(0); print_config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{1.0f}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{ 1.0f }); + auto& obj_cfg = model().objects[0]->config; @@ -16048,7 +16051,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) obj_cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); print_config.set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); @@ -16057,7 +16060,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config(); auto new_height = std::ceil((params.end - params.start) / params.step) + 1; - auto obj_bb = model().objects[0]->bounding_box_exact(); + auto obj_bb = model().objects[0]->bounding_box_exact(); if (new_height < obj_bb.size().z()) { cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower); } @@ -16065,8 +16068,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) _calib_pa_select_added_objects(); } -void Plater::_calib_pa_select_added_objects() -{ +void Plater::_calib_pa_select_added_objects() { // update printable state for new volumes on canvas3D wxGetApp().plater()->canvas3D()->update_instance_printable_state_for_objects({0}); @@ -16088,28 +16090,27 @@ void Plater::_calib_pa_select_added_objects() // ORCA: Add pattern parameter void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, int pass, InfillPattern pattern) { - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; /// --- scale --- // model is created for a 0.4 nozzle, scale z with nozzle size. const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - std::vector extruder_types = printer_config->option("extruder_type")->values; - std::vector nozzle_volume_types = - wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; + std::vector extruder_types = printer_config->option("extruder_type")->values; + std::vector nozzle_volume_types = wxGetApp().preset_bundle->project_config.option("nozzle_volume_type")->values; assert(nozzle_diameter_config->values.size() > 0); float nozzle_diameter = nozzle_diameter_config->values[0]; - float xyScale = nozzle_diameter / 0.6; - // scale z to have 10 layers - // 2 bottom, 5 top, 3 sparse infill + float xyScale = nozzle_diameter / 0.6; + //scale z to have 10 layers + // 2 bottom, 5 top, 3 sparse infill double first_layer_height = print_config->option("initial_layer_print_height")->value; - double layer_height = nozzle_diameter / 2.0; // prefer 0.2 layer height for 0.4 nozzle - first_layer_height = std::max(first_layer_height, layer_height); + double layer_height = nozzle_diameter / 2.0; // prefer 0.2 layer height for 0.4 nozzle + first_layer_height = std::max(first_layer_height, layer_height); - const auto canvas = wxGetApp().plater()->canvas3D(); - auto& selection = canvas->get_selection(); + const auto canvas = wxGetApp().plater()->canvas3D(); + auto& selection = canvas->get_selection(); selection.setup_cache(); TransformationType transformation_type; transformation_type.set_relative(); @@ -16124,7 +16125,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i auto cur_flowrate = filament_config->option("filament_flow_ratio")->get_at(0); // TODO: per-filament param std::vector internal_solid_speeds = generate_max_speed_parameter_value("internal_solid_infill_speed", linear, pass); - std::vector top_surface_speeds = generate_max_speed_parameter_value("top_surface_speed", linear, pass); + std::vector top_surface_speeds = generate_max_speed_parameter_value("top_surface_speed", linear, pass); // adjust parameters for (auto _obj : objects) { @@ -16135,7 +16136,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i _obj->config.set_key_value("enable_extra_bridge_layer", new ConfigOptionEnum(eblDisabled)); _obj->config.set_key_value("internal_bridge_density", new ConfigOptionPercent(100)); _obj->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(35)); - _obj->config.set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(100, true)); + _obj->config.set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(100,true)); _obj->config.set_key_value("bottom_shell_layers", new ConfigOptionInt(2)); _obj->config.set_key_value("top_shell_layers", new ConfigOptionInt(5)); _obj->config.set_key_value("top_shell_thickness", new ConfigOptionFloat(0)); @@ -16150,8 +16151,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i _obj->config.set_key_value("top_solid_infill_flow_ratio", new ConfigOptionFloat(1.0f)); _obj->config.set_key_value("infill_direction", new ConfigOptionFloat(45)); _obj->config.set_key_value("solid_infill_direction", new ConfigOptionFloat(135)); - _obj->config.set_key_value("center_of_surface_pattern", - new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); + _obj->config.set_key_value("center_of_surface_pattern", new ConfigOptionEnum(CenterOfSurfacePattern::Each_Surface)); _obj->config.set_key_value("separated_infills", new ConfigOptionBool(false)); _obj->config.set_key_value("align_infill_direction_to_model", new ConfigOptionBool(true)); _obj->config.set_key_value("ironing_type", new ConfigOptionEnum(IroningType::NoIroning)); @@ -16176,18 +16176,20 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i obj_name[0] = '-'; // Orca: force set locale to C to avoid parsing error const std::string _loc = std::setlocale(LC_NUMERIC, nullptr); - std::setlocale(LC_NUMERIC, "C"); - auto modifier = 1.0f; + std::setlocale(LC_NUMERIC,"C"); + auto modifier = 1.0f; try { modifier = stof(obj_name); - } catch (...) {} + } catch (...) { + } // restore locale std::setlocale(LC_NUMERIC, _loc.c_str()); - if (linear) - _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat((cur_flowrate + modifier) / cur_flowrate)); + if(linear) + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat((cur_flowrate + modifier)/cur_flowrate)); else - _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat(1.0f + modifier / 100.f)); + _obj->config.set_key_value("print_flow_ratio", new ConfigOptionFloat(1.0f + modifier/100.f)); + } print_config->set_key_value("layer_height", new ConfigOptionFloat(layer_height)); @@ -16205,8 +16207,7 @@ void adjust_settings_for_flowrate_calib(ModelObjectPtrs& objects, bool linear, i } // ORCA: Add pattern parameter -void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) -{ +void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { if (pass != 1 && pass != 2) return; wxString calib_name; @@ -16249,28 +16250,27 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) changed_objects(object_idx); } -void Plater::calib_temp(const Calib_Params& params) -{ + +void Plater::calib_temp(const Calib_Params& params) { constexpr double base_temp_tower_nozzle_diameter = 0.4; - constexpr double base_temp_tower_block_height = 10.0; - constexpr int base_temp_tower_temp_step = 5; + constexpr double base_temp_tower_block_height = 10.0; + constexpr int base_temp_tower_temp_step = 5; const auto calib_temp_name = wxString::Format(L"Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); - if (params.mode != CalibMode::Calib_Temp_Tower) - return; - + if (params.mode != CalibMode::Calib_Temp_Tower) return; + if (!add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc")) return; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto start_temp = lround(params.start); + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto start_temp = lround(params.start); const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); - double nozzle_diameter = base_temp_tower_nozzle_diameter; + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = base_temp_tower_nozzle_diameter; if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { - nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; } if (nozzle_diameter <= 0.0) @@ -16280,7 +16280,7 @@ void Plater::calib_temp(const Calib_Params& params) const double block_height = base_temp_tower_block_height; // cut upper - auto obj_bb = model().objects[0]->bounding_box_exact(); + auto obj_bb = model().objects[0]->bounding_box_exact(); auto block_count = lround((500 - params.end) / base_temp_tower_temp_step + 1); if (block_count > 0) { // subtract EPSILON offset to avoid cutting at the exact location where the flat surface is @@ -16291,7 +16291,7 @@ void Plater::calib_temp(const Calib_Params& params) } // cut bottom - obj_bb = model().objects[0]->bounding_box_exact(); + obj_bb = model().objects[0]->bounding_box_exact(); block_count = lround((500 - params.start) / base_temp_tower_temp_step); if (block_count > 0) { auto new_height = block_count * block_height + EPSILON; @@ -16310,7 +16310,7 @@ void Plater::calib_temp(const Calib_Params& params) set_config_values(filament_config, "nozzle_temperature", (int) start_temp); // When resizing is disabled the 0.4 mm / 0.2 mm reference model is printed as-is (preset layer height kept). if (params.nozzle_based_resize) - model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter / 2)); + model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter/2)); model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum(btOuterOnly)); model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(5.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); @@ -16322,9 +16322,10 @@ void Plater::calib_temp(const Calib_Params& params) auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); if (params.nozzle_based_resize) - print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter / 2)); + print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter/2)); - changed_objects({0}); + + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config(); @@ -16343,23 +16344,23 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/volumetric_speed/SpeedTestStructure.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto obj = model().objects[0]; - auto& obj_cfg = obj->config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; + auto& obj_cfg = obj->config; - auto bed_shape = printer_config->option("printable_area")->values; + auto bed_shape = printer_config->option("printable_area")->values; BoundingBoxf bed_ext = get_extents(bed_shape); - auto scale_obj = (bed_ext.size().x() - 10) / obj->bounding_box_exact().size().x(); + auto scale_obj = (bed_ext.size().x() - 10) / obj->bounding_box_exact().size().x(); if (scale_obj < 1.0) obj->scale(scale_obj, 1, 1); const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); assert(nozzle_diameter_config->values.size() > 0); double nozzle_diameter = nozzle_diameter_config->values[0]; - double line_width = nozzle_diameter * 1.75; - double layer_height = nozzle_diameter * 0.8; + double line_width = nozzle_diameter * 1.75; + double layer_height = nozzle_diameter * 0.8; auto max_lh = printer_config->option("max_layer_height"); for (size_t i = 0; i < max_lh->values.size(); ++i) { @@ -16368,8 +16369,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) } const double filament_max_volumetric_speed = filament_config->option("filament_max_volumetric_speed")->get_at(0); - set_config_values(filament_config, "filament_max_volumetric_speed", - std::max(filament_max_volumetric_speed, 200.0)); + set_config_values(filament_config, "filament_max_volumetric_speed", std::max(filament_max_volumetric_speed, 200.0)); filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); obj_cfg.set_key_value("enable_overhang_speed", new ConfigOptionBoolsNullable(1, false)); @@ -16389,7 +16389,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) print_config->set_key_value("max_volumetric_extrusion_rate_slope", new ConfigOptionFloat(0)); print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINTER)->update_dirty(); @@ -16405,8 +16405,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params) } auto new_params = params; - auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * - filament_config->option("filament_flow_ratio")->get_at(0); + auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option("filament_flow_ratio")->get_at(0); new_params.end = params.end / mm3_per_mm; new_params.start = params.start / mm3_per_mm; new_params.step = params.step / mm3_per_mm; @@ -16425,10 +16424,10 @@ void Plater::calib_retraction(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/retraction/retraction_tower.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; - auto obj = model().objects[0]; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto obj = model().objects[0]; print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); @@ -16444,8 +16443,7 @@ void Plater::calib_retraction(const Calib_Params& params) auto max_lh = printer_config->option("max_layer_height"); for (size_t i = 0; i < max_lh->values.size(); ++i) { - if (max_lh->values[i] < layer_height) - max_lh->values[i] = layer_height; + if (max_lh->values[i] < layer_height) max_lh->values[i] = layer_height; } printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); @@ -16462,7 +16460,8 @@ void Plater::calib_retraction(const Calib_Params& params) obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false)); obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false)); - changed_objects({0}); + + changed_objects({ 0 }); // cut upper auto obj_bb = obj->bounding_box_exact(); @@ -16484,15 +16483,15 @@ void Plater::calib_VFA(const Calib_Params& params) if (!add_model(false, Slic3r::resources_dir() + "/calib/vfa/vfa.drc")) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const ConfigOptionFloats* nozzle_diameter_config = printer_config->option("nozzle_diameter"); - size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); - double nozzle_diameter = vfa_base_nozzle_diameter; + size_t nozzle_id = static_cast(std::max(params.extruder_id, 0)); + double nozzle_diameter = vfa_base_nozzle_diameter; if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) { - nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); + nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1); nozzle_diameter = nozzle_diameter_config->values[nozzle_id]; } if (nozzle_diameter <= 0.0) @@ -16524,7 +16523,7 @@ void Plater::calib_VFA(const Calib_Params& params) model().objects[0]->ensure_on_bed(); printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); set_config_values(print_config, "enable_overhang_speed", false); print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); @@ -16544,7 +16543,7 @@ void Plater::calib_VFA(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -16552,7 +16551,7 @@ void Plater::calib_VFA(const Calib_Params& params) // Pass the resolved layer height on (only meaningful when resized). GCode's VFA stepping is layer-based, so // it does not require it, but keep it consistent with the geometry. - Calib_Params calib_params = params; + Calib_Params calib_params = params; calib_params.vfa_layer_height = params.nozzle_based_resize ? layer_height : 0.0; p->background_process.fff_print()->set_calib_params(calib_params); } @@ -16565,31 +16564,20 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) if (params.mode != CalibMode::Calib_Input_shaping_freq) return; - if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : - "/calib/input_shaping/fast_tower_test.drc"))) + if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); if (has_junction_deviation(printer_config)) { - printer_config - ->set_key_value("machine_max_junction_deviation", - new ConfigOptionFloats{ - (std::max(printer_config->option("machine_max_junction_deviation")->values.front(), - 0.25))}); + printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); set_config_values(print_config, "default_junction_deviation", 0); } else { const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; - printer_config->set_key_value("machine_max_jerk_x", - new ConfigOptionFloats{ - std::max(printer_config->option("machine_max_jerk_x")->values.front(), - jerk_value)}); - printer_config->set_key_value("machine_max_jerk_y", - new ConfigOptionFloats{ - std::max(printer_config->option("machine_max_jerk_y")->values.front(), - jerk_value)}); + printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); set_config_values(print_config, "default_jerk", 0); } @@ -16601,8 +16589,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2)); set_config_values(print_config, "enable_overhang_speed", false); @@ -16615,8 +16603,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), - printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -16626,7 +16613,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -16643,31 +16630,20 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) if (params.mode != CalibMode::Calib_Input_shaping_damp) return; - if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : - "/calib/input_shaping/fast_tower_test.drc"))) + if (!add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"))) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; - auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; + auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; const auto gcode_flavor_option = printer_config->option>("gcode_flavor"); if (has_junction_deviation(printer_config)) { - printer_config - ->set_key_value("machine_max_junction_deviation", - new ConfigOptionFloats{ - (std::max(printer_config->option("machine_max_junction_deviation")->values.front(), - 0.25))}); + printer_config->set_key_value("machine_max_junction_deviation", new ConfigOptionFloats {(std::max(printer_config->option("machine_max_junction_deviation")->values.front(), 0.25))}); set_config_values(print_config, "default_junction_deviation", 0); } else { const double jerk_value = (gcode_flavor_option && gcode_flavor_option->value == GCodeFlavor::gcfKlipper) ? 5.0 : 10.0; - printer_config->set_key_value("machine_max_jerk_x", - new ConfigOptionFloats{ - std::max(printer_config->option("machine_max_jerk_x")->values.front(), - jerk_value)}); - printer_config->set_key_value("machine_max_jerk_y", - new ConfigOptionFloats{ - std::max(printer_config->option("machine_max_jerk_y")->values.front(), - jerk_value)}); + printer_config->set_key_value("machine_max_jerk_x", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_x")->values.front(), jerk_value)}); + printer_config->set_key_value("machine_max_jerk_y", new ConfigOptionFloats{std::max(printer_config->option("machine_max_jerk_y")->values.front(), jerk_value)}); set_config_values(print_config, "default_jerk", 0); } @@ -16679,8 +16655,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{false}); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); set_config_values(print_config, "enable_overhang_speed", false); print_config->set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); @@ -16692,8 +16668,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), - printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -16703,7 +16678,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -16720,12 +16695,12 @@ void Plater::Calib_Cornering(const Calib_Params& params) if (params.mode != CalibMode::Calib_Cornering) return; - const std::string cornering_model_path = params.test_model == 0 ? "/calib/input_shaping/ringing_tower.drc" : - (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : - "/calib/cornering/SCV-V2.drc"); + const std::string cornering_model_path = params.test_model == 0 + ? "/calib/input_shaping/ringing_tower.drc" + : (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc"); if (!add_model(false, Slic3r::resources_dir() + cornering_model_path)) return; - auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config; auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config; auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config; @@ -16747,8 +16722,8 @@ void Plater::Calib_Cornering(const Calib_Params& params) printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false}); printer_config->set_key_value("input_shaping_emit", new ConfigOptionBool{true}); printer_config->set_key_value("input_shaping_type", new ConfigOptionEnum(InputShaperType::Disable)); - filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0}); - filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats{0.0}); + filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 }); + filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 }); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); const double filament_max_volumetric_speed = filament_config->option("filament_max_volumetric_speed")->get_at(0); filament_config->set_key_value("filament_max_volumetric_speed", new ConfigOptionFloats{std::max(filament_max_volumetric_speed, 200.0)}); @@ -16762,8 +16737,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) print_config->set_key_value("spiral_mode", new ConfigOptionBool(true)); print_config->set_key_value("spiral_mode_smooth", new ConfigOptionBool(false)); print_config->set_key_value("bottom_surface_pattern", new ConfigOptionEnum(ipRectilinear)); - const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), - printer_config->option("machine_max_speed_y")->get_at(0)); + const double machine_max_speed = std::min(printer_config->option("machine_max_speed_x")->get_at(0), printer_config->option("machine_max_speed_y")->get_at(0)); const double machine_max_acceleration = printer_config->option("machine_max_acceleration_extruding")->get_at(0); set_config_values(print_config, "outer_wall_speed", machine_max_speed); set_config_values(print_config, "default_acceleration", machine_max_acceleration); @@ -16773,7 +16747,7 @@ void Plater::Calib_Cornering(const Calib_Params& params) model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0)); model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0)); - changed_objects({0}); + changed_objects({ 0 }); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty(); wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings(); @@ -16798,7 +16772,7 @@ void Plater::import_zip_archive() void Plater::import_sl1_archive() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle() && p->m_sla_import_dlg->ShowModal() == wxID_OK) { p->take_snapshot(_u8L("Import SLA archive")); replace_job(w, std::make_unique(p->m_sla_import_dlg)); @@ -16810,8 +16784,8 @@ void Plater::extract_config_from_project() wxString input_file; wxGetApp().load_project(this, input_file); - if (!input_file.empty()) - load_files({into_path(input_file)}, LoadStrategy::LoadConfig); + if (! input_file.empty()) + load_files({ into_path(input_file) }, LoadStrategy::LoadConfig); } void Plater::load_gcode() @@ -16823,12 +16797,14 @@ void Plater::load_gcode() load_gcode(input_file); } -// BBS: remove GCodeViewer as seperate APP logic +//BBS: remove GCodeViewer as seperate APP logic void Plater::load_gcode(const wxString& filename) { BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " entry and filename: " << filename; BOOST_LOG_TRIVIAL(info) << __FUNCTION__; - if (!is_gcode_file(into_u8(filename)) || (m_last_loaded_gcode == filename && m_only_gcode)) + if (! is_gcode_file(into_u8(filename)) + || (m_last_loaded_gcode == filename && m_only_gcode) + ) return; m_last_loaded_gcode = filename; @@ -16840,37 +16816,40 @@ void Plater::load_gcode(const wxString& filename) m_only_gcode = true; // cleanup view before to start loading/processing - // BBS: update gcode to current partplate's + //BBS: update gcode to current partplate's GCodeProcessorResult* current_result = p->partplate_list.get_current_slice_result(); - Print& current_print = p->partplate_list.get_current_fff_print(); - // BBS:already reset in new_project - // current_result->reset(); - // p->gcode_result.reset(); - // reset_gcode_toolpaths(); + Print& current_print = p->partplate_list.get_current_fff_print(); + //BBS:already reset in new_project + //current_result->reset(); + //p->gcode_result.reset(); + //reset_gcode_toolpaths(); p->preview->reload_print(m_only_gcode); wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW); p->set_current_panel(p->preview, true); p->get_current_canvas3D()->render(); - // p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); + //p->notification_manager->bbl_show_plateinfo_notification(into_u8(_L("Preview only mode for gcode file."))); wxBusyCursor wait; // process gcode GCodeProcessor processor; processor.init_filament_maps_and_nozzle_type_when_import_only_gcode(); - try { + try + { GCodeProcessor::s_IsBBLPrinter = wxGetApp().preset_bundle->is_bbl_vendor(); processor.process_file(filename.ToUTF8().data()); - } catch (const std::exception& ex) { + } + catch (const std::exception& ex) + { show_error(this, ex.what()); return; } *current_result = std::move(processor.extract_result()); - // current_result->filename = filename; + //current_result->filename = filename; BedType bed_type = current_result->bed_type; if (bed_type != BedType::btCount) { - DynamicPrintConfig& proj_config = wxGetApp().preset_bundle->project_config; + DynamicPrintConfig &proj_config = wxGetApp().preset_bundle->project_config; proj_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); on_bed_type_change(bed_type); } @@ -16879,14 +16858,14 @@ void Plater::load_gcode(const wxString& filename) current_print.apply_config_for_render(processor.export_config_for_render()); - // BBS: add cost info when drag in gcode - auto& ps = current_result->print_statistics; + //BBS: add cost info when drag in gcode + auto& ps = current_result->print_statistics; double total_cost = 0.0; for (auto volume : ps.total_volumes_per_extruder) { size_t extruder_id = volume.first; - double density = current_result->filament_densities.at(extruder_id); - double cost = current_result->filament_costs.at(extruder_id); - double weight = volume.second * density * 0.001; + double density = current_result->filament_densities.at(extruder_id); + double cost = current_result->filament_costs.at(extruder_id); + double weight = volume.second * density * 0.001; total_cost += weight * cost * 0.001; } current_print.print_statistics().total_cost = total_cost; @@ -16895,15 +16874,13 @@ void Plater::load_gcode(const wxString& filename) // show results p->preview->reload_print(m_only_gcode); - // BBS: zoom to bed 0 for gcode preview - // p->preview->get_canvas3d()->zoom_to_gcode(); + //BBS: zoom to bed 0 for gcode preview + //p->preview->get_canvas3d()->zoom_to_gcode(); p->preview->get_canvas3d()->zoom_to_plate(0); if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) { MessageDialog(this, _L("The selected file") + ":\n" + filename + "\n" + _L("Does not contain valid G-code."), - wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), - wxCLOSE | wxICON_WARNING | wxCENTRE) - .ShowModal(); + wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal(); set_project_filename(DEFAULT_PROJECT_NAME); } else { set_project_filename(filename); @@ -16914,7 +16891,7 @@ void Plater::load_gcode(const wxString& filename) p->view3D->get_canvas3d()->remove_raycasters_for_picking(SceneRaycaster::EType::Bed); } - p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, false); // 20250416 ban gcode to send print + p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, false); //20250416 ban gcode to send print } void Plater::reload_gcode_from_disk() @@ -16924,43 +16901,45 @@ void Plater::reload_gcode_from_disk() load_gcode(filename); } -void Plater::reload_print() { p->preview->reload_print(); } +void Plater::reload_print() +{ + p->preview->reload_print(); +} // BBS -wxString Plater::get_project_name() { return p->get_project_name(); } +wxString Plater::get_project_name() +{ + return p->get_project_name(); +} void Plater::update_all_plate_thumbnails(bool force_update) { for (int i = 0; i < get_partplate_list().get_plate_count(); i++) { - PartPlate* plate = get_partplate_list().get_plate(i); - ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; + PartPlate* plate = get_partplate_list().get_plate(i); + ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i}; if (force_update || !plate->thumbnail_data.is_valid()) { - get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, - thumbnail_params, Camera::EType::Ortho); + get_view3D_canvas3D()->render_thumbnail(plate->thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho); } if (force_update || !plate->no_light_thumbnail_data.is_valid()) { - get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, - plate->plate_thumbnail_height, thumbnail_params, Camera::EType::Ortho, - Camera::ViewAngleType::Iso, false, true); + get_view3D_canvas3D()->render_thumbnail(plate->no_light_thumbnail_data, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); } } } -void Plater::update_obj_preview_thumbnail( - ModelObject* mo, int obj_idx, int vol_idx, std::vector colors, int camera_view_angle_type) +void Plater::update_obj_preview_thumbnail(ModelObject *mo, int obj_idx, int vol_idx, std::vector colors, int camera_view_angle_type) { - PartPlate* plate = get_partplate_list().get_plate(0); + PartPlate * plate = get_partplate_list().get_plate(0); ThumbnailsParams thumbnail_params = {{}, false, true, true, true, 0, false}; GLVolumeCollection cur_volumes; cur_volumes.load_object_volume(mo, obj_idx, vol_idx, 0, "volume", true, false, false, false); ModelObjectPtrs model_objects; model_objects.emplace_back(mo); - get_view3D_canvas3D()->render_thumbnail(plate->obj_preview_thumbnail_data, colors, plate->plate_thumbnail_width, - plate->plate_thumbnail_height, thumbnail_params, model_objects, cur_volumes, - Camera::EType::Ortho, (Camera::ViewAngleType) camera_view_angle_type, false, false); + get_view3D_canvas3D()->render_thumbnail(plate->obj_preview_thumbnail_data, colors, plate->plate_thumbnail_width, plate->plate_thumbnail_height, thumbnail_params, + model_objects, cur_volumes, Camera::EType::Ortho, (Camera::ViewAngleType) camera_view_angle_type, false, false); } -// invalid all plate's thumbnails +//invalid all plate's thumbnails void Plater::invalid_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) @@ -16977,28 +16956,29 @@ void Plater::invalid_all_plate_thumbnails() void Plater::force_update_all_plate_thumbnails() { if (using_exported_file() || skip_thumbnail_invalid) { - } else { + } + else { invalid_all_plate_thumbnails(); update_all_plate_thumbnails(true); } } // BBS: backup -std::vector Plater::load_files(const std::vector& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) -{ - // BBS: wish to reset state when load a new file +std::vector Plater::load_files(const std::vector& 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 + //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, published_out); } bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) { - // std::vector unzipped_paths; + //std::vector unzipped_paths; std::vector non_project_paths; std::vector project_paths; - try { + try + { mz_zip_archive archive; mz_zip_zero_struct(&archive); @@ -17012,14 +16992,15 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) // selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path. std::vector> selected_paths; FileArchiveDialog dlg(static_cast(wxGetApp().mainframe), &archive, selected_paths); - if (dlg.ShowModal() == wxID_OK) { + if (dlg.ShowModal() == wxID_OK) + { std::string archive_path_string = archive_path.string(); - archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); + archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4); fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data()); for (auto& path_w_size : selected_paths) { const fs::path& path = path_w_size.first; - size_t size = path_w_size.second; + size_t size = path_w_size.second; // find path in zip archive for (mz_uint i = 0; i < num_entries; ++i) { if (mz_zip_reader_file_stat(&archive, i, &stat)) { @@ -17033,29 +17014,29 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) if (path != archive_path) continue; // decompressing - try { + try + { std::replace(name.begin(), name.end(), '\\', '/'); // rename if file exists - std::string filename = path.filename().string(); - std::string extension = path.extension().string(); - std::string just_filename = filename.substr(0, filename.size() - extension.size()); + std::string filename = path.filename().string(); + std::string extension = path.extension().string(); + std::string just_filename = filename.substr(0, filename.size() - extension.size()); std::string final_filename = just_filename; size_t version = 0; - while (fs::exists(archive_dir / (final_filename + extension))) { + while (fs::exists(archive_dir / (final_filename + extension))) + { ++version; final_filename = just_filename + "(" + std::to_string(version) + ")"; } - filename = final_filename + extension; + filename = final_filename + extension; fs::path final_path = archive_dir / filename; - std::string buffer((size_t) stat.m_uncomp_size, 0); + std::string buffer((size_t)stat.m_uncomp_size, 0); // Decompress action. We already has correct file index in stat structure. - mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*) buffer.data(), - (size_t) stat.m_uncomp_size, 0); + mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0); if (res == 0) { // TRN: First argument = path to file, second argument = error description - wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), - mz_zip_get_error_string(mz_zip_get_last_error(&archive))); + wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive))); BOOST_LOG_TRIVIAL(error) << error_log; show_error(nullptr, error_log); break; @@ -17065,9 +17046,7 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) file.write(buffer.c_str(), buffer.size()); file.close(); if (!fs::exists(final_path)) { - wxString error_log = - GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), - final_path.string()); + wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string()); BOOST_LOG_TRIVIAL(error) << error_log; show_error(nullptr, error_log); break; @@ -17086,7 +17065,9 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) project_paths.emplace_back(final_path); break; - } catch (const std::exception& e) { + } + catch (const std::exception& e) + { // ensure the zip archive is closed and rethrow the exception close_zip_reader(&archive); throw Slic3r::FileIOError(e.what()); @@ -17096,25 +17077,30 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) } close_zip_reader(&archive); if (non_project_paths.size() + project_paths.size() != selected_paths.size()) - BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " << selected_paths.size() - << " Decopressed files: " << non_project_paths.size() + project_paths.size(); + BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: " + << selected_paths.size() + << " Decopressed files: " + << non_project_paths.size() + project_paths.size(); } else { close_zip_reader(&archive); return false; } - } catch (const Slic3r::FileIOError& e) { + } + catch (const Slic3r::FileIOError& e) { // zip reader should be already closed or not even opened GUI::show_error(this, e.what()); return false; } // none selected - if (project_paths.empty() && non_project_paths.empty()) { + if (project_paths.empty() && non_project_paths.empty()) + { return false; } // 1 project file and some models - behave like drag n drop of 3mf and then load models - if (project_paths.size() == 1) { + if (project_paths.size() == 1) + { wxArrayString aux; aux.Add(from_u8(project_paths.front().string())); bool loaded3mf = load_files(aux); @@ -17139,6 +17125,7 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) load_files(project_paths, LoadStrategy::LoadModel); load_files(non_project_paths, LoadStrategy::LoadModel); + for (const fs::path& path : project_paths) { // Delete file from temp file (path variable), it will stay only in app memory. boost::system::error_code ec; @@ -17162,32 +17149,32 @@ bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path) class ProjectDropDialog : public DPIDialog { private: - wxColour m_def_color = wxColour(255, 255, 255); - int m_action{1}; - bool m_remember_choice{false}; + wxColour m_def_color = wxColour(255, 255, 255); + int m_action{1}; + bool m_remember_choice{false}; public: - ProjectDropDialog(const std::string& filename); + ProjectDropDialog(const std::string &filename); - wxPanel* m_top_line; - wxStaticText* m_fname_title; - wxStaticText* m_fname_f; - StaticBox* m_panel_select; + wxPanel * m_top_line; + wxStaticText *m_fname_title; + wxStaticText *m_fname_f; + StaticBox * m_panel_select; - void on_select_ok(wxCommandEvent& event); - void on_select_cancel(wxCommandEvent& event); + void on_select_ok(wxCommandEvent &event); + void on_select_cancel(wxCommandEvent &event); - int get_action() const { return m_action; } - void set_action(int index) { m_action = index; } + int get_action() const { return m_action; } + void set_action(int index) { m_action = index; } - wxBoxSizer* create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip); + wxBoxSizer *create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip); protected: - void on_dpi_changed(const wxRect& suggested_rect) override; + void on_dpi_changed(const wxRect &suggested_rect) override; }; -ProjectDropDialog::ProjectDropDialog(const std::string& filename) - : DPIDialog(static_cast(wxGetApp().mainframe), +ProjectDropDialog::ProjectDropDialog(const std::string &filename) + : DPIDialog(static_cast(wxGetApp().mainframe), wxID_ANY, from_u8((boost::format(_utf8(L("Drop project file")))).str()), wxDefaultPosition, @@ -17198,7 +17185,7 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename) // def setting SetBackgroundColour(m_def_color); - wxBoxSizer* m_sizer_main = new wxBoxSizer(wxVERTICAL); + wxBoxSizer *m_sizer_main = new wxBoxSizer(wxVERTICAL); m_top_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); m_top_line->SetBackgroundColour(wxColour(166, 169, 170)); @@ -17214,25 +17201,24 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename) m_fname_f = new wxStaticText(this, wxID_ANY, filename); m_fname_f->SetFont(::Label::Head_14); - m_fname_f->SetMaxSize(wxSize(FromDIP(300), -1)); + m_fname_f->SetMaxSize(wxSize(FromDIP(300),-1)); m_fname_f->Wrap(FromDIP(300)); m_fname_f->SetForegroundColour(wxColour("#363636")); m_sizer_main->Add(m_fname_title, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); m_sizer_main->AddSpacer(FromDIP(10)); - m_sizer_main->Add(m_fname_f, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); + m_sizer_main->Add(m_fname_f , 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); m_sizer_main->AddSpacer(FromDIP(10)); - auto radio_group = new RadioGroup(this, - { - _L("Open as project"), // 0 - _L("Import geometry only") // 1 - }, - wxVERTICAL); - radio_group->SetMinSize(wxSize(FromDIP(300), -1)); + auto radio_group = new RadioGroup(this, { + _L("Open as project"), // 0 + _L("Import geometry only") // 1 + }, wxVERTICAL); + radio_group->SetMinSize(wxSize(FromDIP(300),-1)); radio_group->SetSelection(get_action() - 1); - radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, - [this, radio_group](wxCommandEvent& e) { set_action(radio_group->GetSelection() + 1); }); + radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this, radio_group](wxCommandEvent &e) { + set_action(radio_group->GetSelection() + 1); + }); m_sizer_main->Add(radio_group, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(20)); @@ -17242,8 +17228,8 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename) // Orca: hide the "Don't show again" checkbox, people keeps accidentally checked this then forgot // wxBoxSizer *m_sizer_left = new wxBoxSizer(wxHORIZONTAL); // - // auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, - // under 'Load Behaviour'.")); m_sizer_left->Add(dont_show_again, 0, wxALL, 5); + // auto dont_show_again = create_remember_checkbox(_L("Remember my choice."), this, _L("This option can be changed later in preferences, under 'Load Behaviour'.")); + // m_sizer_left->Add(dont_show_again, 0, wxALL, 5); // // m_sizer_bottom->Add(m_sizer_left, 0, wxEXPAND, 5); @@ -17263,9 +17249,9 @@ ProjectDropDialog::ProjectDropDialog(const std::string& filename) wxGetApp().UpdateDlgDarkUI(this); } -wxBoxSizer* ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow* parent, wxString tooltip) +wxBoxSizer *ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow *parent, wxString tooltip) { - wxBoxSizer* m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); + wxBoxSizer *m_sizer_checkbox = new wxBoxSizer(wxHORIZONTAL); m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 5); auto checkbox = new ::CheckBox(parent); @@ -17275,13 +17261,13 @@ wxBoxSizer* ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8); auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxSize(-1, -1), 0); - checkbox_title->SetForegroundColour(wxColour(144, 144, 144)); + checkbox_title->SetForegroundColour(wxColour(144,144,144)); checkbox_title->SetFont(::Label::Body_13); checkbox_title->Wrap(-1); checkbox_title->SetToolTip(tooltip); m_sizer_checkbox->Add(checkbox_title, 0, wxALIGN_CENTER | wxALL, 3); - checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent& e) { + checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, checkbox](wxCommandEvent &e) { m_remember_choice = checkbox->GetValue(); e.Skip(); }); @@ -17289,24 +17275,28 @@ wxBoxSizer* ProjectDropDialog::create_remember_checkbox(wxString title, wxWindow return m_sizer_checkbox; } -void ProjectDropDialog::on_select_ok(wxCommandEvent& event) +void ProjectDropDialog::on_select_ok(wxCommandEvent &event) { if (m_remember_choice) { LoadType load_type = static_cast(get_action()); - switch (load_type) { - case LoadType::OpenProject: - wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL); - break; - case LoadType::LoadGeometry: - wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY); - break; + switch (load_type) + { + case LoadType::OpenProject: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_ALL); + break; + case LoadType::LoadGeometry: + wxGetApp().app_config->set(SETTING_PROJECT_LOAD_BEHAVIOUR, OPTION_PROJECT_LOAD_BEHAVIOUR_LOAD_GEOMETRY); + break; } } EndModal(wxID_OK); } -void ProjectDropDialog::on_select_cancel(wxCommandEvent& event) { EndModal(wxID_CANCEL); } +void ProjectDropDialog::on_select_cancel(wxCommandEvent &event) +{ + EndModal(wxID_CANCEL); +} void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) { @@ -17314,7 +17304,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect) Refresh(); } -// BBS: remove GCodeViewer as seperate APP logic +//BBS: remove GCodeViewer as seperate APP logic bool Plater::load_files(const wxArrayString& filenames) { const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase); @@ -17333,14 +17323,14 @@ bool Plater::load_files(const wxArrayString& filenames) continue; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": normal_paths %1%, gcode_paths %2%") % normal_paths.size() % gcode_paths.size(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": normal_paths %1%, gcode_paths %2%")%normal_paths.size() %gcode_paths.size(); if (normal_paths.empty() && gcode_paths.empty()) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": can not find valid path, return directly"); // Likely no supported files return false; - } else if (normal_paths.empty()) { - // only gcode files + } + else if (normal_paths.empty()){ + //only gcode files if (gcode_paths.size() > 1) { show_info(this, _L("Only one G-code file can be opened at a time."), _L("G-code loading")); return false; @@ -17355,12 +17345,12 @@ bool Plater::load_files(const wxArrayString& filenames) } //// searches for project files - // for (std::vector::const_reverse_iterator it = normal_paths.rbegin(); it != normal_paths.rend(); ++it) { - // std::string filename = (*it).filename().string(); - // ////BBS: only 3mf will be treated as project file - // if (open_3mf_file((*it))) - // return true; - // } + //for (std::vector::const_reverse_iterator it = normal_paths.rbegin(); it != normal_paths.rend(); ++it) { + // std::string filename = (*it).filename().string(); + // ////BBS: only 3mf will be treated as project file + // if (open_3mf_file((*it))) + // return true; + //} //// other files std::string snapshot_label; @@ -17379,31 +17369,20 @@ bool Plater::load_files(const wxArrayString& filenames) } } - // Plater::TakeSnapshot snapshot(this, snapshot_label); - // load_files(normal_paths, LoadStrategy::LoadModel); + //Plater::TakeSnapshot snapshot(this, snapshot_label); + //load_files(normal_paths, LoadStrategy::LoadModel); // BBS: check file types - std::sort(normal_paths.begin(), normal_paths.end(), - [](fs::path obj1, fs::path obj2) { return obj1.filename().string() < obj2.filename().string(); }); + std::sort(normal_paths.begin(), normal_paths.end(), [](fs::path obj1, fs::path obj2) { return obj1.filename().string() < obj2.filename().string(); }); auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(normal_paths); - if (normal_paths.size() > 1 && amf_files_count < normal_paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MFOther; - } - if (normal_paths.size() > 1 && amf_files_count == normal_paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MF; - } - if (normal_paths.size() > 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::MultipleOther; - } - if (normal_paths.size() == 1 && amf_files_count == 1) { - loadfiles_type = LoadFilesType::Single3MF; - }; - if (normal_paths.size() == 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::SingleOther; - }; + if (normal_paths.size() > 1 && amf_files_count < normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (normal_paths.size() > 1 && amf_files_count == normal_paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (normal_paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (normal_paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (normal_paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; auto first_file = std::vector{}; auto tmf_file = std::vector{}; @@ -17411,7 +17390,8 @@ bool Plater::load_files(const wxArrayString& filenames) auto res = true; if (this->m_only_gcode || this->m_exported_file) { - if ((loadfiles_type == LoadFilesType::SingleOther) || (loadfiles_type == LoadFilesType::MultipleOther)) { + if ((loadfiles_type == LoadFilesType::SingleOther) + || (loadfiles_type == LoadFilesType::MultipleOther)) { show_info(this, _L("Unable to add models in preview mode"), _L("Add Models")); return false; } @@ -17433,46 +17413,38 @@ bool Plater::load_files(const wxArrayString& filenames) }; switch (loadfiles_type) { - case LoadFilesType::Single3MF: open_3mf_file(normal_paths[0]); break; + case LoadFilesType::Single3MF: + open_3mf_file(normal_paths[0]); + break; case LoadFilesType::SingleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); - if (handle_zips(normal_paths)) - return true; - if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { - res = false; - } + if (handle_zips(normal_paths)) return true; + if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { res = false; } break; } case LoadFilesType::Multiple3MF: first_file = std::vector{normal_paths[0]}; for (auto i = 0; i < normal_paths.size(); i++) { - if (i > 0) { - other_file.push_back(normal_paths[i]); - } + if (i > 0) { other_file.push_back(normal_paths[i]); } }; open_3mf_file(first_file[0]); - if (load_files(other_file, LoadStrategy::LoadModel).empty()) { - res = false; - } + if (load_files(other_file, LoadStrategy::LoadModel).empty()) { res = false; } break; case LoadFilesType::MultipleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); if (handle_zips(normal_paths)) { - if (normal_paths.empty()) - return true; - } - if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { - res = false; + if (normal_paths.empty()) return true; } + if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { res = false; } break; } case LoadFilesType::Multiple3MFOther: - for (const auto& path : normal_paths) { - if (boost::iends_with(path.filename().string(), ".3mf")) { + for (const auto &path : normal_paths) { + if (boost::iends_with(path.filename().string(), ".3mf")){ if (first_file.size() <= 0) first_file.push_back(path); else @@ -17483,16 +17455,11 @@ bool Plater::load_files(const wxArrayString& filenames) } open_3mf_file(first_file[0]); - if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { - res = false; - } + if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { res = false; } if (res && handle_zips(other_file)) { - if (normal_paths.empty()) - return true; - } - if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { - res = false; + if (normal_paths.empty()) return true; } + if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; } break; default: break; } @@ -17515,7 +17482,7 @@ LoadType determine_load_type(std::string filename, std::string override_setting) } else if (setting == OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK) { ProjectDropDialog dlg(filename); if (dlg.ShowModal() == wxID_OK) { - int choice = dlg.get_action(); + int choice = dlg.get_action(); LoadType load_type = static_cast(choice); wxGetApp().app_config->set("import_project_action", std::to_string(choice)); @@ -17530,42 +17497,38 @@ LoadType determine_load_type(std::string filename, std::string override_setting) } } -bool Plater::open_3mf_file(const fs::path& file_path) +bool Plater::open_3mf_file(const fs::path &file_path) { std::string filename = encode_path(file_path.filename().string().c_str()); if (!boost::algorithm::iends_with(filename, ".3mf")) { return false; } - bool not_empty_plate = !model().objects.empty(); - bool load_setting_ask_when_relevant = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR) == - OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT; - LoadType load_type = determine_load_type(filename, (not_empty_plate && load_setting_ask_when_relevant) ? - OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK : - ""); + bool not_empty_plate = !model().objects.empty(); + bool load_setting_ask_when_relevant = wxGetApp().app_config->get(SETTING_PROJECT_LOAD_BEHAVIOUR) == OPTION_PROJECT_LOAD_BEHAVIOUR_ASK_WHEN_RELEVANT; + LoadType load_type = determine_load_type(filename, (not_empty_plate && load_setting_ask_when_relevant) ? OPTION_PROJECT_LOAD_BEHAVIOUR_ALWAYS_ASK : ""); - if (load_type == LoadType::Unknown) - return false; + if (load_type == LoadType::Unknown) return false; switch (load_type) { - case LoadType::OpenProject: { - if (wxGetApp().can_load_project()) - load_project(from_path(file_path), ""); - break; - } - case LoadType::LoadGeometry: { - Plater::TakeSnapshot snapshot(this, "Import Object"); - load_files({file_path}, LoadStrategy::LoadModel); - break; - } - case LoadType::LoadConfig: { - load_files({file_path}, LoadStrategy::LoadConfig); - break; - } - case LoadType::Unknown: { - assert(false); - break; - } + case LoadType::OpenProject: { + if (wxGetApp().can_load_project()) + load_project(from_path(file_path), ""); + break; + } + case LoadType::LoadGeometry: { + Plater::TakeSnapshot snapshot(this, "Import Object"); + load_files({file_path}, LoadStrategy::LoadModel); + break; + } + case LoadType::LoadConfig: { + load_files({file_path}, LoadStrategy::LoadConfig); + break; + } + case LoadType::Unknown: { + assert(false); + break; + } } return true; @@ -17574,7 +17537,7 @@ bool Plater::open_3mf_file(const fs::path& file_path) int Plater::get_3mf_file_count(std::vector paths) { auto count = 0; - for (const auto& path : paths) { + for (const auto &path : paths) { if (boost::iends_with(path.filename().string(), ".3mf")) { count++; } @@ -17587,12 +17550,10 @@ void Plater::add_file() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry"; wxArrayString input_files; wxGetApp().import_model(this, input_files); - if (input_files.empty()) - return; + if (input_files.empty()) return; std::vector paths; - for (const auto& file : input_files) - paths.emplace_back(into_path(file)); + for (const auto &file : input_files) paths.emplace_back(into_path(file)); std::string snapshot_label; assert(!paths.empty()); @@ -17609,28 +17570,21 @@ void Plater::add_file() auto loadfiles_type = LoadFilesType::NoFile; auto amf_files_count = get_3mf_file_count(paths); - if (paths.size() > 1 && amf_files_count < paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MFOther; - } - if (paths.size() > 1 && amf_files_count == paths.size()) { - loadfiles_type = LoadFilesType::Multiple3MF; - } - if (paths.size() > 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::MultipleOther; - } - if (paths.size() == 1 && amf_files_count == 1) { - loadfiles_type = LoadFilesType::Single3MF; - }; - if (paths.size() == 1 && amf_files_count == 0) { - loadfiles_type = LoadFilesType::SingleOther; - }; + if (paths.size() > 1 && amf_files_count < paths.size()) { loadfiles_type = LoadFilesType::Multiple3MFOther; } + if (paths.size() > 1 && amf_files_count == paths.size()) { loadfiles_type = LoadFilesType::Multiple3MF; } + if (paths.size() > 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::MultipleOther; } + if (paths.size() == 1 && amf_files_count == 1) { loadfiles_type = LoadFilesType::Single3MF; }; + if (paths.size() == 1 && amf_files_count == 0) { loadfiles_type = LoadFilesType::SingleOther; }; auto first_file = std::vector{}; auto tmf_file = std::vector{}; auto other_file = std::vector{}; - switch (loadfiles_type) { - case LoadFilesType::Single3MF: open_3mf_file(paths[0]); break; + switch (loadfiles_type) + { + case LoadFilesType::Single3MF: + open_3mf_file(paths[0]); + break; case LoadFilesType::SingleOther: { Plater::TakeSnapshot snapshot(this, snapshot_label); @@ -17648,15 +17602,11 @@ void Plater::add_file() case LoadFilesType::Multiple3MF: first_file = std::vector{paths[0]}; for (auto i = 0; i < paths.size(); i++) { - if (i > 0) { - other_file.push_back(paths[i]); - } + if (i > 0) { other_file.push_back(paths[i]); } }; open_3mf_file(first_file[0]); - if (!load_files(other_file, LoadStrategy::LoadModel).empty()) { - wxGetApp().mainframe->update_title(); - } + if (!load_files(other_file, LoadStrategy::LoadModel).empty()) { wxGetApp().mainframe->update_title(); } break; case LoadFilesType::MultipleOther: { @@ -17668,13 +17618,13 @@ void Plater::add_file() } wxGetApp().mainframe->update_title(); if (wxGetApp().app_config->get("recent_models") == "true") - for (auto& path : paths) + for (auto &path : paths) wxGetApp().mainframe->add_to_recent_projects(path.wstring()); } break; } case LoadFilesType::Multiple3MFOther: - for (const auto& path : paths) { + for (const auto &path : paths) { if (boost::iends_with(path.filename().string(), ".3mf")) { if (first_file.size() <= 0) first_file.push_back(path); @@ -17690,11 +17640,11 @@ void Plater::add_file() if (!load_files(other_file, LoadStrategy::LoadModel, false).empty()) { wxGetApp().mainframe->update_title(); if (wxGetApp().app_config->get("recent_models") == "true") - for (auto& file : other_file) + for (auto &file : other_file) wxGetApp().mainframe->add_to_recent_projects(file.wstring()); } break; - default: break; + default:break; } } @@ -17703,31 +17653,33 @@ void Plater::update(bool conside_update_flag, bool force_background_processing_u if (is_new_project_and_check_state()) { return; } - unsigned int flag = force_background_processing_update ? (unsigned int) Plater::priv::UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE : - 0; + unsigned int flag = force_background_processing_update ? (unsigned int)Plater::priv::UpdateParams::FORCE_BACKGROUND_PROCESSING_UPDATE : 0; if (conside_update_flag) { if (need_update()) { p->update(flag); p->set_need_update(false); } - } else + } + else p->update(flag); } void Plater::object_list_changed() { p->object_list_changed(); } -Worker& Plater::get_ui_job_worker() { return p->m_worker; } +Worker &Plater::get_ui_job_worker() { return p->m_worker; } -const Worker& Plater::get_ui_job_worker() const { return p->m_worker; } +const Worker &Plater::get_ui_job_worker() const { return p->m_worker; } void Plater::update_ui_from_settings() { p->update_ui_from_settings(); } void Plater::select_view(const std::string& direction) { p->select_view(direction); } -// BBS: add no_slice logic +//BBS: add no_slice logic void Plater::select_view_3D(const std::string& name, bool no_slice) { p->select_view_3D(name, no_slice); } -void Plater::reload_paint_after_background_process_apply() { p->preview->set_reload_paint_after_background_process_apply(true); } +void Plater::reload_paint_after_background_process_apply() { + p->preview->set_reload_paint_after_background_process_apply(true); +} bool Plater::is_preview_shown() const { return p->is_preview_shown(); } bool Plater::is_preview_loaded() const { return p->is_preview_loaded(); } @@ -17737,7 +17689,7 @@ bool Plater::are_view3D_labels_shown() const { return p->are_view3D_labels_shown void Plater::show_view3D_labels(bool show) { p->show_view3D_labels(show); } bool Plater::is_view3D_overhang_shown() const { return p->is_view3D_overhang_shown(); } -void Plater::show_view3D_overhang(bool show) { p->show_view3D_overhang(show); } +void Plater::show_view3D_overhang(bool show) { p->show_view3D_overhang(show); } bool Plater::is_sidebar_enabled() const { return p->sidebar_layout.is_enabled; } void Plater::enable_sidebar(bool enabled) { p->enable_sidebar(enabled); } @@ -17747,7 +17699,7 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_ void Plater::reset_window_layout() { p->reset_window_layout(); } -// BBS +//BBS void Plater::select_curr_plate_all() { p->select_curr_plate_all(); } void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); } @@ -17759,10 +17711,9 @@ void Plater::remove(size_t obj_idx) { p->remove(obj_idx); } void Plater::reset(bool apply_presets_change) { p->reset(apply_presets_change); } void Plater::reset_with_confirm() { - if (p->model.objects.empty() || - MessageDialog(static_cast(this), _L("All objects will be removed, continue?"), - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Delete All"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE) - .ShowModal() == wxID_YES) { + if (p->model.objects.empty() || MessageDialog(static_cast(this), _L("All objects will be removed, continue?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Delete All"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE) + .ShowModal() == wxID_YES) { reset(); // BBS: jump to plater panel wxGetApp().mainframe->select_tab(TAB_ID_HOME); @@ -17773,15 +17724,13 @@ void Plater::reset_with_confirm() int GUI::Plater::close_with_confirm(std::function second_check) { if (up_to_date(false, false)) { - if (second_check && !second_check(false)) - return wxID_CANCEL; + if (second_check && !second_check(false)) return wxID_CANCEL; model().set_backup_path(""); return wxID_NO; } - MessageDialog dlg(static_cast(this), - _L("The current project has unsaved changes. Would you like to save before continuing\?"), - wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE); + MessageDialog dlg(static_cast(this), _L("The current project has unsaved changes. Would you like to save before continuing\?"), + wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Save"), wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxCENTRE); dlg.show_dsa_button(_L("Remember my choice.")); auto choise = wxGetApp().app_config->get("save_project_choise"); auto result = choise.empty() ? dlg.ShowModal() : choise == "yes" ? wxID_YES : wxID_NO; @@ -17801,8 +17750,7 @@ int GUI::Plater::close_with_confirm(std::function second_check) } } - if (second_check && !second_check(result == wxID_YES)) - return wxID_CANCEL; + if (second_check && !second_check(result == wxID_YES)) return wxID_CANCEL; model().set_backup_path(""); up_to_date(true, false); @@ -17811,21 +17759,23 @@ int GUI::Plater::close_with_confirm(std::function second_check) return result; } -// BBS: trigger a restore project event +//BBS: trigger a restore project event void Plater::trigger_restore_project(int skip_confirm) { auto evt = new wxCommandEvent(EVT_RESTORE_PROJECT, this->GetId()); evt->SetInt(skip_confirm); wxQueueEvent(this, evt); - // wxPostEvent(this, *evt); + //wxPostEvent(this, *evt); } -// BBS -bool Plater::delete_object_from_model(size_t obj_idx, bool refresh_immediately) -{ return p->delete_object_from_model(obj_idx, refresh_immediately); } +//BBS +bool Plater::delete_object_from_model(size_t obj_idx, bool refresh_immediately) { return p->delete_object_from_model(obj_idx, refresh_immediately); } -// BBS: delete all from model -void Plater::delete_all_objects_from_model() { p->delete_all_objects_from_model(); } +//BBS: delete all from model +void Plater::delete_all_objects_from_model() +{ + p->delete_all_objects_from_model(); +} void Plater::set_selected_visible(bool visible) { @@ -17838,6 +17788,7 @@ void Plater::set_selected_visible(bool visible) p->get_current_canvas3D()->set_selected_visible(visible); } + void Plater::remove_selected() { /*if (p->get_selection().is_empty()) @@ -17852,33 +17803,30 @@ void Plater::remove_selected() Plater::TakeSnapshot snapshot(this, "Delete Selected Objects"); get_ui_job_worker().cancel_all(); - // BBS delete current selected - // p->view3D->delete_selected(); + //BBS delete current selected + // p->view3D->delete_selected(); p->get_current_canvas3D()->delete_selected(); } void Plater::increase_instances(size_t num) { - if (!can_increase_instances()) { - return; - } + if (! can_increase_instances()) { return; } Plater::TakeSnapshot snapshot(this, "Increase Instances"); int obj_idx = p->get_selected_object_idx(); - ModelObject* model_object = p->model.objects[obj_idx]; + ModelObject* model_object = p->model.objects[obj_idx]; ModelInstance* model_instance = model_object->instances.back(); - bool was_one_instance = model_object->instances.size() == 1; + bool was_one_instance = model_object->instances.size()==1; double offset_base = canvas3D()->get_size_proportional_to_max_bed_size(0.05); - double offset = offset_base; + double offset = offset_base; for (size_t i = 0; i < num; i++, offset += offset_base) { Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0); - model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), - model_instance->get_mirror()); - // p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); + model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); +// p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); } #ifdef SUPPORT_AUTO_CENTER @@ -17888,23 +17836,21 @@ void Plater::increase_instances(size_t num) p->update(); - p->get_selection().add_instance(obj_idx, (int) model_object->instances.size() - 1); + p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); sidebar().obj_list()->increase_object_instances(obj_idx, was_one_instance ? num + 1 : num); p->selection_changed(); this->p->schedule_background_process(); - // if (wxGetApp().app_config->get("auto_arrange") == "true") { - // this->set_prepare_state(Job::PREPARE_STATE_MENU); - // this->arrange(); - // } + //if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + //} } void Plater::decrease_instances(size_t num) { - if (!can_decrease_instances()) { - return; - } + if (! can_decrease_instances()) { return; } Plater::TakeSnapshot snapshot(this, "Decrease Instances"); @@ -17912,28 +17858,34 @@ void Plater::decrease_instances(size_t num) ModelObject* model_object = p->model.objects[obj_idx]; if (model_object->instances.size() > num) { - for (size_t i = 0; i < num; ++i) + for (size_t i = 0; i < num; ++ i) model_object->delete_last_instance(); p->update(); // Delete object from Sidebar list. Do it after update, so that the GLScene selection is updated with the modified model. sidebar().obj_list()->decrease_object_instances(obj_idx, num); - } else { + } + else { remove(obj_idx); } if (!model_object->instances.empty()) - p->get_selection().add_instance(obj_idx, (int) model_object->instances.size() - 1); + p->get_selection().add_instance(obj_idx, (int)model_object->instances.size() - 1); p->selection_changed(); this->p->schedule_background_process(); - // if (wxGetApp().app_config->get("auto_arrange") == "true") { - // this->set_prepare_state(Job::PREPARE_STATE_MENU); - // this->arrange(); - // } + //if (wxGetApp().app_config->get("auto_arrange") == "true") { + // this->set_prepare_state(Job::PREPARE_STATE_MENU); + // this->arrange(); + //} } -static long GetNumberFromUser( - const wxString& msg, const wxString& prompt, const wxString& title, long value, long min, long max, wxWindow* parent) +static long GetNumberFromUser( const wxString& msg, + const wxString& prompt, + const wxString& title, + long value, + long min, + long max, + wxWindow* parent) { #ifdef _WIN32 wxNumberEntryDialog dialog(parent, msg, prompt, title, value, min, max, wxDefaultPosition); @@ -17955,14 +17907,14 @@ void Plater::set_number_of_copies(/*size_t num*/) ModelObject* model_object = p->model.objects[obj_idx]; - const int num = GetNumberFromUser(" ", _L("Number of copies:"), _L("Copies of the selected object"), model_object->instances.size(), 0, - 1000, this); + const int num = GetNumberFromUser( " ", _L("Number of copies:"), + _L("Copies of the selected object"), model_object->instances.size(), 0, 1000, this ); if (num < 0) return; - Plater::TakeSnapshot snapshot(this, (boost::format("Set numbers of copies to %1%") % num).str()); + Plater::TakeSnapshot snapshot(this, (boost::format("Set numbers of copies to %1%")%num).str()); - int diff = num - (int) model_object->instances.size(); + int diff = num - (int)model_object->instances.size(); if (diff > 0) increase_instances(diff); else if (diff < 0) @@ -17971,7 +17923,7 @@ void Plater::set_number_of_copies(/*size_t num*/) void Plater::fill_bed_with_copies() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique()); @@ -17980,16 +17932,22 @@ void Plater::fill_bed_with_copies() void Plater::fill_bed_with_instances() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique(true)); } } -bool Plater::is_selection_empty() const { return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); } +bool Plater::is_selection_empty() const +{ + return p->get_selection().is_empty() || p->get_selection().is_wipe_tower(); +} -void Plater::scale_selection_to_fit_print_volume() { p->scale_selection_to_fit_print_volume(); } +void Plater::scale_selection_to_fit_print_volume() +{ + p->scale_selection_to_fit_print_volume(); +} void Plater::convert_unit(ConversionType conv_type) { @@ -18000,14 +17958,13 @@ void Plater::convert_unit(ConversionType conv_type) TakeSnapshot snapshot(this, conv_type == ConversionType::CONV_FROM_INCH ? "Convert from imperial units" : conv_type == ConversionType::CONV_TO_INCH ? "Revert conversion from imperial units" : - conv_type == ConversionType::CONV_FROM_METER ? "Convert from meters" : - "Revert conversion from meters"); + conv_type == ConversionType::CONV_FROM_METER ? "Convert from meters" : "Revert conversion from meters"); wxBusyCursor wait; ModelObjectPtrs objects; std::reverse(obj_idxs.begin(), obj_idxs.end()); for (int obj_idx : obj_idxs) { - ModelObject* object = p->model.objects[obj_idx]; + ModelObject *object = p->model.objects[obj_idx]; object->convert_units(objects, conv_type, volume_idxs); remove(obj_idx); } @@ -18015,12 +17972,13 @@ void Plater::convert_unit(ConversionType conv_type) p->load_model_objects(objects); Selection& selection = p->view3D->get_canvas3d()->get_selection(); - size_t last_obj_idx = p->model.objects.size() - 1; + size_t last_obj_idx = p->model.objects.size() - 1; if (volume_idxs.empty()) { for (size_t i = 0; i < objects.size(); ++i) - selection.add_object((unsigned int) (last_obj_idx - i), i == 0); - } else { + selection.add_object((unsigned int)(last_obj_idx - i), i == 0); + } + else { for (int vol_idx : volume_idxs) selection.add_volume(last_obj_idx, vol_idx, 0, false); } @@ -18042,9 +18000,9 @@ void Plater::apply_cut_object_to_model(size_t obj_idx, const ModelObjectPtrs& ne wxGetApp().obj_list()->update_info_items(idx); Selection& selection = p->get_selection(); - size_t last_id = p->model.objects.size() - 1; + size_t last_id = p->model.objects.size() - 1; for (size_t i = 0; i < new_objects.size(); ++i) - selection.add_object((unsigned int) (last_id - i), i == 0); + selection.add_object((unsigned int)(last_id - i), i == 0); // UIThreadWorker w; // arrange(w, true); @@ -18056,8 +18014,8 @@ void Plater::export_gcode(bool prefer_removable) if (p->model.objects.empty()) return; - // if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) - // return; + //if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true)) + // return; if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) return; @@ -18071,20 +18029,21 @@ void Plater::export_gcode(bool prefer_removable) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); - } catch (const Slic3r::PlaceholderParserError& ex) { + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); + } catch (const Slic3r::PlaceholderParserError &ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); return; - } catch (const std::exception& ex) { + } catch (const std::exception &ex) { show_error(this, ex.what(), false); return; } - default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - AppConfig& appconfig = *wxGetApp().app_config; - RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); + default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); + AppConfig &appconfig = *wxGetApp().app_config; + RemovableDriveManager &removable_drive_manager = *wxGetApp().removable_drive_manager(); // Get a last save path, either to removable media or to an internal media. - std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); + std::string start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), prefer_removable); if (prefer_removable) { // Returns a path to a removable media if it exists, prefering start_dir. Update the internal removable drives database. start_dir = removable_drive_manager.get_removable_drive_path(start_dir); @@ -18096,14 +18055,17 @@ void Plater::export_gcode(bool prefer_removable) fs::path output_path; { std::string ext = default_output_file.extension().string(); - wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SLA file as:"), start_dir, - from_path(default_output_file.filename()), - GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_SL1, ext), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + wxFileDialog dlg(this, (printer_technology() == ptFFF) ? _L("Save G-code file as:") : _L("Save SLA file as:"), + start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards((printer_technology() == ptFFF) ? FT_GCODE : FT_SL1, ext), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); while (has_illegal_filename_characters(output_path.filename().string())) { show_error(this, _L("The provided file name is not valid.") + "\n" + - _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); + _L("The following characters are not allowed by a FAT file system:") + " <>:/\\|?*\""); dlg.SetFilename(from_path(output_path.filename())); if (dlg.ShowModal() == wxID_OK) output_path = into_path(dlg.GetPath()); @@ -18115,17 +18077,17 @@ void Plater::export_gcode(bool prefer_removable) } } - if (!output_path.empty()) { + if (! output_path.empty()) { bool path_on_removable_media = removable_drive_manager.set_and_verify_last_save_path(output_path.string()); - // bool path_on_removable_media = false; + //bool path_on_removable_media = false; p->notification_manager->new_export_began(path_on_removable_media); - p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; - p->last_output_path = output_path.string(); + p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; + p->last_output_path = output_path.string(); p->last_output_dir_path = output_path.parent_path().string(); p->export_gcode(output_path, path_on_removable_media); // Storing a path to AppConfig either as path to removable media or a path to internal media. - // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled - // the external drives while the dialog was open. + // is_path_on_removable_drive() is called with the "true" parameter to update its internal database as the user may have shuffled the external drives + // while the dialog was open. appconfig.update_last_output_dir(output_path.parent_path().string(), path_on_removable_media); try { @@ -18137,18 +18099,22 @@ void Plater::export_gcode(bool prefer_removable) j["printer_preset"] = printer_config.config.opt_string("inherits"); } - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; if (preset_bundle) { j["gcode_printer_model"] = preset_bundle->printers.get_edited_preset().get_printer_type(preset_bundle); } - NetworkAgent* agent = wxGetApp().getAgent(); + NetworkAgent *agent = wxGetApp().getAgent(); } catch (...) {} + } } -void Plater::send_to_printer(bool isall) { p->on_action_send_to_printer(isall); } +void Plater::send_to_printer(bool isall) +{ + p->on_action_send_to_printer(isall); +} -// BBS export gcode 3mf to file +//BBS export gcode 3mf to file void Plater::export_gcode_3mf(bool export_all) { if (p->model.objects.empty()) @@ -18157,7 +18123,7 @@ void Plater::export_gcode_3mf(bool export_all) if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) return; - // calc default_output_file, get default output file from background process + //calc default_output_file, get default output file from background process fs::path default_output_file; AppConfig& appconfig = *wxGetApp().app_config; std::string start_dir; @@ -18167,52 +18133,60 @@ void Plater::export_gcode_3mf(bool export_all) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); - } catch (const Slic3r::PlaceholderParserError& ex) { + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); + } + catch (const Slic3r::PlaceholderParserError& ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); return; - } catch (const std::exception& ex) { + } + catch (const std::exception& ex) { show_error(this, ex.what(), false); return; } default_output_file.replace_extension(".gcode.3mf"); default_output_file = fs::path(Slic3r::fold_utf8_to_ascii(default_output_file.string())); - // Get a last save path + //Get a last save path start_dir = appconfig.get_last_output_dir(default_output_file.parent_path().string(), false); fs::path output_path; { std::string ext = default_output_file.extension().string(); - wxFileDialog dlg(this, _L("Save Sliced file as:"), start_dir, from_path(default_output_file.filename()), - GUI::file_wildcards(FT_GCODE_3MF, ""), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); + wxFileDialog dlg(this, _L("Save Sliced file as:"), + start_dir, + from_path(default_output_file.filename()), + GUI::file_wildcards(FT_GCODE_3MF, ""), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); if (dlg.ShowModal() == wxID_OK) { output_path = into_path(dlg.GetPath()); - ext = output_path.extension().string(); + ext = output_path.extension().string(); if (ext != ".3mf") output_path = output_path.string() + ".3mf"; } } if (!output_path.empty()) { - // BBS do not set to removable media path + //BBS do not set to removable media path bool path_on_removable_media = false; p->notification_manager->new_export_began(path_on_removable_media); p->exporting_status = path_on_removable_media ? ExportingStatus::EXPORTING_TO_REMOVABLE : ExportingStatus::EXPORTING_TO_LOCAL; - // BBS do not save last output path - p->last_output_path = output_path.string(); + //BBS do not save last output path + p->last_output_path = output_path.string(); p->last_output_dir_path = output_path.parent_path().string(); - int plate_idx = get_partplate_list().get_curr_plate_index(); + int plate_idx = get_partplate_list().get_curr_plate_index(); if (export_all) plate_idx = PLATE_ALL_IDX; - export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, - plate_idx); // BBS: silence + export_3mf(output_path, SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel, plate_idx); // BBS: silence RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); + bool on_removable = removable_drive_manager.is_path_on_removable_drive(p->last_output_dir_path); + // update last output dir appconfig.update_last_output_dir(output_path.parent_path().string(), false); p->notification_manager->push_exporting_finished_notification(output_path.string(), p->last_output_dir_path, on_removable); @@ -18224,13 +18198,10 @@ void Plater::send_gcode_finish(wxString name) auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name); p->notification_manager->push_exporting_finished_notification(out_str, "", false); } - void Plater::export_core_3mf() { wxString path = p->get_export_file(FT_3MF); - if (path.empty()) { - return; - } + if (path.empty()) { return; } const std::string path_u8 = into_u8(path); export_3mf(path_u8, SaveStrategy::Silence); } @@ -18401,28 +18372,26 @@ void Plater::set_pending_published(const std::vector& published_key p->m_pending_material_keys = material_keys; } -Preset* get_printer_preset(const MachineObject* obj) +Preset *get_printer_preset(const MachineObject *obj) { if (!obj) return nullptr; - Preset* printer_preset = nullptr; + Preset *printer_preset = nullptr; - PresetBundle* preset_bundle = wxGetApp().preset_bundle; + PresetBundle *preset_bundle = wxGetApp().preset_bundle; for (auto printer_it = preset_bundle->printers.begin(); printer_it != preset_bundle->printers.end(); printer_it++) { // only use system printer preset if (!printer_it->is_system) continue; - ConfigOption* printer_nozzle_opt = printer_it->config.option("nozzle_diameter"); - ConfigOptionFloats* printer_nozzle_vals = nullptr; - if (printer_nozzle_opt) - printer_nozzle_vals = dynamic_cast(printer_nozzle_opt); + ConfigOption *printer_nozzle_opt = printer_it->config.option("nozzle_diameter"); + ConfigOptionFloats *printer_nozzle_vals = nullptr; + if (printer_nozzle_opt) printer_nozzle_vals = dynamic_cast(printer_nozzle_opt); std::string model_id = printer_it->get_current_printer_type(preset_bundle); - std::string printer_type = obj->get_show_printer_type(); - bool nozzle_diameter_matches_or_unknown = printer_nozzle_vals && - obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, printer_nozzle_vals->get_at(0)); + std::string printer_type = obj->get_show_printer_type(); + bool nozzle_diameter_matches_or_unknown = printer_nozzle_vals && obj->GetExtderSystem()->NozzleDiameterMatchesOrUnknown(0, printer_nozzle_vals->get_at(0)); if (model_id.compare(printer_type) == 0 && nozzle_diameter_matches_or_unknown) { printer_preset = &(*printer_it); } @@ -18430,7 +18399,7 @@ Preset* get_printer_preset(const MachineObject* obj) return printer_preset; } -bool Plater::check_printer_initialized(MachineObject* obj, bool only_warning, bool popup_warning) +bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bool popup_warning) { if (!obj) return false; @@ -18439,6 +18408,7 @@ bool Plater::check_printer_initialized(MachineObject* obj, bool only_warning, bo const auto& extruders = obj->GetExtderSystem()->GetExtruders(); for (const DevExtder& extruder : extruders) { + // Skip check if nozzle type is unknown if (extruder.GetNozzleType() == NozzleType::ntUndefine) { continue; @@ -18454,16 +18424,14 @@ bool Plater::check_printer_initialized(MachineObject* obj, bool only_warning, bo if (popup_warning) { if (!only_warning) { if (DevPrinterConfigUtil::get_printer_can_set_nozzle(obj->get_show_printer_type())) { - MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please set the nozzle and try again."), - _L("Warning"), wxOK | wxICON_WARNING); + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please set the nozzle and try again."), _L("Warning"), wxOK | wxICON_WARNING); dlg.ShowModal(); } else { - MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please check."), _L("Warning"), - wxOK | wxICON_WARNING); + MessageDialog dlg(wxGetApp().plater(), _L("The nozzle type is not set. Please check."), _L("Warning"), wxOK | wxICON_WARNING); dlg.ShowModal(); } - PrinterPartsDialog* print_parts_dlg = new PrinterPartsDialog(nullptr); + PrinterPartsDialog *print_parts_dlg = new PrinterPartsDialog(nullptr); print_parts_dlg->update_machine_obj(obj); print_parts_dlg->ShowModal(); } else { @@ -18484,25 +18452,26 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st std::vector csgmesh; csgmesh.reserve(2 * mo.volumes.size()); bool has_splitable_volume = csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), - csg::mpartsPositive | csg::mpartsNegative); - + csg::mpartsPositive | csg::mpartsNegative); + std::string fail_msg = _u8L("Unable to perform boolean operation on model meshes. " - "Only positive parts will be kept. You may fix the meshes and try again."); - if (auto fail_reason_name = csg::check_csgmesh_booleans(Range{std::begin(csgmesh), std::end(csgmesh)}); - std::get<0>(fail_reason_name) != csg::BooleanFailReason::OK) { + "Only positive parts will be kept. You may fix the meshes and try again."); + if (auto fail_reason_name = csg::check_csgmesh_booleans(Range{ std::begin(csgmesh), std::end(csgmesh) }); std::get<0>(fail_reason_name) != csg::BooleanFailReason::OK) { std::string name = std::get<1>(fail_reason_name); - std::map fail_reasons = - {{csg::BooleanFailReason::OK, "OK"}, - {csg::BooleanFailReason::MeshEmpty, Slic3r::format(_u8L("Reason: part \"%1%\" is empty."), name)}, - {csg::BooleanFailReason::NotBoundAVolume, Slic3r::format(_u8L("Reason: part \"%1%\" does not bound a volume."), name)}, - {csg::BooleanFailReason::SelfIntersect, Slic3r::format(_u8L("Reason: part \"%1%\" has self intersection."), name)}, - {csg::BooleanFailReason::NoIntersection, Slic3r::format(_u8L("Reason: \"%1%\" and another part have no intersection."), name)}}; + std::map fail_reasons = { + {csg::BooleanFailReason::OK, "OK"}, + {csg::BooleanFailReason::MeshEmpty, Slic3r::format( _u8L("Reason: part \"%1%\" is empty."), name)}, + {csg::BooleanFailReason::NotBoundAVolume, Slic3r::format(_u8L("Reason: part \"%1%\" does not bound a volume."), name)}, + {csg::BooleanFailReason::SelfIntersect, Slic3r::format(_u8L("Reason: part \"%1%\" has self intersection."), name)}, + {csg::BooleanFailReason::NoIntersection, Slic3r::format(_u8L("Reason: \"%1%\" and another part have no intersection."), name)} }; fail_msg += " " + fail_reasons[std::get<0>(fail_reason_name)]; - } else { + } + else { try { - MeshBoolean::mcut::McutMeshPtr meshPtr = csg::perform_csgmesh_booleans_mcut(Range{std::begin(csgmesh), std::end(csgmesh)}); - mesh = MeshBoolean::mcut::mcut_to_triangle_mesh(*meshPtr); - } catch (...) {} + MeshBoolean::mcut::McutMeshPtr meshPtr = csg::perform_csgmesh_booleans_mcut(Range{ std::begin(csgmesh), std::end(csgmesh) }); + mesh = MeshBoolean::mcut::mcut_to_triangle_mesh(*meshPtr); + } + catch (...) {} #if 0 // if mcut fails, try again with CGAL if (mesh.empty()) { @@ -18535,7 +18504,8 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st m.transform(i->get_matrix(), true); mesh.merge(m); } - } else if (0 <= instance_id && instance_id < int(mo.instances.size())) + } + else if (0 <= instance_id && instance_id < int(mo.instances.size())) mesh.transform(mo.instances[instance_id]->get_matrix(), true); return mesh; } @@ -18544,9 +18514,7 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st #define EXPORT_WITH_BOOLEAN 0 void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, FileType file_type) { - if (p->model.objects.empty()) { - return; - } + if (p->model.objects.empty()) { return; } int quality = 0; @@ -18570,35 +18538,31 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } else { path = p->get_export_file(file_type); } - if (path.empty()) { - return; - } + if (path.empty()) { return; } const std::string path_u8 = into_u8(path); wxBusyCursor wait; const auto& selection = p->get_selection(); - const auto obj_idx = selection.get_object_idx(); + const auto obj_idx = selection.get_object_idx(); #if EXPORT_WITH_BOOLEAN if (selection_only && (obj_idx == -1 || selection.is_wipe_tower())) return; #else // BBS support selecting multiple objects - if (selection_only && selection.is_wipe_tower()) - return; + if (selection_only && selection.is_wipe_tower()) return; // BBS if (selection_only) { // only support selection single full object and mulitiple full object - if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) - return; + if (!selection.is_single_full_object() && !selection.is_multiple_full_object()) return; } // Following lambda generates a combined mesh for export with normals pointing outwards. - auto mesh_to_export_fff_no_boolean = [this](const ModelObject& mo, int instance_id) { + auto mesh_to_export_fff_no_boolean = [this](const ModelObject &mo, int instance_id) { TriangleMesh mesh; - // Prusa export negative parts + //Prusa export negative parts std::vector csgmesh; csgmesh.reserve(2 * mo.volumes.size()); csg::model_to_csgmesh(mo, Transform3d::Identity(), std::back_inserter(csgmesh), @@ -18610,13 +18574,14 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } else if (std::get<2>(csg::check_csgmesh_booleans(csgrange)) == csgrange.end()) { try { auto cgalm = csg::perform_csgmesh_booleans(csgrange); - mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm); + mesh = MeshBoolean::cgal::cgal_to_triangle_mesh(*cgalm); } catch (...) {} } if (mesh.empty()) { - get_notification_manager()->push_plater_error_notification(_u8L("Unable to perform boolean operation on model meshes. " - "Only positive parts will be exported.")); + get_notification_manager()->push_plater_error_notification( + _u8L("Unable to perform boolean operation on model meshes. " + "Only positive parts will be exported.")); for (const ModelVolume* v : mo.volumes) if (v->is_model_part()) { @@ -18628,7 +18593,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil if (instance_id == -1) { TriangleMesh vols_mesh(mesh); mesh = TriangleMesh(); - for (const ModelInstance* i : mo.instances) { + for (const ModelInstance *i : mo.instances) { TriangleMesh m = vols_mesh; m.transform(i->get_matrix(), true); mesh.merge(m); @@ -18641,17 +18606,15 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil auto mesh_to_export_sla = [&, this](const ModelObject& mo, int instance_id) { TriangleMesh mesh; - const SLAPrintObject* object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); + const SLAPrintObject *object = this->p->sla_print.get_print_object_by_model_object_id(mo.id()); if (auto m = object->get_mesh_to_print(); m.empty()) - mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) { - return get_notification_manager()->push_general_error_notification(msg); - }); + mesh = combine_mesh_fff(mo, instance_id, [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); else { const Transform3d mesh_trafo_inv = object->trafo().inverse(); - const bool is_left_handed = object->is_left_handed(); + const bool is_left_handed = object->is_left_handed(); - auto pad_mesh = extended ? object->pad_mesh() : TriangleMesh{}; + auto pad_mesh = extended? object->pad_mesh() : TriangleMesh{}; pad_mesh.transform(mesh_trafo_inv); auto supports_mesh = extended ? object->support_mesh() : TriangleMesh{}; @@ -18660,16 +18623,16 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil const std::vector& obj_instances = object->instances(); for (const SLAPrintObject::Instance& obj_instance : obj_instances) { auto it = std::find_if(object->model_object()->instances.begin(), object->model_object()->instances.end(), - [&obj_instance](const ModelInstance* mi) { return mi->id() == obj_instance.instance_id; }); + [&obj_instance](const ModelInstance *mi) { return mi->id() == obj_instance.instance_id; }); assert(it != object->model_object()->instances.end()); if (it != object->model_object()->instances.end()) { - const bool one_inst_only = selection_only && !selection.is_single_full_object(); + const bool one_inst_only = selection_only && ! selection.is_single_full_object(); const int instance_idx = it - object->model_object()->instances.begin(); - const Transform3d& inst_transform = - one_inst_only ? Transform3d::Identity() : - object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); + const Transform3d& inst_transform = one_inst_only + ? Transform3d::Identity() + : object->model_object()->instances[instance_idx]->get_transformation().get_matrix(); TriangleMesh inst_mesh; @@ -18692,10 +18655,10 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil inst_mesh.merge(inst_object_mesh); - // ensure that the instance lays on the bed + // ensure that the instance lays on the bed inst_mesh.translate(0.0f, 0.0f, -inst_mesh.bounding_box().min.z()); - // merge instance with global mesh + // merge instance with global mesh mesh.merge(inst_mesh); if (one_inst_only) @@ -18707,22 +18670,20 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil return mesh; }; - std::function mesh_to_export; + std::function + mesh_to_export; if (p->printer_technology == ptFFF) #if EXPORT_WITH_BOOLEAN - mesh_to_export = [this](const ModelObject& mo, int instance_id) { - return Plater::combine_mesh_fff(mo, instance_id, [this](const std::string& msg) { - return get_notification_manager()->push_general_error_notification(msg); - }); - }; + mesh_to_export = [this](const ModelObject& mo, int instance_id) {return Plater::combine_mesh_fff(mo, instance_id, + [this](const std::string& msg) {return get_notification_manager()->push_general_error_notification(msg); }); }; #else mesh_to_export = mesh_to_export_fff_no_boolean; #endif else mesh_to_export = mesh_to_export_sla; - auto get_save_file = [file_type](std::string const& dir, std::string const& name) { + auto get_save_file = [file_type](std::string const & dir, std::string const & name) { std::string ext = ""; switch (file_type) { case FT_STL: ext = ".stl"; break; @@ -18730,38 +18691,39 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } auto path = dir + name + ext; - int n = 1; + int n = 1; while (boost::filesystem::exists(path)) - path = dir + name + "(" + std::to_string(n++) + ")" + ext; + path = dir + name + "(" + std::to_string(n++) + ")"+ext; return path; }; TriangleMesh mesh; if (selection_only) { if (selection.is_single_full_object()) { - const auto obj_idx = selection.get_object_idx(); + const auto obj_idx = selection.get_object_idx(); const ModelObject* model_object = p->model.objects[obj_idx]; if (selection.get_mode() == Selection::Instance) mesh = mesh_to_export(*model_object, (model_object->instances.size() > 1) ? -1 : selection.get_instance_idx()); else { const GLVolume* volume = selection.get_first_volume(); - mesh = model_object->volumes[volume->volume_idx()]->mesh(); + mesh = model_object->volumes[volume->volume_idx()]->mesh(); mesh.transform(volume->get_volume_transformation().get_matrix(), true); } - if (model_object->instances.size() == 1) - mesh.translate(-model_object->origin_translation.cast()); - } else if (selection.is_multiple_full_object() && !multi_stls) { + if (model_object->instances.size() == 1) mesh.translate(-model_object->origin_translation.cast()); + } + else if (selection.is_multiple_full_object() && !multi_stls) { const std::set>& instances_idxs = p->get_selection().get_selected_object_instances(); for (const std::pair& i : instances_idxs) { ModelObject* object = p->model.objects[i.first]; mesh.merge(mesh_to_export(*object, i.second)); } - } else if (selection.is_multiple_full_object() && multi_stls) { - const std::set>& instances_idxs = p->get_selection().get_selected_object_instances(); - for (const std::pair& i : instances_idxs) { - ModelObject* object = p->model.objects[i.first]; - auto mesh = mesh_to_export(*object, i.second); + } + else if (selection.is_multiple_full_object() && multi_stls) { + const std::set> &instances_idxs = p->get_selection().get_selected_object_instances(); + for (const std::pair &i : instances_idxs) { + ModelObject *object = p->model.objects[i.first]; + auto mesh = mesh_to_export(*object, i.second); mesh.translate(-object->origin_translation.cast()); switch (file_type) { @@ -18771,7 +18733,8 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } return; } - } else if (!multi_stls) { + } + else if (!multi_stls) { for (const ModelObject* o : p->model.objects) { mesh.merge(mesh_to_export(*o, -1)); } @@ -18794,7 +18757,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil } } -// BBS: remove amf export +//BBS: remove amf export /*void Plater::export_amf() { if (p->model.objects.empty()) { return; } @@ -18815,7 +18778,7 @@ void Plater::export_stl(bool extended, bool selection_only, bool multi_stls, Fil }*/ namespace { -std::string get_file_name(const std::string& file_path) +std::string get_file_name(const std::string &file_path) { size_t pos_last_delimiter = file_path.find_last_of("/\\"); size_t pos_point = file_path.find_last_of('.'); @@ -18823,33 +18786,33 @@ std::string get_file_name(const std::string& file_path) size_t count = pos_point - pos_last_delimiter - 1; return file_path.substr(offset, count); } -using SvgFile = EmbossShape::SvgFile; +using SvgFile = EmbossShape::SvgFile; using SvgFiles = std::vector; -std::string create_unique_3mf_filepath(const std::string& file, const SvgFiles svgs) +std::string create_unique_3mf_filepath(const std::string &file, const SvgFiles svgs) { // const std::string MODEL_FOLDER = "3D/"; // copy from file 3mf.cpp std::string path_in_3mf = "3D/" + file + ".svg"; - size_t suffix_number = 0; - bool is_unique = false; - do { - is_unique = true; - path_in_3mf = "3D/" + file + ((suffix_number++) ? ("_" + std::to_string(suffix_number)) : "") + ".svg"; - for (SvgFile* svgfile : svgs) { + size_t suffix_number = 0; + bool is_unique = false; + do{ + is_unique = true; + path_in_3mf = "3D/" + file + ((suffix_number++)? ("_" + std::to_string(suffix_number)) : "") + ".svg"; + for (SvgFile *svgfile : svgs) { if (svgfile->path_in_3mf.empty()) continue; if (svgfile->path_in_3mf.compare(path_in_3mf) == 0) { is_unique = false; break; } - } + } } while (!is_unique); return path_in_3mf; } -bool set_by_local_path(SvgFile& svg, const SvgFiles& svgs) +bool set_by_local_path(SvgFile &svg, const SvgFiles& svgs) { // Try to find already used svg file - for (SvgFile* svg_ : svgs) { + for (SvgFile *svg_ : svgs) { if (svg_->path_in_3mf.empty()) continue; if (svg.path.compare(svg_->path) == 0) { @@ -18864,13 +18827,13 @@ bool set_by_local_path(SvgFile& svg, const SvgFiles& svgs) /// Function to secure private data before store to 3mf /// /// Data(also private) to clean before publishing -void publish(Model& model, SaveStrategy strategy) -{ +void publish(Model &model, SaveStrategy strategy) { + // SVG file publishing bool exist_new = false; SvgFiles svgfiles; - for (ModelObject* object : model.objects) { - for (ModelVolume* volume : object->volumes) { + for (ModelObject *object: model.objects){ + for (ModelVolume *volume : object->volumes) { if (!volume->emboss_shape.has_value()) continue; if (volume->text_configuration.has_value()) @@ -18887,7 +18850,7 @@ void publish(Model& model, SaveStrategy strategy) } } - for (SvgFile* svgfile : svgfiles) { + for (SvgFile *svgfile : svgfiles) { if (!svgfile->path_in_3mf.empty()) continue; // already suggested path (previous save) @@ -18899,26 +18862,26 @@ void publish(Model& model, SaveStrategy strategy) // check whether original filename is already in: filename = get_file_name(svgfile->path); } - svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); + svgfile->path_in_3mf = create_unique_3mf_filepath(filename, svgfiles); } } -} // namespace +} // BBS: backup int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy strategy, int export_plate_idx, Export3mfProgressFn proFn) { int ret = 0; - // if (p->model.objects.empty()) { - // MessageDialog dialog(nullptr, _L("No objects to export."), _L("Save project"), wxYES); - // if (dialog.ShowModal() == wxYES) - // return -1; - // } + //if (p->model.objects.empty()) { + // MessageDialog dialog(nullptr, _L("No objects to export."), _L("Save project"), wxYES); + // if (dialog.ShowModal() == wxYES) + // return -1; + //} if (output_path.empty()) return -1; bool export_config = true; - wxString path = from_path(output_path); + wxString path = from_path(output_path); if (!path.Lower().EndsWith(".3mf")) return -1; @@ -18927,19 +18890,17 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy // modify model publish(p->model, strategy); - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config_secure(); const std::string path_u8 = into_u8(path); wxBusyCursor wait; - BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ - << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") % output_path.string() % - (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int) strategy; + BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") + %output_path.string()%(strategy & SaveStrategy::Backup)%export_plate_idx %(unsigned int)strategy; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") % std::string("") % - (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int) strategy; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": path=%1%, backup=%2%, export_plate_idx=%3%, SaveStrategy=%4%") + % std::string("") % (strategy & SaveStrategy::Backup) % export_plate_idx % (unsigned int)strategy; - // BBS: add plate logic for thumbnail generate + //BBS: add plate logic for thumbnail generate std::vector thumbnails; std::vector no_light_thumbnails; std::vector calibration_thumbnails; @@ -18950,113 +18911,106 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy if (!(strategy & SaveStrategy::Backup)) { for (int i = 0; i < p->partplate_list.get_plate_count(); i++) { ThumbnailData* thumbnail_data = &p->partplate_list.get_plate(i)->thumbnail_data; - if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { - // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; - } else { + if (p->partplate_list.get_plate(i)->thumbnail_data.is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%")%i; + } + else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; + const ThumbnailsParams thumbnail_params = { {}, false, true, true, true, i }; p->generate_thumbnail(p->partplate_list.get_plate(i)->thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, - thumbnail_params, Camera::EType::Ortho); + thumbnail_params, Camera::EType::Ortho); } thumbnails.push_back(thumbnail_data); - ThumbnailData* no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; + ThumbnailData *no_light_thumbnail_data = &p->partplate_list.get_plate(i)->no_light_thumbnail_data; if (p->partplate_list.get_plate(i)->no_light_thumbnail_data.is_valid() && using_exported_file()) { // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate thumbnail for gcode/exported mode of plate %1%") % i; } else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate thumbnail for plate %1%") % i; const ThumbnailsParams thumbnail_params = {{}, false, true, true, true, i}; - p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, - THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, - true); + p->generate_thumbnail(p->partplate_list.get_plate(i)->no_light_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Iso, false, true); } no_light_thumbnails.push_back(no_light_thumbnail_data); - // ThumbnailData* calibration_data = &p->partplate_list.get_plate(i)->cali_thumbnail_data; - // calibration_thumbnails.push_back(calibration_data); + //ThumbnailData* calibration_data = &p->partplate_list.get_plate(i)->cali_thumbnail_data; + //calibration_thumbnails.push_back(calibration_data); PlateBBoxData* plate_bbox_data = &p->partplate_list.get_plate(i)->cali_bboxes_data; plate_bboxes.push_back(plate_bbox_data); - // generate top and picking thumbnails + //generate top and picking thumbnails ThumbnailData* top_thumbnail = &p->partplate_list.get_plate(i)->top_thumbnail_data; - if (top_thumbnail->is_valid() && using_exported_file()) { - // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%") % - i; - } else { + if (top_thumbnail->is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate top_thumbnail for gcode/exported mode of plate %1%")%i; + } + else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate top_thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = {{}, false, true, false, true, i}; - p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, - THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, - false); + const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; + p->generate_thumbnail(p->partplate_list.get_plate(i)->top_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, false); } top_thumbnails.push_back(top_thumbnail); ThumbnailData* picking_thumbnail = &p->partplate_list.get_plate(i)->pick_thumbnail_data; - if (picking_thumbnail->is_valid() && using_exported_file()) { - // no need to generate thumbnail - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%") % - i; - } else { + if (picking_thumbnail->is_valid() && using_exported_file()) { + //no need to generate thumbnail + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": non need to re-generate pick_thumbnail for gcode/exported mode of plate %1%")%i; + } + else { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": re-generate pick_thumbnail for plate %1%") % i; - const ThumbnailsParams thumbnail_params = {{}, false, true, false, true, i}; - p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, - THUMBNAIL_SIZE_3MF.second, thumbnail_params, Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, - true, true); + const ThumbnailsParams thumbnail_params = { {}, false, true, false, true, i }; + p->generate_thumbnail(p->partplate_list.get_plate(i)->pick_thumbnail_data, THUMBNAIL_SIZE_3MF.first, THUMBNAIL_SIZE_3MF.second, thumbnail_params, + Camera::EType::Ortho, Camera::ViewAngleType::Top_Plate, true,true); } picking_thumbnails.push_back(picking_thumbnail); } if (p->partplate_list.get_curr_plate()->is_slice_result_valid()) { - // BBS generate BBS calibration thumbnails + //BBS generate BBS calibration thumbnails int index = p->partplate_list.get_curr_plate_index(); - // ThumbnailData* calibration_data = calibration_thumbnails[index]; - // const ThumbnailsParams calibration_params = { {}, false, true, true, true, p->partplate_list.get_curr_plate_index() }; - // p->generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, - // calibration_params); + //ThumbnailData* calibration_data = calibration_thumbnails[index]; + //const ThumbnailsParams calibration_params = { {}, false, true, true, true, p->partplate_list.get_curr_plate_index() }; + //p->generate_calibration_thumbnail(*calibration_data, PartPlate::cali_thumbnail_width, PartPlate::cali_thumbnail_height, calibration_params); if (using_exported_file()) { - // do nothing - } else + //do nothing + } + else *plate_bboxes[index] = p->generate_first_layer_bbox(); } } - // BBS: add bbs 3mf logic + //BBS: add bbs 3mf logic PlateDataPtrs plate_data_list; - p->partplate_list.store_to_3mf_structure(plate_data_list, - (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), - export_plate_idx); + p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx); // BBS: backup - PresetBundle& preset_bundle = *wxGetApp().preset_bundle; + PresetBundle& preset_bundle = *wxGetApp().preset_bundle; std::vector project_presets = preset_bundle.get_current_project_embedded_presets(); StoreParams store_params; - store_params.path = path_u8; - store_params.model = &p->model; - store_params.plate_data_list = plate_data_list; - store_params.export_plate_idx = export_plate_idx; - store_params.project_presets = project_presets; - store_params.config = export_config ? &cfg : nullptr; - store_params.thumbnail_data = thumbnails; - store_params.no_light_thumbnail_data = no_light_thumbnails; - store_params.top_thumbnail_data = top_thumbnails; - store_params.pick_thumbnail_data = picking_thumbnails; + store_params.path = path_u8; + store_params.model = &p->model; + store_params.plate_data_list = plate_data_list; + store_params.export_plate_idx = export_plate_idx; + store_params.project_presets = project_presets; + store_params.config = export_config ? &cfg : nullptr; + store_params.thumbnail_data = thumbnails; + store_params.no_light_thumbnail_data = no_light_thumbnails; + store_params.top_thumbnail_data = top_thumbnails; + store_params.pick_thumbnail_data = picking_thumbnails; store_params.calibration_thumbnail_data = calibration_thumbnails; - store_params.proFn = proFn; - store_params.id_bboxes = plate_bboxes; // BBS - store_params.project = &p->project; - store_params.strategy = strategy | SaveStrategy::Zip64; + store_params.proFn = proFn; + store_params.id_bboxes = plate_bboxes;//BBS + store_params.project = &p->project; + store_params.strategy = strategy | SaveStrategy::Zip64; + // get type and color for platedata - auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + auto* filament_color = dynamic_cast(cfg.option("filament_colour")); auto* nozzle_diameter_option = dynamic_cast(cfg.option("nozzle_diameter")); - auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); std::string nozzle_diameter_str; if (nozzle_diameter_option) nozzle_diameter_str = nozzle_diameter_option->serialize(); @@ -19064,14 +19018,14 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy std::string printer_model_id = preset_bundle.printers.get_edited_preset().get_printer_type(&preset_bundle); for (int i = 0; i < plate_data_list.size(); i++) { - PlateData* plate_data = plate_data_list[i]; + PlateData *plate_data = plate_data_list[i]; plate_data->printer_model_id = printer_model_id; plate_data->nozzle_diameters = nozzle_diameter_str; for (auto it = plate_data->slice_filaments_info.begin(); it != plate_data->slice_filaments_info.end(); it++) { std::string display_filament_type; - it->type = cfg.get_filament_type(display_filament_type, it->id); + it->type = cfg.get_filament_type(display_filament_type, it->id); it->filament_id = filament_id_opt ? filament_id_opt->get_at(it->id) : ""; - it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; + it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; // save filament info used in curr plate int index = p->partplate_list.get_curr_plate_index(); if (store_params.id_bboxes.size() > index) { @@ -19095,11 +19049,11 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy if (p->model.design_info == nullptr) { // set designInfo before export and reset after export if (wxGetApp().is_user_login()) { - p->model.design_info = std::make_shared(); - // p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); + p->model.design_info = std::make_shared(); + //p->model.design_info->Designer = wxGetApp().getAgent()->get_user_nickname(); p->model.design_info->Designer = ""; p->model.design_info->DesignerUserId = wxGetApp().getAgent()->get_user_id(); - BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = " << ""; + BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer = "<< ""; BOOST_LOG_TRIVIAL(trace) << "design_info prepare, designer_user_id = " << p->model.design_info->DesignerUserId; } } @@ -19116,12 +19070,15 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy p->set_project_filename(path); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << path; } - } else { + } + else { ret = -1; } - if (project_presets.size() > 0) { - for (unsigned int i = 0; i < project_presets.size(); i++) { + if (project_presets.size() > 0) + { + for (unsigned int i = 0; i < project_presets.size(); i++) + { delete project_presets[i]; } project_presets.clear(); @@ -19129,40 +19086,61 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy release_PlateData_list(plate_data_list); - for (unsigned int i = 0; i < calibration_thumbnails.size(); i++) { - // release the data here, as it will always be generated when export + for (unsigned int i = 0; i < calibration_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export calibration_thumbnails[i]->reset(); } for (unsigned int i = 0; i < no_light_thumbnails.size(); i++) { // release the data here, as it will always be generated when export no_light_thumbnails[i]->reset(); } - for (unsigned int i = 0; i < top_thumbnails.size(); i++) { - // release the data here, as it will always be generated when export + for (unsigned int i = 0; i < top_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export top_thumbnails[i]->reset(); } top_thumbnails.clear(); - for (unsigned int i = 0; i < picking_thumbnails.size(); i++) { - // release the data here, as it will always be generated when export - picking_thumbnails[i]->reset(); - ; + for (unsigned int i = 0; i < picking_thumbnails.size(); i++) + { + //release the data here, as it will always be generated when export + picking_thumbnails[i]->reset();; } picking_thumbnails.clear(); return ret; } -void Plater::publish_project() { return; } +void Plater::publish_project() +{ + return; +} -void Plater::reload_from_disk() { p->reload_from_disk(); } -void Plater::replace_with_stl() { p->replace_with_stl(); } +void Plater::reload_from_disk() +{ + p->reload_from_disk(); +} -void Plater::replace_all_with_stl() { p->replace_all_with_stl(); } +void Plater::replace_with_stl() +{ + p->replace_with_stl(); +} -void Plater::reload_all_from_disk() { p->reload_all_from_disk(); } +void Plater::replace_all_with_stl() +{ + p->replace_all_with_stl(); +} -bool Plater::has_toolpaths_to_export() const { return p->preview->get_canvas3d()->has_toolpaths_to_export(); } +void Plater::reload_all_from_disk() +{ + p->reload_all_from_disk(); +} + +bool Plater::has_toolpaths_to_export() const +{ + return p->preview->get_canvas3d()->has_toolpaths_to_export(); +} void Plater::export_toolpaths_to_obj() const { @@ -19177,11 +19155,13 @@ void Plater::export_toolpaths_to_obj() const p->preview->get_canvas3d()->export_toolpaths_to_obj(into_u8(path).c_str()); } -bool Plater::is_empty_project() { return model().objects.empty(); } +bool Plater::is_empty_project() { + return model().objects.empty(); +} bool Plater::is_multi_extruder_ams_empty() { - std::vector extruder_ams_count_str = p->config->option("extruder_ams_count", true)->values; - std::vector> extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); + std::vector extruder_ams_count_str = p->config->option("extruder_ams_count", true)->values; + std::vector> extruder_ams_counts = get_extruder_ams_count(extruder_ams_count_str); for (auto extruder_ams_count : extruder_ams_counts) { for (auto iter = extruder_ams_count.begin(); iter != extruder_ams_count.end(); ++iter) { if (iter->second != 0) @@ -19191,14 +19171,13 @@ bool Plater::is_multi_extruder_ams_empty() return true; } -// BBS: add multiple plate reslice logic +//BBS: add multiple plate reslice logic void Plater::reslice() { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(", Line %1%: enter, process_completed_with_error=%2%") % __LINE__ % - p->process_completed_with_error; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: enter, process_completed_with_error=%2%")%__LINE__ %p->process_completed_with_error; // There is "invalid data" button instead "slice now" - if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) { + if (p->process_completed_with_error == p->partplate_list.get_curr_plate_index()) + { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": process_completed_with_error, return directly"); reset_gcode_toolpaths(); return; @@ -19234,7 +19213,8 @@ void Plater::reslice() // get stopped. unsigned timeout_ms = 10000; if (!stop_queue(this->get_ui_job_worker(), timeout_ms)) { - BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " << timeout_ms << " milliseconds timeout!"; + BOOST_LOG_TRIVIAL(error) << "Could not stop UI job within " + << timeout_ms << " milliseconds timeout!"; return; } @@ -19249,33 +19229,32 @@ void Plater::reslice() object->sla_points_status = sla::PointsStatus::Generating; } - // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); // If the SLA processing of just a single object's supports is running, restart slicing for the whole object. this->p->background_process.set_task(PrintBase::TaskParams()); // Only restarts if the state is valid. - // BBS: jusdge the result + //BBS: jusdge the result bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(", Line %1%: restart background,state=%2%, result=%3%") % __LINE__ % state % result; - if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) { - // BBS: add logs - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(": state %1% is UPDATE_BACKGROUND_PROCESS_INVALID, can not slice") % state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: restart background,state=%2%, result=%3%")%__LINE__%state %result; + if ((state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) != 0) + { + //BBS: add logs + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": state %1% is UPDATE_BACKGROUND_PROCESS_INVALID, can not slice") % state; p->update_fff_scene_only_shells(); return; } - if ((!result) && p->m_slice_all && (p->m_cur_slice_plate < (p->partplate_list.get_plate_count() - 1))) { - // slice next - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ - << boost::format(": in slicing all, current plate %1% already sliced, skip to next") % - p->m_cur_slice_plate; - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); + if ((!result) && p->m_slice_all && (p->m_cur_slice_plate < (p->partplate_list.get_plate_count() - 1))) + { + //slice next + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": in slicing all, current plate %1% already sliced, skip to next") % p->m_cur_slice_plate ; + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); // Post the "complete" callback message, so that it will slice the next plate soon wxQueueEvent(this, evt.Clone()); p->m_is_slicing = true; @@ -19290,31 +19269,32 @@ void Plater::reslice() bool clean_gcode_toolpaths = true; // BBS - if (p->background_process.running()) { - // p->ready_to_slice = false; + if (p->background_process.running()) + { + //p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process is running, m_is_slicing is true"); - } else if (!p->background_process.empty() && !p->background_process.idle()) { - // p->show_action_buttons(true); - // p->ready_to_slice = true; + } + else if (!p->background_process.empty() && !p->background_process.idle()) { + //p->show_action_buttons(true); + //p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": background process changes to not_idle, set ready_to_slice back to true"); - } else { - // BBS: add reset logic for empty plate - PartPlate* current_plate = p->background_process.get_current_plate(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process changes to not_idle, set ready_to_slice back to true"); + } + else { + //BBS: add reset logic for empty plate + PartPlate * current_plate = p->background_process.get_current_plate(); if (!current_plate->has_printable_instances()) { clean_gcode_toolpaths = true; current_plate->update_slice_result_valid_state(false); - } else { + } + else { clean_gcode_toolpaths = false; current_plate->update_slice_result_valid_state(true); } p->main_frame->update_slice_print_status(MainFrame::eEventSliceUpdate, false); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": background process in idle state, use previous result, clean_gcode_toolpaths=%1%") % - clean_gcode_toolpaths; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": background process in idle state, use previous result, clean_gcode_toolpaths=%1%")%clean_gcode_toolpaths; } if (clean_gcode_toolpaths) @@ -19322,8 +19302,7 @@ void Plater::reslice() p->preview->reload_print(); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); record_slice_preset("slicing"); } @@ -19331,12 +19310,14 @@ void Plater::reslice() void Plater::record_slice_preset(std::string action) { // record slice preset - try { + try + { json j; auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset; if (printer_preset.is_system) { j["printer_preset_name"] = printer_preset.name; - } else { + } + else { j["printer_preset_name"] = printer_preset.config.opt_string("inherits"); } const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map; @@ -19353,7 +19334,8 @@ void Plater::record_slice_preset(std::string action) auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]); if (filament_preset->is_system) { j["filament_preset_" + std::to_string(i)] = filament_preset->name; - } else { + } + else { j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits"); } } @@ -19361,12 +19343,12 @@ void Plater::record_slice_preset(std::string action) Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset(); if (print_preset.is_system) { j["process_preset"] = print_preset.name; - } else { + } + else { j["process_preset"] = print_preset.config.opt_string("inherits"); } j["support_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("support_type")); - j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at( - print_preset.config.opt_enum("sparse_infill_pattern")); + j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("sparse_infill_pattern")); j["sparse_infill_density"] = print_preset.config.opt("sparse_infill_density")->value; j["brim_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("brim_type")); @@ -19374,7 +19356,7 @@ void Plater::record_slice_preset(std::string action) if (p->background_process.fff_print()) { const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config(); - json values = json::array(); + json values = json::array(); if (full_config.has("different_settings_to_system")) { std::vector different_values = full_config.option("different_settings_to_system")->values; for (auto& item : different_values) { @@ -19384,26 +19366,28 @@ void Plater::record_slice_preset(std::string action) j["different_settings_to_system"] = values; } - j["record_event"] = action; + j["record_event"] = action; NetworkAgent* agent = wxGetApp().getAgent(); - } catch (...) { + } + catch (...) + { return; } } -// BBS: add project slicing related logic +//BBS: add project slicing related logic int Plater::start_next_slice() { // Stop arrange and (or) optimize rotation tasks. - // this->stop_jobs(); + //this->stop_jobs(); - // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": update_background_process returns %1%") % state; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": update_background_process returns %1%")%state; if (!p->partplate_list.get_curr_plate()->can_slice()) { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": found invalidated apply in update_background_process."); @@ -19412,27 +19396,34 @@ int Plater::start_next_slice() // Only restarts if the state is valid. bool result = this->p->restart_background_process(state | priv::UPDATE_BACKGROUND_PROCESS_FORCE_RESTART); - if (!result) { - // slice next - SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, SlicingProcessCompletedEvent::Finished, nullptr); + if (!result) + { + //slice next + SlicingProcessCompletedEvent evt(EVT_PROCESS_COMPLETED, 0, + SlicingProcessCompletedEvent::Finished, nullptr); // Post the "complete" callback message, so that it will slice the next plate soon wxQueueEvent(this, evt.Clone()); } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": restart_background_process returns %1%") % result; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": restart_background_process returns %1%")%result; return 0; } -void Plater::reslice_SLA_supports(const ModelObject& object, bool postpone_error_messages) -{ reslice_SLA_until_step(slaposPad, object, postpone_error_messages); } -void Plater::reslice_SLA_hollowing(const ModelObject& object, bool postpone_error_messages) -{ reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); } - -void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject& object, bool postpone_error_messages) +void Plater::reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages) { - // FIXME Don't reslice if export of G-code or sending to OctoPrint is running. - // bitmask of UpdateBackgroundProcessReturnState + reslice_SLA_until_step(slaposPad, object, postpone_error_messages); +} + +void Plater::reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages) +{ + reslice_SLA_until_step(slaposDrillHoles, object, postpone_error_messages); +} + +void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages) +{ + //FIXME Don't reslice if export of G-code or sending to OctoPrint is running. + // bitmask of UpdateBackgroundProcessReturnState unsigned int state = this->p->update_background_process(true, postpone_error_messages); if (state & priv::UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE) this->p->view3D->reload_scene(false); @@ -19448,7 +19439,7 @@ void Plater::reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject& // Otherwise calculate everything, but start with the provided object. if (!this->p->background_processing_enabled()) { task.single_model_instance_only = true; - task.to_object_step = step; + task.to_object_step = step; } this->p->background_process.set_task(task); // and let the background processing start. @@ -19459,7 +19450,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) // if physical_printer is selected, send gcode for this printer // DynamicPrintConfig* physical_printer_config = wxGetApp().preset_bundle->physical_printers.get_selected_printer_config(); DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - if (!physical_printer_config || p->model.objects.empty()) + if (! physical_printer_config || p->model.objects.empty()) return; PrintHostJob upload_job(physical_printer_config); @@ -19468,7 +19459,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) // Orca: the use_3mf printer option makes us send a .gcode.3mf to the printer const auto* use_3mf_opt = physical_printer_config->option("use_3mf"); - const bool use_3mf = use_3mf_opt != nullptr && use_3mf_opt->value; + const bool use_3mf = use_3mf_opt != nullptr && use_3mf_opt->value; upload_job.upload_data.use_3mf = use_3mf; // Orca: the concrete plate to export/send (PLATE_CURRENT_IDX resolves to the current plate). @@ -19482,7 +19473,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) unsigned int state = this->p->update_restart_background_process(false, false); if (state & priv::UPDATE_BACKGROUND_PROCESS_INVALID) return; - default_output_file = this->p->background_process.output_filepath_for_project(into_path(this->p->get_project_filename(".3mf"))); + default_output_file = this->p->background_process.output_filepath_for_project( + into_path(this->p->get_project_filename(".3mf"))); } catch (const Slic3r::PlaceholderParserError& ex) { // Show the error with monospaced font. show_error(this, ex.what(), true); @@ -19518,8 +19510,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } { - auto preset_bundle = wxGetApp().preset_bundle; - auto config = get_app_config(); + auto preset_bundle = wxGetApp().preset_bundle; + auto config = get_app_config(); const auto host_type_opt = physical_printer_config->option>("host_type"); const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htElegooLink; @@ -19534,8 +19526,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); } else if (host_type == htCrealityPrint) { - pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), - groups, storage_paths, storage_names, + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, config->get_bool("open_device_tab_post_upload"), upload_job.printhost.get()); } else if (flashforge_local_api) { @@ -19546,10 +19538,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } std::vector slots; - bool supports_material_station = false; + bool supports_material_station = false; { wxBusyCursor wait; - wxString msg; + wxString msg; if (!flashforge_host->fetch_material_slots(slots, &supports_material_station, msg)) { show_error(this, msg.empty() ? _L("Unable to log in to the Flashforge printer.") : msg, false); return; @@ -19557,10 +19549,10 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } std::vector project_filaments; - PlateDataPtrs plate_data_list; - DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config(); - const auto* filament_color = dynamic_cast(cfg.option("filament_colour")); - const auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); + PlateDataPtrs plate_data_list; + DynamicPrintConfig cfg = wxGetApp().preset_bundle->full_config(); + const auto* filament_color = dynamic_cast(cfg.option("filament_colour")); + const auto* filament_id_opt = dynamic_cast(cfg.option("filament_ids")); auto enrich_project_filaments = [&](std::vector& filaments) { for (auto& filament : filaments) { if (filament.id < 0) @@ -19569,7 +19561,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) std::string display_filament_type; try { filament.type = cfg.get_filament_type(display_filament_type, filament.id); - } catch (...) {} + } catch (...) { + } if (filament.type.empty()) filament.type = display_filament_type; @@ -19584,9 +19577,7 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) }; p->partplate_list.store_to_3mf_structure(plate_data_list, true, plate_idx); - PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast(plate_data_list.size())) ? - plate_data_list[resolved_plate_idx] : - nullptr; + PlateData* selected_plate_data = (resolved_plate_idx >= 0 && resolved_plate_idx < static_cast(plate_data_list.size())) ? plate_data_list[resolved_plate_idx] : nullptr; if (selected_plate_data == nullptr && !plate_data_list.empty()) selected_plate_data = plate_data_list.front(); @@ -19602,10 +19593,13 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) enrich_project_filaments(project_filaments); release_PlateData_list(plate_data_list); - pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), - groups, storage_paths, storage_names, - config->get_bool("open_device_tab_post_upload"), flashforge_host, - supports_material_station, std::move(slots), project_filaments); + pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, + storage_paths, storage_names, + config->get_bool("open_device_tab_post_upload"), + flashforge_host, + supports_material_station, + std::move(slots), + project_filaments); } else { pDlg = std::make_unique(default_output_file, upload_job.printhost->get_post_upload_actions(), groups, storage_paths, storage_names, config->get_bool("open_device_tab_post_upload")); @@ -19618,11 +19612,11 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) config->set_bool("open_device_tab_post_upload", pDlg->switch_to_device_tab()); // PrintHostUpload upload_data; - upload_job.switch_to_device_tab = pDlg->switch_to_device_tab(); - upload_job.upload_data.upload_path = pDlg->filename(); - upload_job.upload_data.post_action = pDlg->post_action(); - upload_job.upload_data.group = pDlg->group(); - upload_job.upload_data.storage = pDlg->storage(); + upload_job.switch_to_device_tab = pDlg->switch_to_device_tab(); + upload_job.upload_data.upload_path = pDlg->filename(); + upload_job.upload_data.post_action = pDlg->post_action(); + upload_job.upload_data.group = pDlg->group(); + upload_job.upload_data.storage = pDlg->storage(); upload_job.upload_data.extended_info = pDlg->extendedInfo(); // Orca: gcode inside a .gcode.3mf is index-coded (Metadata/plate_.gcode) and a bundle may // carry several of them, so the upload must name which plate to print via a 1-based plateindex. @@ -19636,10 +19630,8 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn) } // Show "Is printer clean" dialog for PrusaConnect - Upload and print. - if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && - upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { - GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), - wxOK | wxCANCEL); + if (std::string(upload_job.printhost->get_name()) == "PrusaConnect" && upload_job.upload_data.post_action == PrintHostPostUploadAction::StartPrint) { + GUI::MessageDialog dlg(nullptr, _L("Is the printer ready? Is the print sheet in place, empty and clean?"), _L("Upload and Print"), wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) return; } @@ -19669,14 +19661,15 @@ int Plater::send_gcode(int plate_idx, Export3mfProgressFn proFn) try { p->m_print_job_data._3mf_path = fs::path(plate->get_tmp_gcode_path()); p->m_print_job_data._3mf_path.replace_extension("3mf"); - } catch (std::exception&) { + } + catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; return -1; } SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithGcode | SaveStrategy::SkipAuxiliary; #if !BBL_RELEASE_TO_PUBLIC - // only save model in QA environment + //only save model in QA environment std::string sel = get_app_config()->get("iot_environment"); if (sel == ENV_PRE_HOST) strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode; @@ -19696,19 +19689,20 @@ int Plater::export_config_3mf(int plate_idx, Export3mfProgressFn proFn) PartPlate* plate = get_partplate_list().get_curr_plate(); try { p->m_print_job_data._3mf_config_path = fs::path(plate->get_temp_config_3mf_path()); - } catch (std::exception&) { + } + catch (std::exception&) { BOOST_LOG_TRIVIAL(error) << "generate 3mf path failed"; return -1; } SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SkipModel | SaveStrategy::WithSliceInfo | SaveStrategy::SkipAuxiliary; - result = export_3mf(p->m_print_job_data._3mf_config_path, strategy, plate_idx, proFn); + result = export_3mf(p->m_print_job_data._3mf_config_path, strategy, plate_idx, proFn); return result; } -// BBS -void Plater::send_calibration_job_finished(wxCommandEvent& evt) +//BBS +void Plater::send_calibration_job_finished(wxCommandEvent & evt) { p->main_frame->request_select_tab(TAB_ID_CALIBRATION); auto calibration_panel = p->main_frame->m_calibration; @@ -19722,9 +19716,9 @@ void Plater::send_calibration_job_finished(wxCommandEvent& evt) evt.Skip(); } -void Plater::print_job_finished(wxCommandEvent& evt) +void Plater::print_job_finished(wxCommandEvent &evt) { - // start print failed + //start print failed if (p) { #ifdef __APPLE__ p->hide_select_machine_dlg(); @@ -19737,60 +19731,63 @@ void Plater::print_job_finished(wxCommandEvent& evt) #endif // __APPLE__ } + Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) - return; + if (!dev) return; dev->set_selected_machine(evt.GetString().ToStdString()); p->main_frame->request_select_tab(TAB_ID_MONITOR); - // jump to monitor and select device status panel + //jump to monitor and select device status panel MonitorPanel* curr_monitor = p->main_frame->m_monitor; - if (curr_monitor) - curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); + if(curr_monitor) + curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); } void Plater::send_job_finished(wxCommandEvent& evt) { Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); - if (!dev) - return; - // dev->set_selected_machine(evt.GetString().ToStdString()); + if (!dev) return; + //dev->set_selected_machine(evt.GetString().ToStdString()); send_gcode_finish(evt.GetString()); p->hide_send_to_printer_dlg(); - // p->main_frame->request_select_tab(TAB_ID_MONITOR); + //p->main_frame->request_select_tab(TAB_ID_MONITOR); ////jump to monitor and select device status panel - // MonitorPanel* curr_monitor = p->main_frame->m_monitor; - // if (curr_monitor) - // curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); + //MonitorPanel* curr_monitor = p->main_frame->m_monitor; + //if (curr_monitor) + // curr_monitor->get_tabpanel()->ChangeSelection(MonitorPanel::PrinterTab::PT_STATUS); } -void Plater::publish_job_finished(wxCommandEvent& evt) +void Plater::publish_job_finished(wxCommandEvent &evt) { p->m_publish_dlg->EndModal(wxID_OK); - // GUI::wxGetApp().load_url(evt.GetString()); - // GUI::wxGetApp().open_publish_page_dialog(evt.GetString()); + // GUI::wxGetApp().load_url(evt.GetString()); + //GUI::wxGetApp().open_publish_page_dialog(evt.GetString()); } // Called when the Eject button is pressed. void Plater::eject_drive() { - wxBusyCursor wait; + wxBusyCursor wait; wxGetApp().removable_drive_manager()->set_and_verify_last_save_path(p->last_output_dir_path); - wxGetApp().removable_drive_manager()->eject_drive(); + wxGetApp().removable_drive_manager()->eject_drive(); } -void Plater::take_snapshot(const std::string& snapshot_name) { p->take_snapshot(snapshot_name); } -// void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); } -void Plater::take_snapshot(const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type) -{ p->take_snapshot(snapshot_name, snapshot_type); } -// void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, -// snapshot_type); } +void Plater::take_snapshot(const std::string &snapshot_name) { p->take_snapshot(snapshot_name); } +//void Plater::take_snapshot(const wxString &snapshot_name) { p->take_snapshot(snapshot_name); } +void Plater::take_snapshot(const std::string &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } +//void Plater::take_snapshot(const wxString &snapshot_name, UndoRedo::SnapshotType snapshot_type) { p->take_snapshot(snapshot_name, snapshot_type); } void Plater::suppress_snapshots() { p->suppress_snapshots(); } void Plater::allow_snapshots() { p->allow_snapshots(); } // BBS: single snapshot -void Plater::single_snapshots_enter(SingleSnapshot* single) { p->single_snapshots_enter(single); } -void Plater::single_snapshots_leave(SingleSnapshot* single) { p->single_snapshots_leave(single); } +void Plater::single_snapshots_enter(SingleSnapshot *single) +{ + p->single_snapshots_enter(single); +} +void Plater::single_snapshots_leave(SingleSnapshot *single) +{ + p->single_snapshots_leave(single); +} void Plater::undo() { p->undo(); } void Plater::redo() { p->redo(); } void Plater::undo_to(int selection) @@ -19816,9 +19813,9 @@ void Plater::redo_to(int selection) bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** out_text) { const std::vector& ss_stack = p->undo_redo_stack().snapshots(); - const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -(++idx) : idx); - if (0 < idx_in_ss_stack && (size_t) idx_in_ss_stack < ss_stack.size() - 1) { + if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { *out_text = ss_stack[idx_in_ss_stack].name.c_str(); return true; } @@ -19826,19 +19823,18 @@ bool Plater::undo_redo_string_getter(const bool is_undo, int idx, const char** o return false; } -int Plater::update_print_required_data(Slic3r::DynamicPrintConfig config, - Slic3r::Model model, - Slic3r::PlateDataPtrs plate_data_list, - std::string file_name, - std::string file_path) -{ return p->update_print_required_data(config, model, plate_data_list, file_name, file_path); } +int Plater::update_print_required_data(Slic3r::DynamicPrintConfig config, Slic3r::Model model, Slic3r::PlateDataPtrs plate_data_list, std::string file_name, std::string file_path) +{ + return p->update_print_required_data(config, model, plate_data_list, file_name, file_path); +} + void Plater::undo_redo_topmost_string_getter(const bool is_undo, std::string& out_text) { const std::vector& ss_stack = p->undo_redo_stack().snapshots(); - const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0); + const int idx_in_ss_stack = p->get_active_snapshot_index() + (is_undo ? -1 : 0); - if (0 < idx_in_ss_stack && (size_t) idx_in_ss_stack < ss_stack.size() - 1) { + if (0 < idx_in_ss_stack && (size_t)idx_in_ss_stack < ss_stack.size() - 1) { out_text = ss_stack[idx_in_ss_stack].name; return; } @@ -19850,7 +19846,7 @@ bool Plater::search_string_getter(int idx, const char** label, const char** tool { const Search::OptionsSearcher& search_list = p->sidebar->get_searcher(); - if (0 <= idx && (size_t) idx < search_list.size()) { + if (0 <= idx && (size_t)idx < search_list.size()) { search_list[idx].get_marked_label_and_tooltip(label, tooltip); return true; } @@ -19877,7 +19873,7 @@ void Plater::on_filament_count_change(size_t num_filaments) sidebar().on_filament_count_change(num_filaments); sidebar().obj_list()->update_objects_list_filament_column(num_filaments); - Slic3r::GUI::PartPlateList& plate_list = get_partplate_list(); + Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); plate_list.set_filament_count(num_filaments); for (int i = 0; i < plate_list.get_plate_count(); ++i) { PartPlate* part_plate = plate_list.get_plate(i); @@ -19891,16 +19887,13 @@ void Plater::on_filament_count_change(size_t num_filaments) } } -void Plater::on_filaments_delete(size_t num_filaments, - size_t filament_id, - int replace_filament_id, - const std::vector& is_mixed_before_delete) +void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int replace_filament_id, const std::vector& is_mixed_before_delete) { // only update elements in plater update_filament_colors_in_full_config(); // update fisrt print sequence and other layer sequence - // move to partplate->on_filament_deleted + //move to partplate->on_filament_deleted /*Slic3r::GUI::PartPlateList &plate_list = get_partplate_list(); for (int i = 0; i < plate_list.get_plate_count(); ++i) { PartPlate *part_plate = plate_list.get_plate(i); @@ -19911,13 +19904,12 @@ void Plater::on_filaments_delete(size_t num_filaments, // A volume assigned to a mixed slot legitimately sits past the physical filament count, so // the paint cleanup must know which slots were mixed. Callers that already shrank the arrays // pass the pre-delete flags; otherwise read the current ones. - const auto& is_mixed = is_mixed_before_delete.empty() ? - wxGetApp().preset_bundle->project_config.option("filament_is_mixed")->values : - is_mixed_before_delete; - for (ModelObject* mo : wxGetApp().model().objects) { - for (ModelVolume* mv : mo->volumes) { - mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1, - is_mixed); // this function is 1 base + const auto &is_mixed = is_mixed_before_delete.empty() + ? wxGetApp().preset_bundle->project_config.option("filament_is_mixed")->values + : is_mixed_before_delete; + for (ModelObject *mo : wxGetApp().model().objects) { + for (ModelVolume *mv : mo->volumes) { + mv->update_extruder_count_when_delete_filament(num_filaments, filament_id + 1, replace_filament_id + 1, is_mixed); // this function is 1 base } } @@ -19927,10 +19919,10 @@ void Plater::on_filaments_delete(size_t num_filaments, sidebar().obj_list()->update_objects_list_filament_column_when_delete_filament(filament_id, num_filaments, replace_filament_id); // update global support filament - static const char* keys[] = {"support_filament", "support_interface_filament"}; + static const char *keys[] = {"support_filament", "support_interface_filament"}; for (auto key : keys) if (p->config->has(key)) { - if (p->config->opt_int(key) == filament_id + 1) + if(p->config->opt_int(key) == filament_id + 1) (*(p->config)).erase(key); else { int new_value = p->config->opt_int(key) > filament_id ? p->config->opt_int(key) - 1 : p->config->opt_int(key); @@ -19948,7 +19940,7 @@ void Plater::on_filaments_delete(size_t num_filaments, }); if (replace_filament_id == -1) item->second.gcodes.erase(iter, item->second.gcodes.end()); - else if (iter != item->second.gcodes.end()) { + else if(iter != item->second.gcodes.end()) { iter->extruder = replace_filament_id + 1; } @@ -19961,10 +19953,10 @@ void Plater::on_filaments_delete(size_t num_filaments, std::vector Plater::get_extruders_colors() { - unsigned char rgba_color[4] = {}; - std::vector colors = get_extruder_colors_from_plater_config(); + unsigned char rgba_color[4] = {}; + std::vector colors = get_extruder_colors_from_plater_config(); std::vector colors_out(colors.size()); - for (const std::string& color : colors) { + for (const std::string &color : colors) { Slic3r::GUI::BitmapCache::parse_color4(color, rgba_color); size_t color_idx = &color - &colors.front(); colors_out[color_idx] = { @@ -19977,21 +19969,24 @@ std::vector Plater::get_extruders_colors() return colors_out; } -void Plater::on_bed_type_change(BedType bed_type) { sidebar().on_bed_type_change(bed_type); } +void Plater::on_bed_type_change(BedType bed_type) +{ + sidebar().on_bed_type_change(bed_type); +} bool Plater::update_filament_colors_in_full_config() { - DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; - const auto& full_config = wxGetApp().preset_bundle->full_config(); - ConfigOptionStrings* color_opt = project_config.option("filament_colour"); + DynamicPrintConfig& project_config = wxGetApp().preset_bundle->project_config; + const auto& full_config = wxGetApp().preset_bundle->full_config(); + ConfigOptionStrings* color_opt = project_config.option("filament_colour"); const ConfigOptionStrings* type_opt = full_config.option("filament_type"); p->config->option("filament_colour")->values = color_opt->values; - p->config->option("filament_type")->values = type_opt->values; + p->config->option("filament_type")->values = type_opt->values; return true; } -void Plater::config_change_notification(const DynamicPrintConfig& config, const std::string& key) +void Plater::config_change_notification(const DynamicPrintConfig &config, const std::string& key) { GLCanvas3D* view3d_canvas = get_view3D_canvas3D(); if (key == std::string("print_sequence")) { @@ -20001,23 +19996,24 @@ void Plater::config_change_notification(const DynamicPrintConfig& config, const if (seq_print->value == PrintSequence::ByObject) { std::string info_text = _u8L("Print By Object: \nWe suggest using auto-arrange to avoid collisions when printing."); notify_manager->bbl_show_seqprintinfo_notification(info_text); - } else + } + else notify_manager->bbl_close_seqprintinfo_notification(); } } // notification for more options } -void Plater::on_config_change(const DynamicPrintConfig& config) +void Plater::on_config_change(const DynamicPrintConfig &config) { - bool update_scheduled = false; + bool update_scheduled = false; bool bed_shape_changed = false; - // bool print_sequence_changed = false; + //bool print_sequence_changed = false; t_config_option_keys diff_keys = p->config->diff(config); size_t old_nozzle_size = 1, new_nozzle_size = 1; - auto* opt_old = p->config->option("nozzle_diameter"); - auto* opt_new = config.option("nozzle_diameter"); + auto * opt_old = p->config->option("nozzle_diameter"); + auto * opt_new = config.option("nozzle_diameter"); if (opt_old && opt_new) { old_nozzle_size = opt_old->values.size(); new_nozzle_size = opt_new->values.size(); @@ -20059,34 +20055,43 @@ void Plater::on_config_change(const DynamicPrintConfig& config) p->reset_gcode_toolpaths(); p->view3D->get_canvas3d()->reset_sequential_print_clearance(); p->preview->get_canvas3d()->reset_volumes(); - // BBS: invalid all the slice results + //BBS: invalid all the slice results p->partplate_list.invalid_all_slice_result(); } - // BBS: add bed_exclude_area - else if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "bed_custom_texture" || - opt_key == "bed_custom_model" || opt_key == "extruder_clearance_height_to_lid" || - opt_key == "extruder_clearance_height_to_rod") { + //BBS: add bed_exclude_area + else if (opt_key == "printable_area" || opt_key == "bed_exclude_area" + || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model" + || opt_key == "extruder_clearance_height_to_lid" + || opt_key == "extruder_clearance_height_to_rod") { bed_shape_changed = true; - update_scheduled = true; - } else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") { + update_scheduled = true; + } + else if (opt_key == "bed_shape" || opt_key == "bed_custom_texture" || opt_key == "bed_custom_model") { bed_shape_changed = true; - update_scheduled = true; - } else if (boost::starts_with(opt_key, "enable_prime_tower") || boost::starts_with(opt_key, "prime_tower") || - boost::starts_with(opt_key, "wipe_tower") || opt_key == "filament_minimal_purge_on_wipe_tower" || - opt_key == "single_extruder_multi_material" || - // BBS - opt_key == "prime_volume") { update_scheduled = true; - } else if (opt_key == "extruder_colour") { + } + else if (boost::starts_with(opt_key, "enable_prime_tower") || + boost::starts_with(opt_key, "prime_tower") || + boost::starts_with(opt_key, "wipe_tower") || + opt_key == "filament_minimal_purge_on_wipe_tower" || + opt_key == "single_extruder_multi_material" || + // BBS + opt_key == "prime_volume") { update_scheduled = true; - // p->sidebar->obj_list()->update_extruder_colors(); - } else if (opt_key == "printable_height") { + } + else if(opt_key == "extruder_colour") { + update_scheduled = true; + //p->sidebar->obj_list()->update_extruder_colors(); + } + else if (opt_key == "printable_height") { bed_shape_changed = true; - update_scheduled = true; - } else if (opt_key == "print_sequence") { update_scheduled = true; - // print_sequence_changed = true; - } else if (opt_key == "printer_model") { + } + else if (opt_key == "print_sequence") { + update_scheduled = true; + //print_sequence_changed = true; + } + else if (opt_key == "printer_model") { p->reset_gcode_toolpaths(); if (old_nozzle_size != new_nozzle_size) { update_flush_volume_matrix(old_nozzle_size, new_nozzle_size); @@ -20094,11 +20099,12 @@ void Plater::on_config_change(const DynamicPrintConfig& config) // update to force bed selection(for texturing) bed_shape_changed = true; - update_scheduled = true; + update_scheduled = true; } // Orca: update when *_filament changed - else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || opt_key == "outer_wall_filament_id" || - opt_key == "inner_wall_filament_id" || opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" || + else if (opt_key == "support_interface_filament" || opt_key == "support_filament" || + opt_key == "outer_wall_filament_id" || opt_key == "inner_wall_filament_id" || + opt_key == "sparse_infill_filament_id" || opt_key == "internal_solid_filament_id" || opt_key == "top_surface_filament_id" || opt_key == "bottom_surface_filament_id") { update_scheduled = true; } @@ -20121,45 +20127,42 @@ void Plater::on_config_change(const DynamicPrintConfig& config) void Plater::update_flush_volume_matrix(size_t old_nozzle_size, size_t new_nozzle_size) { - size_t nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - Slic3r::DynamicPrintConfig* project_config = &wxGetApp().preset_bundle->project_config; + size_t nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + Slic3r::DynamicPrintConfig *project_config = &wxGetApp().preset_bundle->project_config; // Verify whether it is the first time start Studio - size_t filament_nums = project_config->option("filament_colour")->values.size(); + size_t filament_nums = project_config->option("filament_colour")->values.size(); size_t flush_volume_size = project_config->option("flush_volumes_matrix")->values.size(); assert(nozzle_nums == new_nozzle_size); if (old_nozzle_size < new_nozzle_size) { - std::vector first_flush_volume_mtx = - get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + + std::vector first_flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); if (first_flush_volume_mtx.size() == filament_nums * filament_nums * new_nozzle_size) { // load file - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, first_flush_volume_mtx, -1, - new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, first_flush_volume_mtx, -1, new_nozzle_size); } else { first_flush_volume_mtx.resize(filament_nums * filament_nums, 0); std::vector flush_volume_mtx; for (size_t i = 0; i < new_nozzle_size; ++i) { flush_volume_mtx.insert(flush_volume_mtx.end(), first_flush_volume_mtx.begin(), first_flush_volume_mtx.end()); } - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, flush_volume_mtx, -1, - new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, flush_volume_mtx, -1, new_nozzle_size); } std::vector flush_multipliers = project_config->option("flush_multiplier")->values; flush_multipliers.resize(nozzle_nums, 1.f); project_config->option("flush_multiplier")->values = flush_multipliers; - } else if (old_nozzle_size > new_nozzle_size) { + } + else if (old_nozzle_size > new_nozzle_size) { std::vector new_flush_volume_mtx; for (size_t i = 0; i < new_nozzle_size; ++i) { - std::vector flush_volume_mtx = - get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); + std::vector flush_volume_mtx = get_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, -1, old_nozzle_size); flush_volume_mtx.resize(filament_nums * filament_nums, 0); new_flush_volume_mtx.insert(new_flush_volume_mtx.end(), flush_volume_mtx.begin(), flush_volume_mtx.end()); } std::vector flush_multipliers = project_config->option("flush_multiplier")->values; flush_multipliers.resize(nozzle_nums, 1.f); - set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, new_flush_volume_mtx, -1, - new_nozzle_size); + set_flush_volumes_matrix(project_config->option("flush_volumes_matrix")->values, new_flush_volume_mtx, -1, new_nozzle_size); project_config->option("flush_multiplier")->values = flush_multipliers; } } @@ -20173,43 +20176,32 @@ void Plater::set_bed_shape() const if (curr->is_system) texture_filename = PresetUtils::system_printer_bed_texture(*curr); else { - auto* printer_model = curr->config.opt("printer_model"); - if (printer_model != nullptr && !printer_model->value.empty()) { + auto *printer_model = curr->config.opt("printer_model"); + if (printer_model != nullptr && ! printer_model->value.empty()) { texture_filename = bundle->get_texture_for_printer_model(printer_model->value); } } } set_bed_shape(p->config->option("printable_area")->values, - // BBS: add bed exclude areas - p->config->option("bed_exclude_area")->values, - p->config->option("wrapping_exclude_area")->values, - p->config->option("printable_height")->value, - p->config->option("extruder_printable_area")->values, - p->config->option("extruder_printable_height")->values, - p->config->option("bed_custom_texture")->value.empty() ? - texture_filename : - p->config->option("bed_custom_texture")->value, - p->config->option("bed_custom_model")->value); + //BBS: add bed exclude areas + p->config->option("bed_exclude_area")->values, + p->config->option("wrapping_exclude_area")->values, + p->config->option("printable_height")->value, + p->config->option("extruder_printable_area")->values, + p->config->option("extruder_printable_height")->values, + p->config->option("bed_custom_texture")->value.empty() ? texture_filename : p->config->option("bed_custom_texture")->value, + p->config->option("bed_custom_model")->value); } -// BBS: add bed exclude area -void Plater::set_bed_shape(const Pointfs& shape, - const Pointfs& exclude_area, - const Pointfs& wrapping_exclude_area, - const double printable_height, - std::vector extruder_areas, - std::vector extruder_heights, - const std::string& custom_texture, - const std::string& custom_model, - bool force_as_custom) const +//BBS: add bed exclude area +void Plater::set_bed_shape(const Pointfs& shape, const Pointfs& exclude_area, const Pointfs& wrapping_exclude_area, const double printable_height, std::vector extruder_areas, std::vector extruder_heights, const std::string& custom_texture, const std::string& custom_model, bool force_as_custom) const { - p->set_bed_shape(make_counter_clockwise(shape), exclude_area, wrapping_exclude_area, printable_height, extruder_areas, extruder_heights, - custom_texture, custom_model, force_as_custom); + p->set_bed_shape(make_counter_clockwise(shape), exclude_area, wrapping_exclude_area, printable_height, extruder_areas, extruder_heights, custom_texture, custom_model, force_as_custom); } void Plater::force_filament_colors_update() { -// BBS: filament_color logic has been moved out of filament setting +//BBS: filament_color logic has been moved out of filament setting #if 0 bool update_scheduled = false; DynamicPrintConfig* config = p->config; @@ -20242,12 +20234,15 @@ void Plater::force_filament_colors_update() void Plater::force_print_bed_update() { - // Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print - // bed once a new Printer profile config is loaded. + // Fill in the printer model key with something which cannot possibly be valid, so that Plater::on_config_change() will update the print bed + // once a new Printer profile config is loaded. p->config->opt_string("printer_model", true) = "bbl_empty"; } -void Plater::on_activate() { this->p->show_delayed_error_message(); } +void Plater::on_activate() +{ + this->p->show_delayed_error_message(); +} // Get vector of extruder colors considering filament color, if extruder color is undefined. std::vector Plater::get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result) const @@ -20270,9 +20265,8 @@ namespace { // A gradient mixed filament fades between its two components over Z, so the UI shows it as a // two-tone swatch rather than one blended colour. Resolve each slot to its from/to endpoint // colours; non-gradient slots are left untouched. -struct MixedGradientSlot -{ - bool is_gradient = false; +struct MixedGradientSlot { + bool is_gradient = false; std::string color_from; std::string color_to; }; @@ -20285,16 +20279,12 @@ std::vector parse_mixed_gradient_slots(const Slic3r::DynamicP const auto* mixed_comp = config.option("filament_mixed_components"); const auto* grad_range = config.option("filament_mixed_gradient_range"); const auto* fil_colour = config.option("filament_colour"); - if (!is_mixed || !mixed_grad || !mixed_comp || !fil_colour) - return result; + if (!is_mixed || !mixed_grad || !mixed_comp || !fil_colour) return result; for (size_t i = 0; i < slot_count && i < is_mixed->values.size(); ++i) { - if (!is_mixed->values[i]) - continue; - if (i >= mixed_grad->values.size() || !mixed_grad->values[i]) - continue; - if (i >= mixed_comp->values.size()) - continue; + if (!is_mixed->values[i]) continue; + if (i >= mixed_grad->values.size() || !mixed_grad->values[i]) continue; + if (i >= mixed_comp->values.size()) continue; std::vector comp_ids; std::istringstream iss(mixed_comp->values[i]); @@ -20304,8 +20294,7 @@ std::vector parse_mixed_gradient_slots(const Slic3r::DynamicP if (std::sscanf(tok.c_str(), "%u", &v) == 1) comp_ids.push_back(v); } - if (comp_ids.size() != 2) - continue; + if (comp_ids.size() != 2) continue; int direction = 0; if (grad_range && i < grad_range->values.size()) { @@ -20315,11 +20304,13 @@ std::vector parse_mixed_gradient_slots(const Slic3r::DynamicP direction = (v0 > v1) ? 0 : 1; } - unsigned int from_id = (direction == 0) ? comp_ids[0] : comp_ids[1]; - unsigned int to_id = (direction == 0) ? comp_ids[1] : comp_ids[0]; + unsigned int from_id = (direction == 0) ? comp_ids[0] : comp_ids[1]; + unsigned int to_id = (direction == 0) ? comp_ids[1] : comp_ids[0]; result[i].is_gradient = true; - result[i].color_from = (from_id >= 1 && from_id <= fil_colour->values.size()) ? fil_colour->values[from_id - 1] : "#D9D9D9"; - result[i].color_to = (to_id >= 1 && to_id <= fil_colour->values.size()) ? fil_colour->values[to_id - 1] : "#D9D9D9"; + result[i].color_from = (from_id >= 1 && from_id <= fil_colour->values.size()) + ? fil_colour->values[from_id - 1] : "#D9D9D9"; + result[i].color_to = (to_id >= 1 && to_id <= fil_colour->values.size()) + ? fil_colour->values[to_id - 1] : "#D9D9D9"; } return result; } @@ -20330,8 +20321,7 @@ std::vector Plater::get_filament_colors_render_info() const { const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; std::vector color_packs; - if (!config->has("filament_multi_colour")) - return color_packs; + if (!config->has("filament_multi_colour")) return color_packs; color_packs = (config->option("filament_multi_colour"))->values; @@ -20346,16 +20336,14 @@ std::vector Plater::get_filament_colors_render_info() const std::vector Plater::get_filament_color_render_type() const { - const Slic3r::DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; - std::vector ctype; - if (!config->has("filament_colour_type")) - return ctype; + const Slic3r::DynamicPrintConfig *config = &wxGetApp().preset_bundle->project_config; + std::vector ctype; + if (!config->has("filament_colour_type")) return ctype; ctype = (config->option("filament_colour_type"))->values; auto slots = parse_mixed_gradient_slots(*config, ctype.size()); - while (ctype.size() < slots.size()) - ctype.push_back("1"); + while (ctype.size() < slots.size()) ctype.push_back("1"); for (size_t i = 0; i < ctype.size() && i < slots.size(); ++i) { if (slots[i].is_gradient) ctype[i] = "0"; @@ -20371,11 +20359,12 @@ const std::vector>& Plater::get_filament_gradient_ramps() // are built from. The cache is static rather than a Plater member because the extruder icons // ask for the ramps from MenuFactory::init(), which runs while this Plater is still inside its // own constructor, so wxGetApp().plater_ is not assigned yet. - static std::string s_ramps_key; + static std::string s_ramps_key; static std::vector> s_ramps; - static const char* ramp_keys[] = {"filament_is_mixed", "filament_mixed_gradient", "filament_mixed_components", - "filament_colour", "filament_mixed_gradient_range", "filament_mixed_gradient_curve"}; + static const char* ramp_keys[] = {"filament_is_mixed", "filament_mixed_gradient", + "filament_mixed_components", "filament_colour", + "filament_mixed_gradient_range", "filament_mixed_gradient_curve"}; const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; std::string key; @@ -20387,8 +20376,8 @@ const std::vector>& Plater::get_filament_gradient_ramps() // 64 bands outresolve every swatch drawn from this, all of which resample it down to their // own height, so one cached resolution serves the icons and both ImGui filament bars. - const auto* colour_opt = config.option("filament_colour"); - const size_t n = colour_opt ? colour_opt->values.size() : 0; + const auto* colour_opt = config.option("filament_colour"); + const size_t n = colour_opt ? colour_opt->values.size() : 0; s_ramps.assign(n, {}); for (size_t i = 0; i < n; ++i) s_ramps[i] = mixed_gradient_ramp(config, i, 64); @@ -20409,8 +20398,9 @@ std::vector Plater::get_colors_for_color_print(const GCodeProcessor if (code.type == CustomGCode::ColorChange) colors.emplace_back(code.color); } - } else { - // BBS + } + else { + //BBS colors.reserve(colors.size() + p->model.get_curr_plate_custom_gcodes().gcodes.size()); for (const CustomGCode::Item& code : p->model.get_curr_plate_custom_gcodes().gcodes) { if (code.type == CustomGCode::ColorChange) @@ -20423,23 +20413,23 @@ std::vector Plater::get_colors_for_color_print(const GCodeProcessor void Plater::set_global_filament_map_mode(FilamentMapMode mode) { - auto& project_config = wxGetApp().preset_bundle->project_config; - auto mode_ptr = project_config.option>("filament_map_mode"); + auto& project_config = wxGetApp().preset_bundle->project_config; + auto mode_ptr = project_config.option>("filament_map_mode"); FilamentMapMode old_mode = mode_ptr->value; - if (mode != old_mode) + if(mode != old_mode) on_filament_map_mode_change(); mode_ptr->value = mode; } void Plater::set_global_filament_map(const std::vector& filament_map) { - auto& project_config = wxGetApp().preset_bundle->project_config; + auto& project_config = wxGetApp().preset_bundle->project_config; project_config.option("filament_map")->values = filament_map; } void Plater::set_global_filament_volume_map(const std::vector& filament_volume_map) { - auto& project_config = wxGetApp().preset_bundle->project_config; + auto& project_config = wxGetApp().preset_bundle->project_config; project_config.option("filament_volume_map")->values = filament_volume_map; } @@ -20455,6 +20445,7 @@ std::vector Plater::get_global_filament_volume_map() const return project_config.option("filament_volume_map")->values; } + FilamentMapMode Plater::get_global_filament_map_mode() const { auto& project_config = wxGetApp().preset_bundle->project_config; @@ -20464,16 +20455,19 @@ FilamentMapMode Plater::get_global_filament_map_mode() const void Plater::on_filament_map_mode_change() { auto& plate_list = this->get_partplate_list(); - int plate_count = plate_list.get_plate_count(); + int plate_count = plate_list.get_plate_count(); for (int idx = 0; idx < plate_count; ++idx) { - auto plate = plate_list.get_plate(idx); + auto plate=plate_list.get_plate(idx); auto plate_map_mode = plate->get_filament_map_mode(); if (plate_map_mode == fmmDefault) plate->clear_filament_map(); } } -wxWindow* Plater::get_select_machine_dialog() { return p->m_select_machine_dlg; } +wxWindow* Plater::get_select_machine_dialog() +{ + return p->m_select_machine_dlg; +} void Plater::update_print_error_info(int code, std::string msg, std::string extra) { @@ -20488,20 +20482,40 @@ void Plater::update_print_error_info(int code, std::string msg, std::string extr p->main_frame->m_calibration->update_print_error_info(code, msg, extra); } -wxString Plater::get_project_filename(const wxString& extension) const { return p->get_project_filename(extension); } +wxString Plater::get_project_filename(const wxString& extension) const +{ + return p->get_project_filename(extension); +} -wxString Plater::get_export_gcode_filename(const wxString& extension, bool only_filename, bool export_all) const -{ return p->get_export_gcode_filename(extension, only_filename, export_all); } +wxString Plater::get_export_gcode_filename(const wxString & extension, bool only_filename, bool export_all) const +{ + return p->get_export_gcode_filename(extension, only_filename, export_all); +} -void Plater::set_project_filename(const wxString& filename) { p->set_project_filename(filename); } +void Plater::set_project_filename(const wxString& filename) +{ + p->set_project_filename(filename); +} -bool Plater::is_export_gcode_scheduled() const { return p->background_process.is_export_scheduled(); } +bool Plater::is_export_gcode_scheduled() const +{ + return p->background_process.is_export_scheduled(); +} -const Selection& Plater::get_selection() const { return p->get_selection(); } +const Selection &Plater::get_selection() const +{ + return p->get_selection(); +} -int Plater::get_selected_object_idx() { return p->get_selected_object_idx(); } +int Plater::get_selected_object_idx() +{ + return p->get_selected_object_idx(); +} -bool Plater::is_single_full_object_selection() const { return p->get_selection().is_single_full_object(); } +bool Plater::is_single_full_object_selection() const +{ + return p->get_selection().is_single_full_object(); +} GLCanvas3D* Plater::canvas3D() { @@ -20515,9 +20529,15 @@ const GLCanvas3D* Plater::canvas3D() const return p->get_current_canvas3D(); } -GLCanvas3D* Plater::get_view3D_canvas3D() { return p ? p->view3D->get_canvas3d() : nullptr; } +GLCanvas3D* Plater::get_view3D_canvas3D() +{ + return p ? p->view3D->get_canvas3d() : nullptr; +} -GLCanvas3D* Plater::get_preview_canvas3D() { return p->preview->get_canvas3d(); } +GLCanvas3D* Plater::get_preview_canvas3D() +{ + return p->preview->get_canvas3d(); +} GLCanvas3D* Plater::get_assmeble_canvas3D() { @@ -20526,36 +20546,51 @@ GLCanvas3D* Plater::get_assmeble_canvas3D() return nullptr; } -GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) { return p->get_current_canvas3D(exclude_preview); } +GLCanvas3D* Plater::get_current_canvas3D(bool exclude_preview) +{ + return p->get_current_canvas3D(exclude_preview); +} void Plater::arrange() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Arrange")); replace_job(w, std::make_unique()); } } -void Plater::set_current_canvas_as_dirty() { p->set_current_canvas_as_dirty(); } +void Plater::set_current_canvas_as_dirty() +{ + p->set_current_canvas_as_dirty(); +} -void Plater::unbind_canvas_event_handlers() { p->unbind_canvas_event_handlers(); } +void Plater::unbind_canvas_event_handlers() +{ + p->unbind_canvas_event_handlers(); +} -void Plater::reset_canvas_volumes() { p->reset_canvas_volumes(); } +void Plater::reset_canvas_volumes() +{ + p->reset_canvas_volumes(); +} -PrinterTechnology Plater::printer_technology() const { return p->printer_technology; } +PrinterTechnology Plater::printer_technology() const +{ + return p->printer_technology; +} -const DynamicPrintConfig* Plater::config() const { return p->config; } +const DynamicPrintConfig * Plater::config() const { return p->config; } bool Plater::set_printer_technology(PrinterTechnology printer_technology) { p->printer_technology = printer_technology; - bool ret = p->background_process.select_technology(printer_technology); + bool ret = p->background_process.select_technology(printer_technology); if (ret) { // Update the active presets. } - // FIXME for SLA synchronize - // p->background_process.apply(Model)! + //FIXME for SLA synchronize + //p->background_process.apply(Model)! if (printer_technology == ptSLA) { for (ModelObject* model_object : p->model.objects) { @@ -20564,7 +20599,7 @@ bool Plater::set_printer_technology(PrinterTechnology printer_technology) } p->label_btn_export = printer_technology == ptFFF ? L("Export G-code") : L("Export"); - p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); + p->label_btn_send = printer_technology == ptFFF ? L("Send G-code") : L("Send to printer"); if (wxGetApp().mainframe != nullptr) wxGetApp().mainframe->update_menubar(); @@ -20585,8 +20620,7 @@ void Plater::clear_before_change_mesh(int obj_idx) // may be different and they would make no sense. bool paint_removed = false; for (ModelVolume* mv : mo->volumes) { - paint_removed |= !mv->supported_facets.empty() || !mv->seam_facets.empty() || !mv->mmu_segmentation_facets.empty() || - !mv->fuzzy_skin_facets.empty(); + paint_removed |= ! mv->supported_facets.empty() || ! mv->seam_facets.empty() || ! mv->mmu_segmentation_facets.empty() || !mv->fuzzy_skin_facets.empty(); mv->supported_facets.reset(); mv->seam_facets.reset(); mv->mmu_segmentation_facets.reset(); @@ -20594,9 +20628,10 @@ void Plater::clear_before_change_mesh(int obj_idx) } if (paint_removed) { // snapshot_time is captured by copy so the lambda knows where to undo/redo to. - get_notification_manager()->push_notification(NotificationType::CustomSupportsAndSeamRemovedAfterRepair, - NotificationManager::NotificationLevel::PrintInfoNotificationLevel, - _u8L("Custom supports and color painting were removed before repairing.")); + get_notification_manager()->push_notification( + NotificationType::CustomSupportsAndSeamRemovedAfterRepair, + NotificationManager::NotificationLevel::PrintInfoNotificationLevel, + _u8L("Custom supports and color painting were removed before repairing.")); } } @@ -20609,8 +20644,7 @@ void Plater::changed_mesh(int obj_idx) p->schedule_background_process(); } -void Plater::changed_object(ModelObject& object) -{ +void Plater::changed_object(ModelObject &object){ assert(object.get_model() == &p->model); // is object from same model? object.invalidate_bounding_box(); @@ -20626,7 +20660,7 @@ void Plater::changed_object(ModelObject& object) // update print p->schedule_background_process(); - + // Check outside bed get_current_canvas3D()->requires_check_outside_state(); } @@ -20635,7 +20669,7 @@ void Plater::changed_object(int obj_idx) { if (obj_idx < 0) return; - ModelObject* object = p->model.objects[obj_idx]; + ModelObject *object = p->model.objects[obj_idx]; if (object == nullptr) return; changed_object(*object); @@ -20657,7 +20691,8 @@ void Plater::changed_objects(const std::vector& object_idxs) // Update the SLAPrint from the current Model, so that the reload_scene() // pulls the correct data, update the 3D scene. this->p->update_restart_background_process(true, false); - } else { + } + else { p->view3D->reload_scene(false); p->view3D->get_canvas3d()->update_instance_printable_state_for_objects(object_idxs); } @@ -20666,7 +20701,7 @@ void Plater::changed_objects(const std::vector& object_idxs) this->p->schedule_background_process(); } -void Plater::schedule_background_process(bool schedule /* = true*/) +void Plater::schedule_background_process(bool schedule/* = true*/) { if (schedule) this->p->schedule_background_process(); @@ -20674,7 +20709,10 @@ void Plater::schedule_background_process(bool schedule /* = true*/) this->p->suppressed_backround_processing_update = false; } -bool Plater::is_background_process_update_scheduled() const { return this->p->background_process_timer.IsRunning(); } +bool Plater::is_background_process_update_scheduled() const +{ + return this->p->background_process_timer.IsRunning(); +} void Plater::suppress_background_process(const bool stop_background_process) { @@ -20687,29 +20725,28 @@ void Plater::suppress_background_process(const bool stop_background_process) // Expose the slicing process to the device GUI. BackgroundSlicingProcess& Plater::background_process() { return p->background_process; } -void Plater::center_selection() { p->center_selection(); } -void Plater::drop_selection() { p->drop_selection(); } -void Plater::mirror(Axis axis) { p->mirror(axis); } -void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } -void Plater::split_volume() { p->split_volume(); } +void Plater::center_selection() { p->center_selection(); } +void Plater::drop_selection() { p->drop_selection(); } +void Plater::mirror(Axis axis) { p->mirror(axis); } +void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); } +void Plater::split_volume() { p->split_volume(); } void Plater::optimize_rotation() { - auto& w = get_ui_job_worker(); + auto &w = get_ui_job_worker(); if (w.is_idle()) { p->take_snapshot(_u8L("Optimize Rotation")); replace_job(w, std::make_unique()); } } -void Plater::update_menus() { p->menus.update(); } +void Plater::update_menus() { p->menus.update(); } -wxString Plater::get_selected_printer_name_in_combox() -{ - PresetBundle* preset_bundle = wxGetApp().preset_bundle; - std::string printer_model = preset_bundle->printers.get_selected_preset().config.option("printer_model")->value; +wxString Plater::get_selected_printer_name_in_combox() { + PresetBundle * preset_bundle = wxGetApp().preset_bundle; + std::string printer_model = preset_bundle->printers.get_selected_preset().config.option("printer_model")->value; return printer_model; } -void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString& title) +void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWarningType type, const wxString &title) { printer_name.Replace("Bambu Lab", "", false); wxString content; @@ -20723,16 +20760,14 @@ void Plater::pop_warning_and_go_to_device_page(wxString printer_name, PrinterWar _L("OrcaSlicer can't connect to %s. Please check if the printer is powered on and connected to the network."), printer_name); } } else if (type == PrinterWarningType::INCONSISTENT) { - content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), - printer_name, printer_name); + content = wxString::Format(_L("The currently connected printer on the device page is not %s. Please switch to %s before syncing."), printer_name, printer_name); } else if (type == PrinterWarningType::UNINSTALL_FILAMENT) { content = _L("There are no filaments on the printer. Please load the filaments on the printer first."); } else if (type == PrinterWarningType::EMPTY_FILAMENT) { - content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set " - "the filament type."); + content = _L("The filaments on the printer are all unknown types. Please go to the printer screen or software device page to set the filament type."); } MessageDialog dlg(this, content, title, wxOK | wxFORWARD | wxICON_WARNING, _L("Device Page")); - auto result = dlg.ShowModal(); + auto result = dlg.ShowModal(); if (result == wxFORWARD) { wxGetApp().mainframe->select_tab(TAB_ID_MONITOR); } @@ -20743,13 +20778,13 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) if (!wxGetApp().getDeviceManager()) { return false; } - MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); if (obj == nullptr) { return false; } if (!check_printer_initialized(obj, true, popup_warning)) return false; - Preset* machine_preset = get_printer_preset(obj); + Preset * machine_preset = get_printer_preset(obj); if (!machine_preset) return false; @@ -20763,7 +20798,7 @@ bool Plater::is_same_printer_for_connected_and_selected(bool popup_warning) return true; } // BBS -// void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } +//void Plater::show_action_buttons(const bool ready_to_slice) const { p->show_action_buttons(ready_to_slice); } void Plater::fill_color(int extruder_id) { @@ -20772,7 +20807,7 @@ void Plater::fill_color(int extruder_id) } } -// BBS +//BBS void Plater::cut_selection_to_clipboard() { Plater::TakeSnapshot snapshot(this, "Cut Selected Objects"); @@ -20804,7 +20839,7 @@ void Plater::paste_from_clipboard() p->view3D->get_canvas3d()->get_selection().paste_from_clipboard(); } -// BBS: add clone +//BBS: add clone void Plater::clone_selection() { if (is_selection_empty()) @@ -20815,13 +20850,13 @@ void Plater::clone_selection() std::vector Plater::get_empty_cells(const Vec2f step) { - PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); BoundingBoxf3 build_volume = plate->get_build_volume(true); Vec2d vmin(build_volume.min.x(), build_volume.min.y()), vmax(build_volume.max.x(), build_volume.max.y()); BoundingBoxf bbox(vmin, vmax); std::vector cells; - auto min_x = step(0) / 2; // start_point.x() - step(0) * int((start_point.x() - bbox.min.x()) / step(0)); - auto min_y = step(1) / 2; // start_point.y() - step(1) * int((start_point.y() - bbox.min.y()) / step(1)); + auto min_x = step(0)/2;// start_point.x() - step(0) * int((start_point.x() - bbox.min.x()) / step(0)); + auto min_y = step(1)/2;// start_point.y() - step(1) * int((start_point.y() - bbox.min.y()) / step(1)); auto& exclude_box3s = plate->get_exclude_areas(); std::vector exclude_boxs; for (auto& box : exclude_box3s) { @@ -20838,14 +20873,14 @@ std::vector Plater::get_empty_cells(const Vec2f step) break; } } - if (in_exclude) + if(in_exclude) continue; cells.emplace_back(x, y); } return cells; } -void Plater::search(bool plater_is_active, Preset::Type type, wxWindow* tag, TextInput* etag, wxWindow* stag) +void Plater::search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag) { if (plater_is_active) { if (is_preview_shown()) @@ -20856,12 +20891,13 @@ void Plater::search(bool plater_is_active, Preset::Type type, wxWindow* tag, Tex wxKeyEvent evt; #ifdef __APPLE__ evt.m_keyCode = 'f'; -#else /* __APPLE__ */ +#else /* __APPLE__ */ evt.m_keyCode = WXK_CONTROL_F; #endif /* __APPLE__ */ evt.SetControlDown(true); canvas3D()->on_char(evt); - } else + } + else p->sidebar->get_searcher().show_dialog(type, tag, etag, stag); } @@ -20884,8 +20920,7 @@ void Plater::sys_color_changed() p->preview->sys_color_changed(); p->sidebar->sys_color_changed(); p->menus.sys_color_changed(); - if (p->m_select_machine_dlg) - p->m_select_machine_dlg->sys_color_changed(); + if (p->m_select_machine_dlg) p->m_select_machine_dlg->sys_color_changed(); Layout(); GetParent()->Layout(); @@ -20904,48 +20939,60 @@ void Plater::enable_view_toolbar(bool enable) } #endif -bool Plater::init_collapse_toolbar() { return p->init_collapse_toolbar(); } +bool Plater::init_collapse_toolbar() +{ + return p->init_collapse_toolbar(); +} -const Camera& Plater::get_camera() const { return p->camera; } +const Camera& Plater::get_camera() const +{ + return p->camera; +} -Camera& Plater::get_camera() { return p->camera; } +Camera& Plater::get_camera() +{ + return p->camera; +} -// BBS: partplate list related functions -PartPlateList& Plater::get_partplate_list() { return p->partplate_list; } +//BBS: partplate list related functions +PartPlateList& Plater::get_partplate_list() +{ + return p->partplate_list; +} void Plater::apply_background_progress() { - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - int plate_index = p->partplate_list.get_curr_plate_index(); - bool result_valid = part_plate->is_slice_result_valid(); + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + int plate_index = p->partplate_list.get_curr_plate_index(); + bool result_valid = part_plate->is_slice_result_valid(); const auto& preset_bundle = wxGetApp().preset_bundle; - // always apply the current plate's print + //always apply the current plate's print Print::ApplyStatus invalidated; if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } else + } + else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % - plate_index % invalidated % result_valid; - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % plate_index % invalidated % result_valid; + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { part_plate->update_slice_result_valid_state(false); - // p->ready_to_slice = true; + //p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); } } -// BBS: select Plate +//BBS: select Plate int Plater::select_plate(int plate_index, bool need_slice) { int ret; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, need_slice %3% ") % __LINE__ % plate_index % need_slice; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, need_slice %3% ")%__LINE__ %plate_index %need_slice; take_snapshot("select partplate!"); ret = p->partplate_list.select_plate(plate_index); if (!ret) { @@ -20954,106 +21001,124 @@ int Plater::select_plate(int plate_index, bool need_slice) } const auto& preset_bundle = wxGetApp().preset_bundle; - if ((!ret) && (p->background_process.can_switch_print())) { - // select successfully + if ((!ret) && (p->background_process.can_switch_print())) + { + //select successfully p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->update_print_volume_state(); - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - bool result_valid = part_plate->is_slice_result_valid(); - PrintBase* print = nullptr; + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; GCodeResult* gcode_result = nullptr; Print::ApplyStatus invalidated; part_plate->get_print(&print, &gcode_result, NULL); - // always apply the current plate's print + //always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } else + } + else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); bool model_fits, validate_err; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ") % __LINE__ % - plate_index % invalidated % result_valid; - if (result_valid) { - if (is_preview_shown()) { - if (need_slice) { // from preview's thumbnail - if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) || (gcode_result->moves.empty())) { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, after apply, invalidated= %3%, previous result_valid %4% ")%__LINE__ %plate_index %invalidated %result_valid; + if (result_valid) + { + if (is_preview_shown()) + { + if (need_slice) { //from preview's thumbnail + if ((invalidated & PrintBase::APPLY_STATUS_INVALIDATED) || (gcode_result->moves.empty())){ if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) part_plate->update_slice_result_valid_state(false); p->process_completed_with_error = -1; - p->m_slice_all = false; + p->m_slice_all = false; reset_gcode_toolpaths(); reslice(); - } else { + } + else { validate_current_plate(model_fits, validate_err); - // just refresh_print + //just refresh_print reload_print(); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false, true); } - } else { // from multiple slice's next - // do nothing } - } else { + else {// from multiple slice's next + //do nothing + } + } + else + { validate_current_plate(model_fits, validate_err); - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { part_plate->update_slice_result_valid_state(false); // BBS - // p->show_action_buttons(true); - // p->ready_to_slice = true; + //p->show_action_buttons(true); + //p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } else { + } + else + { // BBS - // p->show_action_buttons(false); - // p->ready_to_slice = false; + //p->show_action_buttons(false); + //p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } } - } else { - // check inside status - // model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; - // bool validate_err = false; + } + else + { + //check inside status + //model_fits = p->view3D->get_canvas3d()->check_volumes_outside_state() != ModelInstancePVS_Partly_Outside; + //bool validate_err = false; validate_current_plate(model_fits, validate_err); if (model_fits && !validate_err) { p->process_completed_with_error = -1; - } else { + } + else { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); } - if (is_preview_shown()) { - if (need_slice) { - // p->process_completed_with_error = -1; + if (is_preview_shown()) + { + if (need_slice) + { + //p->process_completed_with_error = -1; p->m_slice_all = false; reset_gcode_toolpaths(); if (model_fits && !validate_err) { - if (!check_ams_status(false)) { + if (!check_ams_status(false)){ return ret; } reslice(); - } else { + } + else { p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); - // sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary - // then the print toolpath will be shown, so we should not refresh print here, only onload shell - // refresh_print(); + //sometimes the previous print's sliced result is still valid, but the newly added object is laid over the boundary + //then the print toolpath will be shown, so we should not refresh print here, only onload shell + //refresh_print(); p->update_fff_scene_only_shells(); } - } else { - // p->ready_to_slice = false; + } + else { + //p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } - } else { - // validate_current_plate(model_fits, validate_err); - // check inside status + } + else + { + //validate_current_plate(model_fits, validate_err); + //check inside status /*if (model_fits && !validate_err){ p->process_completed_with_error = -1; } @@ -21062,13 +21127,16 @@ int Plater::select_plate(int plate_index, bool need_slice) }*/ // BBS: don't show action buttons - // p->show_action_buttons(true); - // p->ready_to_slice = true; - if (model_fits && part_plate->has_printable_instances()) { - // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + //p->show_action_buttons(true); + //p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } else { - // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + } + else + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); } } @@ -21078,7 +21146,7 @@ int Plater::select_plate(int plate_index, bool need_slice) SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%") % __LINE__ % plate_index % ret; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: plate %2%, return %3%")%__LINE__ %plate_index %ret; return ret; } @@ -21089,12 +21157,13 @@ int Plater::select_sliced_plate(int plate_index, bool skip_zoom) Freeze(); ret = select_plate(plate_index, true); - if (ret) { + if (ret) + { BOOST_LOG_TRIVIAL(error) << "select_plate error for plate_idx=" << plate_index; Thaw(); return -1; } - if (skip_zoom) + if(skip_zoom) p->partplate_list.select_plate_view(); Thaw(); @@ -21110,13 +21179,11 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) { ObjectFilamentResults object_results; ModelInstanceEPrintVolumeState state = p->view3D->get_canvas3d()->check_volumes_outside_state(&object_results); - model_fits = (state != ModelInstancePVS_Partly_Outside); + model_fits = (state != ModelInstancePVS_Partly_Outside); - PartPlate* cur_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); - if (model_fits) { // TPU check - bool tpu_valid = cur_plate->check_tpu_printable_status(wxGetApp().preset_bundle->full_config(), - wxGetApp().preset_bundle->get_used_tpu_filaments( - cur_plate->get_extruders(true))); + PartPlate *cur_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); + if (model_fits) { // TPU check + bool tpu_valid = cur_plate->check_tpu_printable_status(wxGetApp().preset_bundle->full_config(), wxGetApp().preset_bundle->get_used_tpu_filaments(cur_plate->get_extruders(true))); model_fits &= tpu_valid; } @@ -21126,23 +21193,19 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) model_fits &= filament_printable; } - model_fits = model_fits && object_results.filaments.empty(); + model_fits = model_fits && object_results.filaments.empty(); validate_error = false; if (p->printer_technology == ptFFF) { - // std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" - // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the - // build volume.");; + //std::string plater_text = _u8L("An object is laid over the boundary of plate or exceeds the height limit.\n" + // "Please solve the problem by moving it totally on or off the plate, and confirming that the height is within the build volume.");; std::vector warnings; Polygons polygons; std::vector> height_polygons; - p->background_process.fff_print()->set_check_multi_filaments_compatibility( - wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); + p->background_process.fff_print()->set_check_multi_filaments_compatibility(wxGetApp().app_config->get("enable_high_low_temp_mixed_printing") == "false"); StringObjectException err = p->background_process.validate(&warnings, &polygons, &height_polygons); // update string by type post_process_string_object_exception(err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%") % err.string % warnings.size() % - model_fits; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": validate err=%1%, warnings=%2%, model_fits %3%")%err.string%warnings.size() %model_fits; if (err.string.empty()) { p->partplate_list.get_curr_plate()->update_apply_result_invalid(false); @@ -21154,13 +21217,14 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) p->view3D->get_canvas3d()->reset_sequential_print_clearance(); p->view3D->get_canvas3d()->set_as_dirty(); p->view3D->get_canvas3d()->request_extra_frame(); - } else { + } + else { // The print is not valid. p->partplate_list.get_curr_plate()->update_apply_result_invalid(true); // Show error as notification. p->notification_manager->push_validate_error_notification(err); p->process_validation_warnings(warnings); - // model_fits = false; + //model_fits = false; validate_error = true; p->view3D->get_canvas3d()->set_sequential_print_clearance_visible(true); p->view3D->get_canvas3d()->set_sequential_print_clearance_render_fill(true); @@ -21170,25 +21234,25 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error) std::string clashed_text = get_object_clashed_text(); if (state == ModelInstancePVS_Partly_Outside) { p->notification_manager->push_plater_error_notification(clashed_text); - } else { + } + else { p->notification_manager->close_plater_error_notification(clashed_text); } - std::string left_unprintable_text = get_left_extruder_unprintable_text(), - right_unprintable_text = get_right_extruder_unprintable_text(); - if (!left_unprintable_text.empty()) { - p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError, - left_unprintable_text); - } else { - p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice( - NotificationType::LeftExtruderUnprintableError); + std::string left_unprintable_text = get_left_extruder_unprintable_text(), right_unprintable_text = get_right_extruder_unprintable_text(); + if (!left_unprintable_text.empty()) + { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError, left_unprintable_text); + } + else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::LeftExtruderUnprintableError); } - if (!right_unprintable_text.empty()) { - p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError, - right_unprintable_text); - } else { - p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice( - NotificationType::RightExtruderUnprintableError); + if (!right_unprintable_text.empty()) + { + p->notification_manager->bbl_show_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError,right_unprintable_text); + } + else { + p->notification_manager->bbl_close_filament_map_invalid_notification_before_slice(NotificationType::RightExtruderUnprintableError); } /*if (state == ModelInstancePVS_Limited) { @@ -21246,18 +21310,16 @@ bool Plater::refresh_missing_plugin_block(bool* block_toggled) std::vector refs = missing_refs(missing); std::sort(refs.begin(), refs.end()); std::string sig; - for (const std::string& r : refs) { - sig += r; - sig += '\n'; - } + for (const std::string& r : refs) { sig += r; sig += '\n'; } return sig; }; // Show/refresh the non-closable notification for one missing set. Only (re)create it when the // set changes; pushing every validate would close+recreate it (flicker, reset hover) since // validate runs on many triggers. shown_sig also gates plugin-load re-validation. - const auto update = [&](NotificationType type, const std::vector& missing, std::string* shown_sig, - const std::string& header, const std::string& resolve_label, + const auto update = [&](NotificationType type, const std::vector& missing, + std::string* shown_sig, const std::string& header, + const std::string& resolve_label, std::function resolve_action) { if (missing.empty()) { if (!shown_sig->empty()) { @@ -21272,49 +21334,46 @@ bool Plater::refresh_missing_plugin_block(bool* block_toggled) for (const auto& m : missing) body.emplace_back(JumpTo{m.ref.capability_name, m.opt, m.opt_type}); - p->notification_manager->push_plugin_missing_notification(type, header, resolve_label, std::move(body), - std::move(resolve_action)); + p->notification_manager->push_plugin_missing_notification( + type, header, resolve_label, std::move(body), std::move(resolve_action)); *shown_sig = sig; } }; - const std::vector missing_cloud = get_missing_cloud_plugins(); - const std::vector missing_local = get_missing_local_plugins(); + const std::vector missing_cloud = get_missing_cloud_plugins(); + const std::vector missing_local = get_missing_local_plugins(); const std::vector missing_cloud_refs = missing_refs(missing_cloud); const std::vector missing_local_refs = missing_refs(missing_local); - update(NotificationType::OrcaCloudPluginMissingError, missing_cloud, &p->m_cloud_missing_shown_sig, - _u8L("OrcaCloud plugins required by the current preset are not installed:"), _u8L("Install Plugins"), - [this, missing_cloud_refs](wxEvtHandler*) { - install_missing_cloud_plugins(missing_cloud_refs); - return false; - }); + update(NotificationType::OrcaCloudPluginMissingError, missing_cloud, + &p->m_cloud_missing_shown_sig, + _u8L("OrcaCloud plugins required by the current preset are not installed:"), + _u8L("Install Plugins"), + [this, missing_cloud_refs](wxEvtHandler*) { install_missing_cloud_plugins(missing_cloud_refs); return false; }); // "Find on OrcaCloud" is only a suggestion: it opens the browser but cannot resolve the missing // plugin in-session, so it never closes the notification or unblocks slicing. The user resolves a // local plugin by installing it or by changing the setting that needs it. - update(NotificationType::OrcaLocalPluginMissingError, missing_local, &p->m_local_missing_shown_sig, - _u8L("Local plugins required by the current preset are missing:"), _u8L("Find on OrcaCloud"), - [missing_local_refs](wxEvtHandler*) { - open_missing_plugins_on_cloud(missing_local_refs); - return false; - }); + update(NotificationType::OrcaLocalPluginMissingError, missing_local, + &p->m_local_missing_shown_sig, + _u8L("Local plugins required by the current preset are missing:"), + _u8L("Find on OrcaCloud"), + [missing_local_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(missing_local_refs); return false; }); - const std::vector inactive = get_inactive_plugins(); - const std::vector broken = get_broken_plugins(); - const std::vector inactive_refs = missing_refs(inactive); - const std::vector broken_refs = missing_refs(broken); + const std::vector inactive = get_inactive_plugins(); + const std::vector broken = get_broken_plugins(); + const std::vector inactive_refs = missing_refs(inactive); + const std::vector broken_refs = missing_refs(broken); - update(NotificationType::OrcaPluginInactiveError, inactive, &p->m_inactive_shown_sig, - _u8L("Plugins required by the current preset are not activated:"), _u8L("Activate Now"), [this, inactive_refs](wxEvtHandler*) { - enable_inactive_plugins(inactive_refs); - return false; - }); - update(NotificationType::OrcaPluginCapabilityUnavailableError, broken, &p->m_broken_shown_sig, - _u8L("The installed plugin does not provide the required capability — it may be outdated:"), _u8L("Find on OrcaCloud"), - [broken_refs](wxEvtHandler*) { - open_missing_plugins_on_cloud(broken_refs); - return false; - }); + update(NotificationType::OrcaPluginInactiveError, inactive, + &p->m_inactive_shown_sig, + _u8L("Plugins required by the current preset are not activated:"), + _u8L("Activate Now"), + [this, inactive_refs](wxEvtHandler*) { enable_inactive_plugins(inactive_refs); return false; }); + update(NotificationType::OrcaPluginCapabilityUnavailableError, broken, + &p->m_broken_shown_sig, + _u8L("The installed plugin does not provide the required capability — it may be outdated:"), + _u8L("Find on OrcaCloud"), + [broken_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(broken_refs); return false; }); const bool blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins(); if (block_toggled) @@ -21326,8 +21385,8 @@ void Plater::revalidate_current_plate_if_plugins_missing() { // Only do work while a missing-plugin notification is up, so the plugin-load hook does not // trigger a full validation for every plugin that loads during normal startup/use. - if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() && p->m_inactive_shown_sig.empty() && - p->m_broken_shown_sig.empty()) + if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() && + p->m_inactive_shown_sig.empty() && p->m_broken_shown_sig.empty()) return; bool model_fits = true, validate_error = false; validate_current_plate(model_fits, validate_error); @@ -21344,14 +21403,15 @@ void Plater::install_missing_cloud_plugins(const std::vector& cloud std::atomic cancel{false}; std::atomic finished{false}; std::atomic torn_down{false}; - std::mutex mtx; - std::string message; + std::mutex mtx; + std::string message; }; auto state = std::make_shared(); state->message = _u8L("Preparing to install plugins..."); wxWindow* parent = wxGetApp().mainframe; - auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100, parent, wxPD_APP_MODAL | wxPD_CAN_ABORT); + auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100, + parent, wxPD_APP_MODAL | wxPD_CAN_ABORT); dialog->Pulse(); // UI-thread timer: animate the pulse, observe the Cancel button, and tear down when the worker @@ -21409,8 +21469,7 @@ bool Plater::plugins_block_slicing() const return has_missing_plugins() || has_inactive_plugins() || has_broken_plugins(); } -void Plater::open_platesettings_dialog(wxCommandEvent& evt) -{ +void Plater::open_platesettings_dialog(wxCommandEvent& evt) { int plate_index = evt.GetInt(); PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence"); PartPlate* curr_plate = p->partplate_list.get_curr_plate(); @@ -21419,7 +21478,8 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) auto curr_print_seq = curr_plate->get_print_seq(); if (curr_print_seq != PrintSequence::ByDefault) { dlg.sync_print_seq(int(curr_print_seq) + 1); - } else + } + else dlg.sync_print_seq(0); auto first_layer_print_seq = curr_plate->get_first_layer_print_sequence(); @@ -21439,8 +21499,8 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) dlg.Bind(EVT_SET_BED_TYPE_CONFIRM, [this, plate_index, &dlg](wxCommandEvent& e) { PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - BedType old_bed_type = curr_plate->get_bed_type(); - auto bt_sel = dlg.get_bed_type_choice(); + BedType old_bed_type = curr_plate->get_bed_type(); + auto bt_sel = dlg.get_bed_type_choice(); if (old_bed_type != bt_sel) { curr_plate->set_bed_type(bt_sel); update_project_dirty_from_presets(); @@ -21467,54 +21527,63 @@ void Plater::open_platesettings_dialog(wxCommandEvent& evt) int spiral_sel = dlg.get_spiral_mode_choice(); if (spiral_sel == 1) { curr_plate->set_spiral_vase_mode(true, false); - } else if (spiral_sel == 2) { + } + else if (spiral_sel == 2) { curr_plate->set_spiral_vase_mode(false, false); - } else { + } + else { curr_plate->set_spiral_vase_mode(false, true); } update_project_dirty_from_presets(); set_plater_dirty(true); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format("select print sequence %1% for plate %2% at plate side") % ps_sel % plate_index; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("select print sequence %1% for plate %2% at plate side") % ps_sel % plate_index; auto plate_config = *(curr_plate->config()); wxGetApp().plater()->config_change_notification(plate_config, std::string("print_sequence")); update(); wxGetApp().obj_list()->update_selections(); - }); + }); dlg.set_plate_name(from_u8(curr_plate->get_plate_name())); dlg.ShowModal(); curr_plate->set_plate_name(dlg.get_plate_name().ToUTF8().data()); } -void Plater::open_filament_map_setting_dialog(wxCommandEvent& evt) +void Plater::open_filament_map_setting_dialog(wxCommandEvent &evt) { PartPlate* curr_plate = p->partplate_list.get_curr_plate(); - int value = evt.GetInt(); // 1 means from gcode view - bool need_slice = value == 1; // If from gcode view, should slice + int value = evt.GetInt(); //1 means from gcode view + bool need_slice = value ==1; // If from gcode view, should slice const auto& project_config = wxGetApp().preset_bundle->project_config; - auto filament_colors = config()->option("filament_colour")->values; - auto filament_types = config()->option("filament_type")->values; + auto filament_colors = config()->option("filament_colour")->values; + auto filament_types = config()->option("filament_type")->values; - auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); - auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); + auto plate_filament_maps = curr_plate->get_real_filament_maps(project_config); + auto plate_filament_map_mode = curr_plate->get_filament_map_mode(); auto plate_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); - if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config + if (plate_filament_maps.size() != filament_colors.size()) // refine it later, save filament map to app config plate_filament_maps.resize(filament_colors.size(), 1); if (plate_filament_volume_maps.size() != filament_colors.size()) plate_filament_volume_maps.resize(filament_colors.size(), 0); - FilamentMapDialog filament_dlg(this, filament_colors, filament_types, plate_filament_maps, plate_filament_volume_maps, - curr_plate->get_extruders(true), plate_filament_map_mode, this->get_machine_sync_status(), false); + FilamentMapDialog filament_dlg(this, + filament_colors, + filament_types, + plate_filament_maps, + plate_filament_volume_maps, + curr_plate->get_extruders(true), + plate_filament_map_mode, + this->get_machine_sync_status(), + false + ); if (filament_dlg.ShowModal() == wxID_OK) { std::vector new_filament_maps = filament_dlg.get_filament_maps(); std::vector old_filament_maps = curr_plate->get_real_filament_maps(project_config); - FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); - FilamentMapMode new_map_mode = filament_dlg.get_mode(); + FilamentMapMode old_map_mode = curr_plate->get_filament_map_mode(); + FilamentMapMode new_map_mode = filament_dlg.get_mode(); std::vector new_filament_volume_maps = filament_dlg.get_filament_volume_maps(); std::vector old_filament_volume_maps = curr_plate->get_real_filament_volume_maps(project_config); @@ -21523,18 +21592,20 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent& evt) curr_plate->set_filament_map_mode(new_map_mode); } - if (new_map_mode == fmmManual) { + if (new_map_mode == fmmManual){ curr_plate->set_filament_maps(new_filament_maps); curr_plate->set_filament_volume_maps(new_filament_volume_maps); } - bool need_invalidate = (old_map_mode != new_map_mode || old_filament_maps != new_filament_maps || + bool need_invalidate = (old_map_mode != new_map_mode || + old_filament_maps != new_filament_maps || old_filament_volume_maps != new_filament_volume_maps); if (need_invalidate) { if (need_slice) { wxPostEvent(this, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); - } else { + } + else { curr_plate->update_slice_result_valid_state(false); set_plater_dirty(true); update(); @@ -21544,7 +21615,8 @@ void Plater::open_filament_map_setting_dialog(wxCommandEvent& evt) return; } -// BBS: select Plate by hover_id + +//BBS: select Plate by hover_id int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModidyPlateName) { int ret; @@ -21553,121 +21625,145 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi plate_index = hover_id / PartPlate::GRABBER_COUNT; action = isModidyPlateName ? PartPlate::PLATE_NAME_HOVER_ID : hover_id % PartPlate::GRABBER_COUNT; - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": enter, hover_id %1%, plate_index %2%, action %3%") % hover_id % plate_index % action; - if (action == 0) { - // select plate + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": enter, hover_id %1%, plate_index %2%, action %3%")%hover_id % plate_index %action; + if (action == 0) + { + //select plate ret = p->partplate_list.select_plate(plate_index); if (!ret) { SimpleEvent event(EVT_GLCANVAS_PLATE_SELECT); p->on_plate_selected(event); } - if ((!ret) && (p->background_process.can_switch_print())) { - // select successfully + if ((!ret)&&(p->background_process.can_switch_print())) + { + //select successfully p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->update_print_volume_state(); - PartPlate* part_plate = p->partplate_list.get_curr_plate(); - bool result_valid = part_plate->is_slice_result_valid(); - PrintBase* print = nullptr; + PartPlate* part_plate = p->partplate_list.get_curr_plate(); + bool result_valid = part_plate->is_slice_result_valid(); + PrintBase* print = nullptr; GCodeResult* gcode_result = nullptr; Print::ApplyStatus invalidated; const auto& preset_bundle = wxGetApp().preset_bundle; part_plate->get_print(&print, &gcode_result, NULL); - // always apply the current plate's print + //always apply the current plate's print if (preset_bundle->get_printer_extruder_count() > 1) { - std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); + std::vector f_maps = part_plate->get_real_filament_maps(preset_bundle->project_config); std::vector f_volume_maps = part_plate->get_filament_volume_maps(); if (f_volume_maps.empty()) { f_volume_maps = preset_bundle->get_default_nozzle_volume_types_for_filaments(f_maps); } invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false, f_maps, f_volume_maps)); - } else + } + else invalidated = p->background_process.apply(this->model(), preset_bundle->full_config(false)); bool model_fits, validate_err; validate_current_plate(model_fits, validate_err); - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(" %1%: after apply, invalidated= %2%, previous result_valid %3% ") % __LINE__ % - invalidated % result_valid; - if (result_valid) { - if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) { - // bool model_fits, validate_err; - // validate_current_plate(model_fits, validate_err); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: after apply, invalidated= %2%, previous result_valid %3% ")%__LINE__ % invalidated %result_valid; + if (result_valid) + { + if (invalidated & PrintBase::APPLY_STATUS_INVALIDATED) + { + //bool model_fits, validate_err; + //validate_current_plate(model_fits, validate_err); part_plate->update_slice_result_valid_state(false); // BBS - // p->show_action_buttons(true); - // p->ready_to_slice = true; + //p->show_action_buttons(true); + //p->ready_to_slice = true; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } else { + } + else + { // BBS - // p->show_action_buttons(false); - // validate_current_plate(model_fits, validate_err); - // p->ready_to_slice = false; + //p->show_action_buttons(false); + //validate_current_plate(model_fits, validate_err); + //p->ready_to_slice = false; p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); reload_print(); } - } else { - // check inside status - if (model_fits && !validate_err) { + } + else + { + //check inside status + if (model_fits && !validate_err){ p->process_completed_with_error = -1; - } else { + } + else { p->process_completed_with_error = p->partplate_list.get_curr_plate_index(); } // BBS: don't show action buttons - // p->show_action_buttons(true); - // p->ready_to_slice = true; - if (model_fits && part_plate->has_printable_instances()) { - // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); + //p->show_action_buttons(true); + //p->ready_to_slice = true; + if (model_fits && part_plate->has_printable_instances()) + { + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, true)); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to true"); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, true); - } else { - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ - << boost::format(": will set can_slice to false, has_printable_instances %1%") % - part_plate->has_printable_instances(); - // p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); + } + else + { + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": will set can_slice to false, has_printable_instances %1%")%part_plate->has_printable_instances(); + //p->view3D->get_canvas3d()->post_event(Event(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, false)); p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false); } } } - } else if ((action == 1) && (!right_click)) { - // delete plate + } + else if ((action == 1)&&(!right_click)) + { + //delete plate ret = delete_plate(plate_index); - } else if ((action == 2) && (!right_click)) { - // arrange the plate - // take_snapshot("select_orient partplate"); + } + else if ((action == 2)&&(!right_click)) + { + //arrange the plate + //take_snapshot("select_orient partplate"); ret = select_plate(plate_index); - if (!ret) { + if (!ret) + { set_prepare_state(Job::PREPARE_STATE_MENU); orient(); - } else { + } + else + { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } else if ((action == 3) && (!right_click)) { - // arrange the plate - // take_snapshot("select_arrange partplate"); + } + else if ((action == 3)&&(!right_click)) + { + //arrange the plate + //take_snapshot("select_arrange partplate"); ret = select_plate(plate_index); - if (!ret) { + if (!ret) + { if (last_arrange_job_is_finished()) { set_prepare_state(Job::PREPARE_STATE_MENU); arrange(); } - } else { + } + else + { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } else if ((action == 4) && (!right_click)) { - // lock the plate + } + else if ((action == 4)&&(!right_click)) + { + //lock the plate take_snapshot("lock partplate"); ret = p->partplate_list.lock_plate(plate_index, !p->partplate_list.is_locked(plate_index)); - } else if ((action == 5) && (!right_click)) { + } + else if ((action == 5)&&(!right_click)) + { // set the plate type ret = select_plate(plate_index); if (!ret) { @@ -21681,10 +21777,11 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } else if ((action == PartPlate::PLATE_FILAMENT_MAP_ID) && (!right_click)) { + } + else if ((action == PartPlate::PLATE_FILAMENT_MAP_ID) && (!right_click)) { ret = select_plate(plate_index); if (!ret) { - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + PartPlate * curr_plate = p->partplate_list.get_curr_plate(); wxCommandEvent evt(EVT_OPEN_FILAMENT_MAP_SETTINGS_DIALOG); evt.SetInt(0); // 0 means not from gcodeviewer evt.SetEventObject(this); @@ -21693,17 +21790,18 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; ret = -1; } - } else if ((action == 6) && (!right_click)) { + } + else if ((action == 6) && (!right_click)) { // set the plate type ret = select_plate(plate_index); if (!ret) { PlateNameEditDialog dlg(this, wxID_ANY, _L("Edit Plate Name")); - PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + PartPlate * curr_plate = p->partplate_list.get_curr_plate(); wxString curr_plate_name = from_u8(curr_plate->get_plate_name()); dlg.set_plate_name(curr_plate_name); - int result = dlg.ShowModal(); + int result=dlg.ShowModal(); if (result == wxID_YES) { wxString dlg_plate_name = dlg.get_plate_name(); curr_plate->set_plate_name(dlg_plate_name.ToUTF8().data()); @@ -21715,7 +21813,7 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi } else if ((action == 7) && (!right_click)) { // move plate to the front take_snapshot("move plate to the front"); - ret = p->partplate_list.move_plate_to_index(plate_index, 0); + ret = p->partplate_list.move_plate_to_index(plate_index,0); p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->sidebar->obj_list()->reload_all_plates(); @@ -21724,12 +21822,13 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi p->partplate_list.select_plate(0); } - else { + else + { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; ret = -1; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: return %2%") % __LINE__ % ret; + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" %1%: return %2%")%__LINE__ % ret; return ret; } @@ -21741,12 +21840,12 @@ int Plater::duplicate_plate(int plate_index) ret = p->partplate_list.duplicate_plate(index); - // need to call update + //need to call update update(); return ret; } -// BBS: delete the plate, index= -1 means the current plate +//BBS: delete the plate, index= -1 means the current plate int Plater::delete_plate(int plate_index) { int index = plate_index, ret; @@ -21757,41 +21856,47 @@ int Plater::delete_plate(int plate_index) take_snapshot("delete partplate"); ret = p->partplate_list.delete_plate(index); - // BBS: update the current print to the current plate + //BBS: update the current print to the current plate p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); p->sidebar->obj_list()->reload_all_plates(); // BBS update default view - // get_camera().select_view("topfront"); - // get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; + //get_camera().select_view("topfront"); + //get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE; - // need to call update + //need to call update update(); return ret; } -// BBS: set bed positions -void Plater::set_bed_position(Vec2d& pos) { p->bed.set_position(pos); } +//BBS: set bed positions +void Plater::set_bed_position(Vec2d& pos) +{ + p->bed.set_position(pos); +} -// BBS: is the background process slicing currently -bool Plater::is_background_process_slicing() const { return p->m_is_slicing; } +//BBS: is the background process slicing currently +bool Plater::is_background_process_slicing() const +{ + return p->m_is_slicing; +} -// BBS: update slicing context +//BBS: update slicing context void Plater::update_slicing_context_to_current_partplate() { p->partplate_list.update_slice_context_to_current_plate(p->background_process); p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); } -// BBS: show object info +//BBS: show object info void Plater::show_object_info() { - NotificationManager* notify_manager = get_notification_manager(); - const Selection& selection = get_selection(); - int selCount = selection.get_volume_idxs().size(); - ModelObjectPtrs objects = model().objects; - int obj_idx = selection.get_object_idx(); + NotificationManager *notify_manager = get_notification_manager(); + const Selection& selection = get_selection(); + int selCount = selection.get_volume_idxs().size(); + ModelObjectPtrs objects = model().objects; + int obj_idx = selection.get_object_idx(); std::string info_text; if (selCount > 1 && !selection.is_single_full_object()) { @@ -21804,22 +21909,25 @@ void Plater::show_object_info() } notify_manager->bbl_show_objectsinfo_notification(info_text, false, !(p->current_panel == p->view3D)); return; - } else if (objects.empty() || (obj_idx < 0) || (obj_idx >= objects.size()) || - objects[obj_idx]->volumes.empty() || // hack to avoid crash when deleting the last object on the bed - (selection.is_single_full_object() && objects[obj_idx]->instances.size() > 1) || - !(selection.is_single_full_instance() || selection.is_single_volume())) { + } + else if (objects.empty() || (obj_idx < 0) || (obj_idx >= objects.size()) || + objects[obj_idx]->volumes.empty() ||// hack to avoid crash when deleting the last object on the bed + (selection.is_single_full_object() && objects[obj_idx]->instances.size()> 1) || + !(selection.is_single_full_instance() || selection.is_single_volume())) + { notify_manager->bbl_close_objectsinfo_notification(); return; } const ModelObject* model_object = objects[obj_idx]; - int inst_idx = selection.get_instance_idx(); - if ((inst_idx < 0) || (inst_idx >= model_object->instances.size())) { + int inst_idx = selection.get_instance_idx(); + if ((inst_idx < 0) || (inst_idx >= model_object->instances.size())) + { notify_manager->bbl_close_objectsinfo_notification(); return; } bool imperial_units = wxGetApp().app_config->get("use_inches") == "1"; - double koef = imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0f; + double koef = imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0f; ModelVolume* vol = nullptr; Transform3d t; @@ -21828,60 +21936,65 @@ void Plater::show_object_info() if (selection.is_single_volume()) { std::vector obj_idxs, vol_idxs; wxGetApp().obj_list()->get_selection_indexes(obj_idxs, vol_idxs); - if (vol_idxs.size() != 1) { - // corner case when merge/split/remove + if (vol_idxs.size() != 1) + { + //corner case when merge/split/remove return; } vol = model_object->volumes[vol_idxs[0]]; - t = model_object->instances[inst_idx]->get_matrix() * vol->get_matrix(); + t = model_object->instances[inst_idx]->get_matrix() * vol->get_matrix(); info_text += (boost::format(_utf8(L("Part name: %1%\n"))) % vol->name).str(); face_count = static_cast(vol->mesh().facets_count()); - size = vol->get_convex_hull().transformed_bounding_box(t).size(); - } else { - // int obj_idx, vol_idx; - // wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx); - // if (obj_idx < 0) { - // //corner case when merge/split/remove - // return; - // } + size = vol->get_convex_hull().transformed_bounding_box(t).size(); + } + else { + //int obj_idx, vol_idx; + //wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx); + //if (obj_idx < 0) { + // //corner case when merge/split/remove + // return; + //} info_text += (boost::format(_utf8(L("Object name: %1%\n"))) % model_object->name).str(); face_count = static_cast(model_object->facets_count()); - size = model_object->instance_convex_hull_bounding_box(inst_idx).size(); + size = model_object->instance_convex_hull_bounding_box(inst_idx).size(); } - // Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); + //Vec3d size = vol ? vol->mesh().transformed_bounding_box(t).size() : model_object->instance_bounding_box(inst_idx).size(); if (imperial_units) - info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% in\n"))) % (size(0) * koef) % (size(1) * koef) % (size(2) * koef)).str(); + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% in\n"))) %(size(0)*koef) %(size(1)*koef) %(size(2)*koef)).str(); else - info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% mm\n"))) % size(0) % size(1) % size(2)).str(); + info_text += (boost::format(_utf8(L("Size: %1% x %2% x %3% mm\n"))) %size(0) %size(1) %size(2)).str(); const TriangleMeshStats& stats = vol ? vol->mesh().stats() : model_object->get_object_stl_stats(); - double volume_val = stats.volume; + double volume_val = stats.volume; if (vol) volume_val *= std::fabs(t.matrix().block(0, 0, 3, 3).determinant()); - volume_val = volume_val * pow(koef, 3); + volume_val = volume_val * pow(koef,3); if (imperial_units) - info_text += (boost::format(_utf8(L("Volume: %1% in³\n"))) % volume_val).str(); + info_text += (boost::format(_utf8(L("Volume: %1% in³\n"))) %volume_val).str(); else - info_text += (boost::format(_utf8(L("Volume: %1% mm³\n"))) % volume_val).str(); - info_text += (boost::format(_utf8(L("Triangles: %1%\n"))) % face_count).str(); + info_text += (boost::format(_utf8(L("Volume: %1% mm³\n"))) %volume_val).str(); + info_text += (boost::format(_utf8(L("Triangles: %1%\n"))) %face_count).str(); wxString info_manifold; int non_manifold_edges = 0; - auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); + auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); - if (non_manifold_edges > 0) { - info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); - } + if (non_manifold_edges > 0) { + info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh."); + } info_manifold = "" + info_manifold + ""; info_text += into_u8(info_manifold); notify_manager->bbl_show_objectsinfo_notification(info_text, non_manifold_edges > 0, !(p->current_panel == p->view3D)); } -bool Plater::show_publish_dialog(bool show) { return p->show_publish_dlg(show); } +bool Plater::show_publish_dialog(bool show) +{ + return p->show_publish_dlg(show); +} -void Plater::post_process_string_object_exception(StringObjectException& err) +void Plater::post_process_string_object_exception(StringObjectException &err) { PresetBundle* preset_bundle = wxGetApp().preset_bundle; if (err.type == StringExceptionType::STRING_EXCEPT_FILAMENT_NOT_MATCH_BED_TYPE) { @@ -21904,9 +22017,8 @@ void Plater::post_process_string_object_exception(StringObjectException& err) break; } } - err.string = format(_L("Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this " - "print job, please set this filament\'s bed temperature to a number that is not zero."), - err.params[0], err.params[1], err.params[2], filament_name); + err.string = format(_L("Plate %d: %s is not suggested for use printing filament %s (%s). If you still want to do this print job, please set this filament\'s bed temperature to a number that is not zero."), + err.params[0], err.params[1], err.params[2], filament_name); err.string += "\n"; } } catch (...) { @@ -21917,8 +22029,10 @@ void Plater::post_process_string_object_exception(StringObjectException& err) return; } -void Plater::update_objects_position_when_select_preset(const std::function& select_prest) -{ p->update_objects_position_when_select_preset(select_prest); } +void Plater::update_objects_position_when_select_preset(const std::function &select_prest) +{ + p->update_objects_position_when_select_preset(select_prest); +} bool Plater::check_ams_status(bool is_slice_all) { @@ -21926,7 +22040,8 @@ bool Plater::check_ams_status(bool is_slice_all) if (!p->check_ams_status_impl(is_slice_all)) { m_check_status = 0; return false; - } else { + } + else { m_check_status = 1; } } @@ -21936,33 +22051,32 @@ bool Plater::check_ams_status(bool is_slice_all) void Plater::update_machine_sync_status() { - DeviceManager* dev_maneger = wxGetApp().getDeviceManager(); + DeviceManager *dev_maneger = wxGetApp().getDeviceManager(); if (!dev_maneger) { GUI::wxGetApp().sidebar().update_sync_status(nullptr); return; } - MachineObject* obj = wxGetApp().getDeviceManager()->get_selected_machine(); + MachineObject *obj = wxGetApp().getDeviceManager()->get_selected_machine(); GUI::wxGetApp().sidebar().update_sync_status(obj); } -bool Plater::get_machine_sync_status() { return p->get_machine_sync_status(); } +bool Plater::get_machine_sync_status() +{ + return p->get_machine_sync_status(); +} void Plater::update_filament_volume_map(int extruder_id, int volume_type) { // Hybrid is a per-extruder mix, not a per-filament volume; reset the affected filaments // to Standard so the manual grouping dialog starts from a concrete assignment. - int selected_volume_type = volume_type == static_cast(NozzleVolumeType::nvtHybrid) ? - static_cast(NozzleVolumeType::nvtStandard) : - volume_type; - auto& partplate_list = get_partplate_list(); + int selected_volume_type = volume_type == static_cast(NozzleVolumeType::nvtHybrid) ? static_cast(NozzleVolumeType::nvtStandard) : volume_type; + auto& partplate_list = get_partplate_list(); for (int idx = 0; idx < partplate_list.get_plate_count(); ++idx) { auto plate = partplate_list.get_plate(idx); - if (!plate) - continue; + if (!plate) continue; auto filament_map = plate->get_filament_maps(); auto filament_volume_map = plate->get_filament_volume_maps(); - if (filament_map.empty() || filament_volume_map.empty()) - continue; + if (filament_map.empty() || filament_volume_map.empty()) continue; if (filament_volume_map.size() < filament_map.size()) { filament_volume_map.resize(filament_map.size(), static_cast(NozzleVolumeType::nvtStandard)); } @@ -21982,10 +22096,16 @@ void Plater::init_environment_texture() p->environment_texture.load_from_file(resources_dir() + "/images/Pmetal_001.png", false, GLTexture::SingleThreaded, false); } -unsigned int Plater::get_environment_texture_id() const { return p->environment_texture.get_id(); } +unsigned int Plater::get_environment_texture_id() const +{ + return p->environment_texture.get_id(); +} #endif // ENABLE_ENVIRONMENT_MAP -const BuildVolume& Plater::build_volume() const { return p->bed.build_volume(); } +const BuildVolume& Plater::build_volume() const +{ + return p->bed.build_volume(); +} // BBS #if 0 @@ -22000,24 +22120,42 @@ GLToolbar& Plater::get_view_toolbar() } #endif -const GLToolbar& Plater::get_collapse_toolbar() const { return p->collapse_toolbar; } +const GLToolbar& Plater::get_collapse_toolbar() const +{ + return p->collapse_toolbar; +} -GLToolbar& Plater::get_collapse_toolbar() { return p->collapse_toolbar; } +GLToolbar& Plater::get_collapse_toolbar() +{ + return p->collapse_toolbar; +} -void Plater::update_preview_bottom_toolbar() { p->update_preview_bottom_toolbar(); } +void Plater::update_preview_bottom_toolbar() +{ + p->update_preview_bottom_toolbar(); +} void Plater::reset_gcode_toolpaths() { - // BBS: add some logs + //BBS: add some logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": reset the gcode viewer's toolpaths"); p->reset_gcode_toolpaths(); } -const Mouse3DController& Plater::get_mouse3d_controller() const { return p->mouse3d_controller; } +const Mouse3DController& Plater::get_mouse3d_controller() const +{ + return p->mouse3d_controller; +} -Mouse3DController& Plater::get_mouse3d_controller() { return p->mouse3d_controller; } +Mouse3DController& Plater::get_mouse3d_controller() +{ + return p->mouse3d_controller; +} -NotificationManager* Plater::get_notification_manager() { return p->notification_manager.get(); } +NotificationManager * Plater::get_notification_manager() +{ + return p->notification_manager.get(); +} DailyTipsWindow* Plater::get_dailytips() const { @@ -22025,11 +22163,20 @@ DailyTipsWindow* Plater::get_dailytips() const return dailytips_win; } -const NotificationManager* Plater::get_notification_manager() const { return p->notification_manager.get(); } +const NotificationManager * Plater::get_notification_manager() const +{ + return p->notification_manager.get(); +} -void Plater::init_notification_manager() { p->init_notification_manager(); } +void Plater::init_notification_manager() +{ + p->init_notification_manager(); +} -void Plater::show_status_message(std::string s) { BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; } +void Plater::show_status_message(std::string s) +{ + BOOST_LOG_TRIVIAL(trace) << "show_status_message:" << s; +} bool Plater::can_delete() const { return p->can_delete(); } bool Plater::can_delete_all() const { return p->can_delete_all(); } @@ -22048,10 +22195,9 @@ bool Plater::can_arrange() const { return p->can_arrange(); } bool Plater::can_layers_editing() const { return p->can_layers_editing(); } bool Plater::can_paste_from_clipboard() const { - if (!IsShown() || !p->is_view3D_shown()) - return false; + if (!IsShown() || !p->is_view3D_shown()) return false; - const Selection& selection = p->view3D->get_canvas3d()->get_selection(); + const Selection& selection = p->view3D->get_canvas3d()->get_selection(); const Selection::Clipboard& clipboard = selection.get_clipboard(); if (clipboard.is_empty() && p->sidebar->obj_list()->clipboard_is_empty()) @@ -22070,7 +22216,7 @@ bool Plater::can_paste_from_clipboard() const return true; } -// BBS support cut +//BBS support cut bool Plater::can_cut_to_clipboard() const { if (is_selection_empty()) @@ -22096,7 +22242,7 @@ bool Plater::can_copy_to_clipboard() const bool Plater::can_undo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_undo_snapshot(); } bool Plater::can_redo() const { return IsShown() && p->is_view3D_shown() && p->undo_redo_stack().has_redo_snapshot(); } bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } -// BBS +//BBS bool Plater::can_fillcolor() const { return p->can_fillcolor(); } bool Plater::has_assmeble_view() const { return p->has_assemble_view(); } bool Plater::can_replace_with_stl() const { return p->can_replace_with_stl(); } @@ -22113,33 +22259,53 @@ void Plater::enter_gizmos_stack() { p->enter_gizmos_stack(); } bool Plater::leave_gizmos_stack() { return p->leave_gizmos_stack(); } // BBS: return false if not changed bool Plater::inside_snapshot_capture() { return p->inside_snapshot_capture(); } -void Plater::toggle_render_statistic_dialog() { p->show_render_statistic_dialog = !p->show_render_statistic_dialog; } +void Plater::toggle_render_statistic_dialog() +{ + p->show_render_statistic_dialog = !p->show_render_statistic_dialog; +} -bool Plater::is_render_statistic_dialog_visible() const { return p->show_render_statistic_dialog; } +bool Plater::is_render_statistic_dialog_visible() const +{ + return p->show_render_statistic_dialog; +} -void Plater::toggle_show_wireframe() { p->show_wireframe = !p->show_wireframe; } +void Plater::toggle_show_wireframe() +{ + p->show_wireframe = !p->show_wireframe; +} -bool Plater::is_show_wireframe() const { return p->show_wireframe; } +bool Plater::is_show_wireframe() const +{ + return p->show_wireframe; +} -void Plater::enable_wireframe(bool status) { p->wireframe_enabled = status; } +void Plater::enable_wireframe(bool status) +{ + p->wireframe_enabled = status; +} + +bool Plater::is_wireframe_enabled() const +{ + return p->wireframe_enabled; +} -bool Plater::is_wireframe_enabled() const { return p->wireframe_enabled; } /*Plater::TakeSnapshot::TakeSnapshot(Plater *plater, const std::string &snapshot_name) : TakeSnapshot(plater, from_u8(snapshot_name)) {} Plater::TakeSnapshot::TakeSnapshot(Plater* plater, const std::string& snapshot_name, UndoRedo::SnapshotType snapshot_type) : TakeSnapshot(plater, from_u8(snapshot_name), snapshot_type) {}*/ + // Wrapper around wxWindow::PopupMenu to suppress error messages popping out while tracking the popup menu. -bool Plater::PopupMenu(wxMenu* menu, const wxPoint& pos) +bool Plater::PopupMenu(wxMenu *menu, const wxPoint& pos) { // Don't want to wake up and trigger reslicing while tracking the pop-up menu. SuppressBackgroundProcessingUpdate sbpu; // When tracking a pop-up menu, postpone error messages from the slicing result. m_tracking_popup_menu = true; - bool out = wxGetApp().mainframe->PopupMenu(menu, pos); + bool out = wxGetApp().mainframe->PopupMenu(menu, pos); m_tracking_popup_menu = false; - if (!m_tracking_popup_menu_error_message.empty()) { + if (! m_tracking_popup_menu_error_message.empty()) { // Don't know whether the CallAfter is necessary, but it should not hurt. // The menus likely sends out some commands, so we may be safer if the dialog is shown after the menu command is processed. wxString message = std::move(m_tracking_popup_menu_error_message); @@ -22148,19 +22314,27 @@ bool Plater::PopupMenu(wxMenu* menu, const wxPoint& pos) } return out; } -void Plater::bring_instance_forward() { p->bring_instance_forward(); } +void Plater::bring_instance_forward() +{ + p->bring_instance_forward(); +} -bool Plater::need_update() const { return p->need_update(); } +bool Plater::need_update() const +{ + return p->need_update(); +} -void Plater::set_need_update(bool need_update) { p->set_need_update(need_update); } +void Plater::set_need_update(bool need_update) +{ + p->set_need_update(need_update); +} // BBS -// BBS: add popup logic for table object +//BBS: add popup logic for table object bool Plater::PopupObjectTable(int object_id, int volume_id, const wxPoint& position) { - if (dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER))->m_extruders_count > 1) { - MessageDialog dlg(this, _L("Currently, the object configuration form cannot be used with a multiple-extruder printer."), - _L("Not available"), wxOK | wxICON_WARNING); + if (dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINTER))->m_extruders_count > 1) { + MessageDialog dlg(this, _L("Currently, the object configuration form cannot be used with a multiple-extruder printer."), _L("Not available"), wxOK | wxICON_WARNING); dlg.ShowModal(); return false; } @@ -22171,34 +22345,41 @@ bool Plater::PopupObjectTableBySelection() { wxDataViewItem item; int obj_idx, vol_idx; - const wxPoint pos = wxPoint(0, 0); // Fake position + const wxPoint pos = wxPoint(0, 0); //Fake position wxGetApp().obj_list()->get_selected_item_indexes(obj_idx, vol_idx, item); return p->PopupObjectTable(obj_idx, vol_idx, pos); } -void Plater::update_title_dirty_status() { p->update_title_dirty_status(); } +void Plater::update_title_dirty_status() +{ + p->update_title_dirty_status(); +} -wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } -wxMenu* Plater::object_menu() { return p->menus.object_menu(); } -wxMenu* Plater::part_menu() { return p->menus.part_menu(); } -wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } -wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } -wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } -wxMenu* Plater::default_menu() { return p->menus.default_menu(); } -wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } -wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } -wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } -wxMenu* Plater::filament_action_menu(int active_filament_menu_id) { return p->menus.filament_action_menu(active_filament_menu_id); } -int Plater::GetPlateIndexByRightMenuInLeftUI() { return p->m_is_RightClickInLeftUI; } -void Plater::SetPlateIndexByRightMenuInLeftUI(int index) { p->m_is_RightClickInLeftUI = index; } -SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() - : m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) -{ wxGetApp().plater()->suppress_background_process(m_was_scheduled); } + +wxMenu* Plater::plate_menu() { return p->menus.plate_menu(); } +wxMenu* Plater::object_menu() { return p->menus.object_menu(); } +wxMenu* Plater::part_menu() { return p->menus.part_menu(); } +wxMenu* Plater::text_part_menu() { return p->menus.text_part_menu(); } +wxMenu* Plater::svg_part_menu() { return p->menus.svg_part_menu(); } +wxMenu* Plater::sla_object_menu() { return p->menus.sla_object_menu(); } +wxMenu* Plater::default_menu() { return p->menus.default_menu(); } +wxMenu* Plater::instance_menu() { return p->menus.instance_menu(); } +wxMenu* Plater::layer_menu() { return p->menus.layer_menu(); } +wxMenu* Plater::multi_selection_menu() { return p->menus.multi_selection_menu(); } +wxMenu *Plater::filament_action_menu(int active_filament_menu_id) { return p->menus.filament_action_menu(active_filament_menu_id); } +int Plater::GetPlateIndexByRightMenuInLeftUI() { return p->m_is_RightClickInLeftUI; } +void Plater::SetPlateIndexByRightMenuInLeftUI(int index) { p->m_is_RightClickInLeftUI = index; } +SuppressBackgroundProcessingUpdate::SuppressBackgroundProcessingUpdate() : + m_was_scheduled(wxGetApp().plater()->is_background_process_update_scheduled()) +{ + wxGetApp().plater()->suppress_background_process(m_was_scheduled); +} SuppressBackgroundProcessingUpdate::~SuppressBackgroundProcessingUpdate() -{ wxGetApp().plater()->schedule_background_process(m_was_scheduled); } -wxString get_view_type_string(Camera::ViewAngleType type) { + wxGetApp().plater()->schedule_background_process(m_was_scheduled); +} +wxString get_view_type_string(Camera::ViewAngleType type) { switch (type) { case Slic3r::GUI::Camera::ViewAngleType::Iso: return _L("isometric"); case Slic3r::GUI::Camera::ViewAngleType::Top_Front: return _L("top_front"); @@ -22211,12 +22392,11 @@ wxString get_view_type_string(Camera::ViewAngleType type) default: return ""; } } -wxArrayString get_all_camera_view_type() -{ +wxArrayString get_all_camera_view_type() { wxArrayString all_types; - for (size_t i = 0; i < (int) Camera::ViewAngleType::Count_ViewAngleType; i++) { + for (size_t i = 0; i < (int)Camera::ViewAngleType::Count_ViewAngleType; i++) { all_types.Add(get_view_type_string((Camera::ViewAngleType) i)); } return all_types; } -}} // namespace Slic3r::GUI +}} // namespace Slic3r::GUI diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index d30b40004d..5c38859a12 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -866,6 +866,65 @@ SCENARIO("Minimal published 3MF omits project config, preset dumps and slicer ta } } +// A minimal published 3MF must not leak the slicer tags of the source project. The exporter seeds +// metadata_item_map from the input file's metadata_items, so re-publishing a project opened from a +// regular Orca/BBS 3MF (the typical remix flow) must strip the Application / OrcaSlicer tags it +// came with, otherwise old receivers route onto the baked-in "old version" popup. +SCENARIO("MinimalPublished strips slicer tags carried by the source project", "[3mf]") { + GIVEN("a model loaded from a regular Orca/BBS 3MF whose metadata carries the slicer tags") { + 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(); + + model.model_info = std::make_shared(); + model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1"; + model.model_info->metadata_items["Application"] = "BambuStudio-2.0.0"; + model.model_info->metadata_items["OrcaSlicer"] = "2.1.0"; + + ScopedTemporaryDir backup_dir("orca_strip_tags"); + model.set_backup_path(backup_dir.string()); + + WHEN("stored using SaveStrategy::MinimalPublished and reloaded") { + 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 | SaveStrategy::MinimalPublished; + REQUIRE(store_bbs_3mf(store_params)); + + Model dst_model; + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector loaded_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, + &loaded_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig); + THEN("the source slicer tags are stripped, not carried through") { + REQUIRE(loaded); + REQUIRE(dst_model.model_info != nullptr); + REQUIRE(dst_model.model_info->metadata_items.count("Application") == 0); + REQUIRE(dst_model.model_info->metadata_items.count("OrcaSlicer") == 0); + // The published marker itself must survive. + REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_TAG] == "1"); + } + THEN("the file classifies as a generic 3MF without a version popup") { + REQUIRE_FALSE(is_bbl_3mf); + REQUIRE_FALSE(is_orca_3mf); + REQUIRE_FALSE(file_version.valid()); + } + release_PlateData_list(dst_plates); + } + } +} + // An entry masks the non-published slots to their defaults so publishing slot 1 never leaks slot // 0's value into the file. Both a full entry (the whole-slot key list) and a partial entry (a // per-slot key) go through the same masking path in filter_published_config (keys and full_keys @@ -938,6 +997,40 @@ SCENARIO("Unmaskable keys are dropped from the published payload instead of leak } } +// A per-extruder printer key carrying a "#N" variant (e.g. retraction_length#1) must not serialize +// every extruder's value: the base is masked to the author's extruder and the other slots are +// restored to their option default, matching the material-side slot-masking invariant. A bare +// printer base key (no variant) keeps whole-vector serialization. +SCENARIO("Published per-extruder printer keys mask the other extruders to their defaults", "[3mf]") { + GIVEN("a full print configuration with three extruders carrying per-extruder retraction values") { + DynamicPrintConfig full_cfg = DynamicPrintConfig::full_print_config(); + // Non-default values on the un-selected slots, so a leak is distinguishable from the mask + // restoring the option default (retraction_length defaults to {0.8}). + full_cfg.opt("retraction_length")->values = { 3.0, 1.2, 4.0 }; + + WHEN("filtering with only extruder 1's retraction_length checked") { + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, { "retraction_length#1" }, {}); + + THEN("the author's extruder value survives") { + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[1], Catch::Matchers::WithinAbs(1.2, 1e-6)); + } + THEN("the other extruders are masked to their default") { + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[0], Catch::Matchers::WithinAbs(0.8, 1e-6)); + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[2], Catch::Matchers::WithinAbs(0.8, 1e-6)); + } + } + WHEN("filtering the bare base key without a '#N' variant") { + DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, { "retraction_length" }, {}); + + THEN("the whole vector is serialized unmasked") { + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[0], Catch::Matchers::WithinAbs(3.0, 1e-6)); + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[1], Catch::Matchers::WithinAbs(1.2, 1e-6)); + REQUIRE_THAT(filtered_cfg.opt("retraction_length")->values[2], Catch::Matchers::WithinAbs(4.0, 1e-6)); + } + } + } +} + // 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 extended material metadata", "[3mf]") { From bcff39661cb4c58104ee82c558b2e62484c0db19 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 3 Sep 2026 13:40:58 +0800 Subject: [PATCH 53/60] Comments and dead code cleanup --- src/libslic3r/PresetBundle.cpp | 75 ++++++++-------------------- src/libslic3r/PublishSettings.hpp | 55 ++++++++------------ src/slic3r/GUI/KBShortcutsDialog.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 19 +++++-- 4 files changed, 57 insertions(+), 94 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index e4bafd0071..4c4114345e 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -5220,29 +5220,18 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // Material pass: 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. - // - full: the slot always lands on a freshly created standalone detached copy - // ("Detach from parent", project-embedded, universally compatible, within-load - // deduped) - handled up front in the entry loop below; no library preset is ever - // reused or mutated; - // - partial: a type requirement gates the application - on type match (or no - // requirement) the keys are applied onto the slot's effective preset; on mismatch - // the slot is replaced with the best visible candidate, scored by the published - // identity (exact preset name resolved through the collection's name machinery, - // then exact setting_id, exact filament_id, then vendor+type, then type only); a - // preset no other slot references wins on equal scores; with no replacement - // available the receiver's material is kept and the keys are reported as skipped; - // - colour: applied to the slot regardless of the type gate. - // - capacity: on a non-SEMM receiver whose printer has fewer nozzles than the - // authored slot needs, the entry becomes an empty mixed-filament placeholder - // appended at the tail (the GUI flags it; the user assigns components from their - // own filaments); on a single-physical-slot receiver it is dropped and reported - // instead, since an empty mix could never be edited there. + // - full: lands on a freshly created standalone detached copy (no library preset used + // or mutated); + // - partial: a type requirement gates application; on mismatch the slot is replaced + // with the best visible candidate by published identity (exact name, setting_id, + // filament_id, vendor+type, type); with no replacement the receiver's material is + // kept and the keys are reported as skipped; + // - colour: applied regardless of the type gate. + // - capacity: past the printer's physical nozzles the entry becomes an empty + // mixed-filament placeholder; on a single-physical-slot receiver it is dropped. // Applied partial values land on the collection's edited layer when the slot references - // it and that layer survives the load (visible as a modification, revertible, the user's - // unsaved edits preserved), otherwise on the stored preset in place. - // To keep slot-to-slot aliasing (several slots referencing one preset) from leaking one - // slot's values into another, published slots sharing a preset with another slot are - // re-pointed at distinct presets before the values are applied. + // it and that layer survives the load, otherwise on the stored preset in place. Slots + // aliasing a shared preset are re-pointed at distinct presets before the values apply. { // Grow the receiver's slots only as far as the highest published slot (never // shrink, never pull filler materials for unpublished slots). @@ -5281,26 +5270,13 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool grow_target = std::max(grow_target, size_t(entry.slot) + 1); } // Mixed-filament definitions live in project-level virtual slots, so applying one - // positionally onto a receiver slot that holds a real, physical filament would - // silently convert hardware-backed state into a virtual mix. Compute each mixed - // entry's destination before anything consumes entry.slot (growth, seeding, - // de-aliasing, the overlay below): - // - a definition landing on a receiver slot that already carries a mixed - // definition keeps its place (a like-for-like override of a virtual slot); - // - everything else goes through one monotone append counter preserving author - // order: dest = max(authored, next_free). With a receiver shorter than the - // publish this keeps the authored positions intact; past them (or around a - // collision with a real filament) the mixes pack onto consecutive fresh slots - // AFTER every positional (real-filament) territory. The definition's cells are - // shifted inside the file-side per-slot mixed arrays so they stay readable - // from the new index. No existing slot changes meaning. - // - destinations are also capped: appends past the extruder limit are dropped - // and reported instead of being forced onto a physical filament. - // Physical entries past the printer's capacity join the same append counter - // (dest = next_free, packed consecutively at the tail - never max(authored, - // next_free), which would grow filler physical slots past the capacity) and are - // flagged mixed_placeholder: they become empty mixed-filament placeholders the - // GUI flags for the user to assign components to. + // positionally onto a receiver slot holding a real filament would convert hardware + // state into a virtual mix. Compute each entry's destination before anything + // consumes entry.slot: a definition on a slot that already holds a mixed definition + // keeps its place (like-for-like); everything else follows one monotone append + // counter preserving author order (dest = max(authored, next_free)). Appends past + // the extruder limit are dropped and reported. Physical entries past capacity join + // the same counter as mixed_placeholder empties the GUI flags for assignment. size_t next_free_slot = this->filament_presets.size(); bool any_mixed_relocated = false; // All authored-slot -> destination moves decided by this pass, applied to the @@ -5441,20 +5417,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool const auto it = exact_name_by_slot.find(slot); return it == exact_name_by_slot.end() ? std::string() : it->second; }; - std::set used_preset_names(this->filament_presets.begin(), this->filament_presets.end()); // Mirror first_visible_idx()'s start index so suppressed default presets are // never picked as a slot material. const size_t first_candidate = this->filaments.is_default_suppressed() ? this->filaments.num_default_presets() : 0; - // Candidate preference for a published entry: exact preset name (the raw author - // name and the collection-resolved name - renames, removed vendor-generic - // library fallback - both identify the exact preset, unambiguous even when ids - // are shared between variants or missing from older files), then the trimmed - // bare form / alias ("Generic PLA" from "Generic PLA @System"): a same-family - // match that must never outrank the exact preset, then exact setting_id - // (variant-level, since "Generic PLA" and "Generic PLA Matte" share - // filament_id), then exact filament_id, then vendor+type, then type only (a - // type-only pick may surface an unrelated preset, e.g. a different vendor's - // PLA). + // Candidate preference for a published entry: exact preset name (raw author and + // collection-resolved), then trimmed bare form / alias, then exact setting_id + // (variant-level), then exact filament_id, then vendor+type, then type only. auto candidate_score = [](const Preset& candidate, const PublishedMaterialEntry& entry, const std::string& resolved_name) -> int { // Exact preset name: raw and collection-resolved forms both outrank the @@ -5512,7 +5480,6 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool initial_preset = this->filament_presets.empty() ? this->filaments.first_visible().name : this->filament_presets.back(); this->filament_presets.emplace_back(initial_preset); - used_preset_names.insert(initial_preset); } // Published slots that alias another slot (multi-extruder with one filament) // get re-pointed at distinct presets: the overlay writes onto the slot's diff --git a/src/libslic3r/PublishSettings.hpp b/src/libslic3r/PublishSettings.hpp index d7f2c13c5f..60ea9a8354 100644 --- a/src/libslic3r/PublishSettings.hpp +++ b/src/libslic3r/PublishSettings.hpp @@ -10,20 +10,16 @@ class PresetBundle; std::string publish_base_key(const std::string &key); // Structural keys that are never applied onto the receiver's presets when loading a published -// 3MF (single source of truth for the denylist): applying them would rewrite the user's preset -// inheritance/structure. filament_ids is nevertheless exported via the identity list in -// filter_published_config because 3MF validation needs it - exported, never applied. +// 3MF (single source of truth for the denylist); applying them would rewrite the user's preset +// inheritance/structure. filament_ids is still exported via the identity list (3MF validation +// needs it) - exported, never applied. const std::set& 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. +// s_project_options). Import applies them into project_config, not a filament preset. const std::set& publish_mixed_keys(); -// 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. +// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (config key + tab icon id). struct PublishablePrinterOption { const char *key; // config key, e.g. "retraction_length" const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length" @@ -33,8 +29,8 @@ struct PublishablePrinterOption { const std::vector& publishable_printer_retraction_options(); const std::vector& publishable_printer_z_hop_options(); -// Union of the two optgroup option lists; the published-3MF overlay applies printer keys only -// when their base key is in this allowlist (anything else is contract-excluded). +// Union of the two optgroup option lists; printer keys apply on import only if their base +// key is in this allowlist. const std::set& publishable_printer_keys(); // Union of setting keys differing from the base/system preset across the current print, @@ -60,32 +56,25 @@ struct PublishedMaterialEntry { // 0-based author filament slot; -1 (hand-crafted files) is skipped. int slot{-1}; std::vector keys; - // "Full Publish": the whole filament preset (full_keys) is published. On the receiver - // Full Publish always creates a standalone parentless copy (libslic3r's "Detach from - // parent"): a new project-embedded preset ("Preset Inside Project") with the full - // resolved config, universally compatible (compatible_printers/condition cleared). - // It lives inside the loaded project only - never written to the user's library, - // no existing preset is ever selected-by-reference or mutated. Identical Full - // entries inside one load share one created instance (within-load dedup). + // "Full Publish": the whole filament preset (full_keys) is published. On the receiver Full + // Publish always creates a standalone parentless copy (libslic3r's "Detach from parent"), + // universally compatible and project-embedded only - never written to the user's library. + // Identical Full entries within one load share one created instance (within-load dedup). bool full{false}; // All non-structural filament keys of the author's slot preset; values travel in the file // config, masked to the author's slot index. std::vector full_keys; // Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a - // partial entry's mismatch the slot is replaced with a same-type filament from the - // receiver's library. Full entries consult no gate: they detach unconditionally, and the - // copy carries whatever values the payload bakes. + // partial entry's mismatch the slot is replaced with a same-type filament. Full entries + // consult no gate. bool publish_type{false}; std::string publish_type_value; // Required filament colour, applied on load regardless of the type match. bool publish_color{false}; std::string color; - // Import-side only, never serialized: the entry's authored slot sits past the receiver - // printer's physical filament capacity, so instead of growing a physical slot the entry - // is appended as an empty mixed-filament placeholder (virtual tail slot; the GUI flags - // it and the user assigns components from their own filaments). The flag also keeps the - // entry out of the payload mixed-definition validation and the value-apply passes, - // which only make sense for a slot that carries a real material. + // Import-side only, never serialized: the authored slot sits past the receiver's physical + // capacity, so the entry is appended as an empty mixed-filament placeholder (virtual tail + // slot; the GUI flags it for the user to assign components). bool mixed_placeholder{false}; }; @@ -93,16 +82,12 @@ struct PublishedMaterialEntry { std::string normalize_filament_type(const std::string& type); class DynamicPrintConfig; -// Clear the compatibility lists/conditions on a filament config so it is compatible -// with every printer and every print profile. A detached published material is -// universally compatible by construction: the baseline clone may carry machine-specific -// restrictions. Empty lists + empty conditions => compatible with everything -// (see is_compatible_with_printer, Preset.cpp:840). +// Clear the compatibility lists/conditions on a filament config so it is universally +// compatible once detached (empty lists + empty conditions = compatible with everything). void make_publish_universal(DynamicPrintConfig &config); -// Naming base for a detached published-material copy: "Generic PLA @System" -> -// "Generic PLA" (truncate at the first '@' variant tail, right-trimmed). Unchanged -// when the name carries no '@'. Empty result means "fall back to identity fields". +// Naming base for a detached published-material copy: "Generic PLA @System" -> "Generic PLA" +// (truncate/right-trim at the first '@' tail). Empty result means "fall back to identity". std::string publish_material_base_name(const std::string &preset_name); // Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys, diff --git a/src/slic3r/GUI/KBShortcutsDialog.cpp b/src/slic3r/GUI/KBShortcutsDialog.cpp index df69fa5434..2f882e35aa 100644 --- a/src/slic3r/GUI/KBShortcutsDialog.cpp +++ b/src/slic3r/GUI/KBShortcutsDialog.cpp @@ -174,7 +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") }, + { ctrl + shift + "E", L("Publish 3MF") }, // File>Import { ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") }, // File>Export diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0d2c5ac4c9..a1915a1200 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15300,11 +15300,12 @@ void Plater::load_project(wxString const& filename2, if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; p->set_project_filename(filename); - } else if (loaded_published) { + } else if (loaded_published && !res.empty()) { // A "published" 3MF loads as a new project: its path must not become the project // filename (Save/Ctrl-S prompts for a destination instead of overwriting it); // reset() already cleared the project name, so restore the default title and keep - // the file in recents. + // the file in recents. Only on a successful load (res not empty): a failed or + // cancelled load must not pollute "Recently opened". p->set_project_name(_L("Untitled")); if (!filename.IsEmpty()) wxGetApp().mainframe->add_to_recent_projects(filename); @@ -18316,8 +18317,18 @@ int Plater::export_published_3mf(const std::vector& published_keys, DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure(); DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys); std::string payload; - for (const std::string& key : filtered_cfg.keys()) - payload += key + " = " + filtered_cfg.opt_serialize(key) + "\n"; + for (const std::string& key : filtered_cfg.keys()) { + // A value containing a newline would break the INI written below (read_ini throws), + // so load_from_ini_string discards the whole settings block on import. Skip such + // keys instead of silently dropping every setting. + std::string value = filtered_cfg.opt_serialize(key); + if (value.find('\n') != std::string::npos) { + BOOST_LOG_TRIVIAL(warning) << "publish: dropping key \"" << key + << "\" from the published payload (value contains a newline)"; + continue; + } + payload += key + " = " + value + "\n"; + } model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload); // Same file layout as save_project(), plus Silence (so export_3mf does not set the project From 904796cf24eb64a336bdae22263260822b2be818 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Thu, 3 Sep 2026 14:12:51 +0800 Subject: [PATCH 54/60] Correctness fixes. Remove hard-coded appends for printer settings --- src/libslic3r/Format/bbs_3mf.cpp | 119 +++++++++++++++++++++++++------ src/slic3r/GUI/Plater.cpp | 6 +- src/slic3r/GUI/Tab.cpp | 31 +++----- 3 files changed, 111 insertions(+), 45 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 4b9c666ce0..20e63e9d5e 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -6999,16 +6999,24 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // as From_Other and import the geometry silently. if (m_minimal_published) { // metadata_item_map is seeded from the input file's metadata_items above, so a - // project opened from a regular Orca/BBS 3MF still carries the Application / - // OrcaSlicer tags it came with. Erase them: skipping the overwrite is not enough, - // and an empty value would still emit a "present-looking" tag to old receivers. + // project opened from a regular Orca/BBS 3MF still carries the slicer-identifying + // tags it came with. Erase every one of them - not just the two most common - + // so a published 3MF is fully tag-less: old receivers classify it as From_Other + // and import the geometry silently instead of showing a baked-in "old version" + // popup, and no version marker survives to seed a later re-save. metadata_item_map.erase(BBL_APPLICATION_TAG); metadata_item_map.erase(ORCASLICER_TAG); + metadata_item_map.erase(BBS_3MF_VERSION); + metadata_item_map.erase(BBS_3MF_VERSION1); } else { metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str(); } } - metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF); + // The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal + // published file along with the tags erased above (skipping the overwrite alone would + // leave the value the source file seeded into metadata_item_map). + if (!m_minimal_published) + metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF); if (!model.mk_name.empty()) { metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name); @@ -7031,10 +7039,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second; stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">" << xml_escape(item.second) << "\n"; - if (item.first == BBL_APPLICATION_TAG) { + if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) { // The OrcaSlicer tag is only written for files that carry the Application // tag, which a minimal published 3MF erases (see the map assignment above): - // the branch below is unreachable in minimal mode. + // the explicit !m_minimal_published guard keeps the tag-less guarantee from + // depending on that erase happening to run first. stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">" << xml_escape(SoftFever_VERSION) << "\n"; } @@ -9213,6 +9222,68 @@ std::string bbs_3mf_get_thumbnail(const char *path) return data; } +namespace { + +// Parses just the model-file elements, mirroring the importer's +// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped, +// whitespace tolerant). Stops the parser as soon as the published flag node is read so the +// geometry/resources that follow are skipped, which keeps the per-file cost small. +struct PublishedXmlProbe +{ + XML_Parser parser{nullptr}; + bool in_metadata{false}; + bool found{false}; + bool published{false}; + std::string curr_name; + std::string curr_value; + + static std::string attribute(const char** attrs, const char* key) + { + if (attrs == nullptr) + return std::string(); + // expat hands the attrs as a NULL-terminated {name, value, ...} array. + for (unsigned int a = 0; attrs[a] != nullptr; a += 2) + if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr) + return attrs[a + 1]; + return std::string(); + } + + static void XMLCALL start(void* user_data, const char* name, const char** attrs) + { + auto* self = static_cast(user_data); + if (::strcmp(name, METADATA_TAG) == 0) { + self->in_metadata = true; + self->curr_name = attribute(attrs, NAME_ATTR); + self->curr_value.clear(); + } else { + self->in_metadata = false; + } + } + + static void XMLCALL characters(void* user_data, const XML_Char* s, int len) + { + auto* self = static_cast(user_data); + if (self->in_metadata) + self->curr_value.append(s, len); + } + + static void XMLCALL end(void* user_data, const char* name) + { + auto* self = static_cast(user_data); + if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0) + return; + self->in_metadata = false; + if (self->curr_name == ORCA_PUBLISHED_TAG) { + self->published = is_published_3mf_flag(xml_unescape(self->curr_value)); + self->found = true; + if (self->parser != nullptr) + XML_StopParser(self->parser, false); + } + } +}; + +} // namespace + bool bbs_3mf_is_published(const std::string &path) { mz_zip_archive archive; @@ -9234,7 +9305,8 @@ bool bbs_3mf_is_published(const std::string &path) if (!open_zip_reader(&archive, path)) return false; - // Read just the model XML and locate the published metadata node; no geometry parsing. + // Read just the model XML (the metadata node sits before the resources, so the probe below + // stops early) rather than by a raw substring match; no geometry parsing. int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0); if (index < 0) return false; @@ -9245,22 +9317,29 @@ bool bbs_3mf_is_published(const std::string &path) if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0)) return false; - const std::string needle = std::string(""; - size_t pos = xml.find(needle); - if (pos == std::string::npos) - return false; - pos += needle.size(); - size_t end = xml.find("", pos); - if (end == std::string::npos) + XML_Parser parser = XML_ParserCreate(nullptr); + if (parser == nullptr) return false; - size_t value_begin = pos, value_end = end; - while (value_begin < value_end && (xml[value_begin] == ' ' || xml[value_begin] == '\t' || xml[value_begin] == '\n' || xml[value_begin] == '\r')) - ++value_begin; - while (value_end > value_begin && (xml[value_end - 1] == ' ' || xml[value_end - 1] == '\t' || xml[value_end - 1] == '\n' || xml[value_end - 1] == '\r')) - --value_end; + PublishedXmlProbe probe; + probe.parser = parser; + XML_SetUserData(parser, &probe); + XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end); + XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters); + // Never resolve external entities from a file we are only probing. + XML_SetExternalEntityRefHandler(parser, nullptr); + XML_SetEntityDeclHandler(parser, nullptr); - return is_published_3mf_flag(xml.substr(value_begin, value_end - value_begin)); + const XML_Status status = XML_Parse(parser, xml.data(), static_cast(xml.size()), 1); + // XML_StopParser(parser, false) from the end handler makes XML_Parse return + // XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag). + const bool parse_ok = (status == XML_STATUS_OK) || + (XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found); + XML_ParserFree(parser); + + if (!parse_ok) + return false; + return probe.published; } bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index a1915a1200..86815bb7ab 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8752,7 +8752,9 @@ std::vector Plater::priv::load_files(const std::vector& input_ } Semver old_version(1, 5, 9); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) { + // A published 3MF has no project config to migrate: skip the old-version + // translations even if a slicer tag slipped through classification. + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && !published_config.published && load_model && load_config && !config_loaded.empty()) { translate_old = true; partplate_list.get_plate_size(current_width, current_depth, current_height); } @@ -8854,7 +8856,7 @@ std::vector Plater::priv::load_files(const std::vector& input_ { // BBS: modify the prime tower params for old version file Semver old_version3(2, 0, 0); - if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) { + if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && !published_config.published && file_version < old_version3) { double old_filament_prime_volume = 0.; int filament_count = 0; { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 96382855ad..962d59d9ca 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -7,6 +7,7 @@ #include "libslic3r/FilamentMixer.hpp" #include "libslic3r/Utils.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/PublishSettings.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "Search.hpp" @@ -5699,32 +5700,16 @@ if (is_marlin_flavor) optgroup->append_single_option_line("extruder_offset", "printer_extruder_basic_information#extruder-offset-position", extruder_idx); //BBS: don't show retract related config menu in machine page - // Keep this optgroup's options in sync with publishable_printer_retraction_options() - // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union - // with the Z-Hop optgroup below. + // These optgroups are built from publishable_printer_retraction/z_hop_options() so the + // published-3MF printer allowlist (their union in libslic3r/PublishSettings.hpp) can + // never drift from what the machine page actually shows. optgroup = page->new_optgroup(L("Retraction"), L"param_retraction"); - optgroup->append_single_option_line("retraction_length", "printer_extruder_retraction#length", extruder_idx); - optgroup->append_single_option_line("retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart", extruder_idx); - optgroup->append_single_option_line("retraction_speed", "printer_extruder_retraction#retraction-speed", extruder_idx); - optgroup->append_single_option_line("deretraction_speed", "printer_extruder_retraction#deretraction-speed", extruder_idx); - optgroup->append_single_option_line("retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold", extruder_idx); - optgroup->append_single_option_line("retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change", extruder_idx); - optgroup->append_single_option_line("wipe", "printer_extruder_retraction#wipe-while-retracting", extruder_idx); - optgroup->append_single_option_line("wipe_distance", "printer_extruder_retraction#wipe-distance", extruder_idx); - optgroup->append_single_option_line("retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe", extruder_idx); - // Orca - optgroup->append_single_option_line("retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe", extruder_idx); + for (const PublishablePrinterOption& opt : publishable_printer_retraction_options()) + optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx); - // Keep this optgroup's options in sync with publishable_printer_z_hop_options() - // in libslic3r/PublishSettings.hpp: the published-3MF printer allowlist is its union - // with the Retraction optgroup above. optgroup = page->new_optgroup(L("Z-Hop"), L"param_extruder_lift_enforcement"); - optgroup->append_single_option_line("retract_lift_enforce", "printer_extruder_z_hop#on-surfaces", extruder_idx); - optgroup->append_single_option_line("z_hop_types", "printer_extruder_z_hop#z-hop-type", extruder_idx); - optgroup->append_single_option_line("z_hop", "printer_extruder_z_hop#z-hop-height", extruder_idx); - optgroup->append_single_option_line("travel_slope", "printer_extruder_z_hop#traveling-angle", extruder_idx); - optgroup->append_single_option_line("retract_lift_above", "printer_extruder_z_hop#only-lift-z-above", extruder_idx); - optgroup->append_single_option_line("retract_lift_below", "printer_extruder_z_hop#only-lift-z-below", extruder_idx); + for (const PublishablePrinterOption& opt : publishable_printer_z_hop_options()) + optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx); optgroup = page->new_optgroup(L("Retraction when switching material"), L"param_retraction_material_change"); optgroup->append_single_option_line("retract_length_toolchange", "printer_extruder_retraction#retraction-when-switching-materials", extruder_idx); From 07dafaf299492203e0a30bf444f64142632f6833 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 3 Sep 2026 17:13:09 +0800 Subject: [PATCH 55/60] fix: default enable-ota flag and fixing startup missing vendor --- src/libslic3r/AppConfig.cpp | 5 +++++ src/slic3r/Utils/PresetUpdater.cpp | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 020fc0662d..39fffd57a5 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -638,6 +638,11 @@ void AppConfig::set_defaults() set_bool("use_printer_agents", false); } + if (get("enable_ota").empty()) + { + set_bool("enable_ota", false); + } + // Remove legacy window positions/sizes erase("app", "main_frame_maximized"); erase("app", "main_frame_pos"); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 42bd346222..d05c7b9586 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1090,9 +1090,6 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const AppConfig *app_config = GUI::wxGetApp().app_config; - if (!app_config->get_bool("enable_ota")) - return; - const auto enabled_vendors = app_config->vendors(); std::set bundles; From c6ab725584362cc951c3fde3acf9905b34b86f9b Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 4 Sep 2026 14:41:20 +0800 Subject: [PATCH 56/60] Fixes issue with mixed filament being loaded into a real slot --- src/libslic3r/PresetBundle.cpp | 149 ++++++++++++- .../libslic3r/test_preset_bundle_loading.cpp | 209 ++++++++++++++++++ 2 files changed, 354 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index da5696840b..dab0dd4de3 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -4759,6 +4759,98 @@ static void apply_mixed_config_relocations(DynamicPrintConfig& } } +// Relocate the per-slot cells of the receiver's OWN mixed-filament definitions (the ones that +// pre-existed in project_config) onto fresh tail slots, clearing each vacated source cell so an +// incoming published real filament can claim it. Unlike apply_mixed_config_relocations - which +// moves the incoming file's config and leaves sources alone - a displaced receiver mix must not +// keep its mixed flag in the physical region: the source slot becomes a physical slot, so its +// mixed flag and definition are reset, while its swatch colour and mapping travel with the +// definition to the tail slot. Reads come from a frozen snapshot so an earlier move's +// destination never overwrites a later move's still-unread source (sources sit in the physical +// region and destinations past it, so they cannot overlap, but the snapshot keeps the helper +// safe for any future reordering). +static void apply_receiver_mix_relocations(DynamicPrintConfig& config, + std::vector>& ams_multi_color_filment, + const std::vector>& moves) +{ + if (moves.empty()) + return; + + auto move_bools = [&](const char* key, bool clear_source) { + ConfigOption* opt = config.optptr(key); + if (opt == nullptr) + return; + auto* live = static_cast(opt); + std::unique_ptr snapshot(opt->clone()); + const auto* frozen = static_cast(snapshot.get()); + for (const auto [from, to] : moves) { + const bool cell = from < frozen->values.size() ? frozen->values[from] : false; + if (live->values.size() <= to) + live->values.resize(to + 1, false); + live->values[to] = cell; + if (clear_source && from < live->values.size()) + live->values[from] = false; + } + }; + auto move_strings = [&](const char* key, bool clear_source) { + ConfigOption* opt = config.optptr(key); + if (opt == nullptr) + return; + auto* live = static_cast(opt); + std::unique_ptr snapshot(opt->clone()); + const auto* frozen = static_cast(snapshot.get()); + for (const auto [from, to] : moves) { + const std::string cell = from < frozen->values.size() ? frozen->values[from] : std::string(); + if (live->values.size() <= to) + live->values.resize(to + 1, std::string{}); + live->values[to] = cell; + if (clear_source && from < live->values.size()) + live->values[from] = std::string{}; + } + }; + auto move_ints = [&](const char* key) { + ConfigOption* opt = config.optptr(key); + if (opt == nullptr) + return; + auto* live = static_cast(opt); + std::unique_ptr snapshot(opt->clone()); + const auto* frozen = static_cast(snapshot.get()); + for (const auto [from, to] : moves) { + const int cell = from < frozen->values.size() ? frozen->values[from] : 0; + if (live->values.size() <= to) + live->values.resize(to + 1, 0); + live->values[to] = cell; + } + }; + + // Mixed-definition cells: move to the tail and clear the source - the vacated physical slot + // no longer holds a mix. + move_bools("filament_is_mixed", true); + move_strings("filament_mixed_components", true); + move_strings("filament_mixed_sublayer_ratios", true); + move_bools("filament_mixed_gradient", true); + move_strings("filament_mixed_gradient_range", true); + move_strings("filament_mixed_gradient_curve", true); + move_bools("filament_mixed_gradient_per_part", true); + // Swatch colour and mapping travel with the definition; the source colour is left for the + // incoming real's publish_color (or the slot's resolved preset) to fill in. + move_strings("filament_colour", false); + move_strings("filament_multi_colour", false); + move_strings("filament_colour_type", false); + move_ints("filament_map"); + move_ints("filament_nozzle_map"); + move_ints("filament_volume_map"); + { + const std::vector> frozen = ams_multi_color_filment; + for (const auto [from, to] : moves) { + const std::vector cell = from < frozen.size() ? frozen[from] : std::vector(); + if (ams_multi_color_filment.size() <= to) + ams_multi_color_filment.resize(to + 1, std::vector{}); + ams_multi_color_filment[to] = cell; + } + } +} + //convert the old filament preset to new one after split static void convert_filament_preset_name(std::string& machine_name, std::string& filament_name) @@ -5287,7 +5379,24 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // counter preserving author order (dest = max(authored, next_free)). Appends past // the extruder limit are dropped and reported. Physical entries past capacity join // the same counter as mixed_placeholder empties the GUI flags for assignment. - size_t next_free_slot = this->filament_presets.size(); + // + // The finished project keeps the physical-first invariant (see the sidebar's + // add_custom_filament: mixed slots always sit at the tail, physical slots packed + // first). That invariant must survive an import, so every receiver mixed slot that + // would end up inside (or ahead of) the incoming physical region is displaced to a + // fresh tail slot, and the tail allocator starts at the physical boundary rather + // than the receiver's slot count - otherwise a relocated mix can collide with a + // keep-placed new real slot. + size_t physical_boundary = this->num_physical_filaments(); + for (const PublishedMaterialEntry& entry : published_config->material_keys) + // Mirror the payload-real keep-place decision below: a real keeps its authored + // slot when that slot already exists on the receiver, or lies within the + // printer's physical capacity. Both end up as physical slots at index + // entry.slot, so they bound the physical region even past the capacity. + if (entry.slot >= 0 && !is_mixed_definition(entry) && + (size_t(entry.slot) < this->filament_presets.size() || size_t(entry.slot) < physical_capacity)) + physical_boundary = std::max(physical_boundary, size_t(entry.slot) + 1); + size_t next_free_slot = std::max(physical_boundary, this->filament_presets.size()); bool any_mixed_relocated = false; // All authored-slot -> destination moves decided by this pass, applied to the // incoming config in one batched snapshot step below (an earlier move's @@ -5295,6 +5404,33 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // onto consecutive slots, so incremental in-place shifts would overwrite a // definition that has not been moved yet). std::vector> mixed_moves; + // Receiver-mix relocations land in project_config, not the incoming config, so they + // get their own move list, applied below after the arrays grow. The swept set lets + // the payload loop below treat a displaced receiver mix as the physical slot it will + // become (a payload mix like-for-like overriding such a slot must itself relocate). + std::vector> receiver_mix_moves; + std::set swept_mix_slots; + for (size_t slot = 0; slot < this->filament_presets.size(); ++slot) { + if (!this->is_mixed_filament(slot)) + continue; + // Slot 0 is the receiver's base filament and is never a virtual mix; the + // sidebar's physical-first layout guarantees it, so never displace it. + if (slot == 0) + continue; + if (slot >= physical_boundary) + continue; // already lives in the tail region + const size_t dest = next_free_slot++; + swept_mix_slots.insert(slot); + receiver_mix_moves.emplace_back(slot, dest); + any_mixed_relocated = true; + // A payload mix authored at the same slot (a mix inside the physical region) is + // processed after this sweep and overrides its own destination below. + published_config->mixed_slot_relocations.insert_or_assign(int(slot), int(dest)); + published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + " -> slot " + + std::to_string(dest) + ": mixed filament"); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated receiver mixed filament slot " << slot + << " -> " << dest << " (physical-first rebalance)"; + } for (auto entry_it = published_config->material_keys.begin(); entry_it != published_config->material_keys.end();) { PublishedMaterialEntry& entry = *entry_it; if (entry.slot < 0) { @@ -5310,7 +5446,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // above the printer's capacity (pre-existing state is never shrunk). bool keep_place = false; if (is_payload_mix) - keep_place = this->is_mixed_filament(size_t(entry.slot)); + keep_place = this->is_mixed_filament(size_t(entry.slot)) && swept_mix_slots.count(size_t(entry.slot)) == 0; else keep_place = size_t(entry.slot) < this->filament_presets.size() || size_t(entry.slot) < physical_capacity; @@ -5357,7 +5493,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool entry.slot = dest_slot; any_mixed_relocated = true; mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot)); - published_config->mixed_slot_relocations.emplace(authored_slot, entry.slot); + published_config->mixed_slot_relocations.insert_or_assign(authored_slot, entry.slot); published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " + std::to_string(entry.slot) + ": mixed filament"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot @@ -5374,7 +5510,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool if (dest_slot != authored_slot) { entry.slot = dest_slot; mixed_moves.emplace_back(size_t(authored_slot), size_t(dest_slot)); - published_config->mixed_slot_relocations.emplace(authored_slot, dest_slot); + published_config->mixed_slot_relocations.insert_or_assign(authored_slot, dest_slot); } published_config->material_replacements.emplace_back( (dest_slot != authored_slot ? @@ -5725,6 +5861,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // slots wrote to it; that compound case is not chased.) const bool edited_survives_load = this->filament_presets.empty() || this->filament_presets.front() == this->filaments.get_edited_preset().name; + // Displaced receiver mixes (see the physical-first rebalance above): move their + // per-slot cells onto the grown tail slots and reset the vacated physical slots, + // so the incoming real filaments can claim them. Runs before mixed_final_slots is + // built, so the rebalanced layout is what the mix validation sees. + apply_receiver_mix_relocations(this->project_config, this->ams_multi_color_filment, receiver_mix_moves); // Final layout for mix-definition validation: every slot that will hold a // mixed definition once this load completes - the receiver's own virtual // slots, each published mixed entry's final (possibly relocated) slot, and diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 9526a64b02..f7971cd1d2 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -2980,6 +2980,215 @@ TEST_CASE("Published 3MF relocates a mixed filament instead of overwriting a phy } } +// A receiver that already owns a MIXED filament must keep the physical-first invariant after a +// published-3MF import: when incoming physical filaments would land on (or ahead of) the +// receiver's mixed slot, that mix is displaced to a fresh tail slot instead of being left +// interleaved with them (the R,M,R bug). +TEST_CASE("Published 3MF relocates the receiver's mixed filament past the incoming physical slots", "[Preset][Bundle][Published]") +{ + // Build the receiver's tool-changer with three slots, the third being the receiver's own + // mixed filament. A SEMM (single_extruder_multi_material) receiver sizes its slot list by + // hand, so a lower slot count than the printer's nozzle count is preserved on load - a + // non-SEMM tool-changer would top the preset list up to the nozzle count and shift the + // expected sizes (the rebalance logic under test is the same either way). + auto make_receiver = [](PresetBundle &bundle, const std::string &components, const std::string &ratios) { + Preset &pla = add_inmemory_preset(bundle.filaments, "My PLA"); + pla.config.opt_string("filament_type", 0u) = "PLA"; + bundle.filament_presets = { "My PLA", "My PLA", "My PLA" }; + bundle.set_num_filaments(3, "#123456"); + bundle.printers.get_edited_preset().config.opt("single_extruder_multi_material", true)->value = true; + bundle.project_config.opt("filament_is_mixed")->values[2] = 1; + bundle.project_config.opt("filament_mixed_components")->values[2] = components; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2] = ratios; + bundle.project_config.opt("filament_colour")->values[2] = "#800080"; + bundle.project_config.opt("filament_multi_colour")->values[2] = "#800080"; + }; + auto make_real_entry = [](int slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.publish_color = true; + entry.color = color; + return entry; + }; + // A four-physical author project with no mixed slots (colour publish only), as in the + // reported Ferrari reference file. + auto make_config_4_real = [] { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#000000", "#FFFFFF", "#FFFF00" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + return config; + }; + + // [R, R, M] + four colour-only physical slots at authored 0..3 -> [R, R, R, R, M]. + { + PresetBundle bundle; + make_receiver(bundle, "1,2", "0.5,0.5"); + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#000000"), + make_real_entry(2, "#FFFFFF"), make_real_entry(3, "#FFFF00") }; + DynamicPrintConfig config = make_config_4_real(); + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + // The receiver grows by one extra virtual slot; the mix lands at the tail. + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + for (size_t i = 0; i < 4; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[4]); + // The definition travelled with its swatch colour; the vacated slot 2 became physical. + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 5); + CHECK(components[4] == "1,2"); + CHECK(components[2].empty()); + const auto &colour = bundle.project_config.opt("filament_colour")->values; + REQUIRE(colour.size() == 5); + CHECK(colour[4] == "#800080"); + CHECK(colour[2] == "#FFFFFF"); + CHECK(pub.mixed_slot_relocations.at(2) == 4); + bool relocated_reported = false; + for (const std::string &message : pub.material_replacements) + if (message.find("slot 2 -> slot 4") != std::string::npos && + message.find("mixed filament") != std::string::npos) + relocated_reported = true; + CHECK(relocated_reported); + CHECK(pub.skipped_keys.empty()); + } + + // [R, R, M] plus a payload mix authored at slot 3: the receiver mix (displaced to slot 3) sits + // ahead of the appended payload mix (slot 4), preserving physical-first tail ordering. + { + PresetBundle bundle; + make_receiver(bundle, "1,2", "0.5,0.5"); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3, 4 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#000000", "#0000FF", "#800080" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99", "GFL99" }; + // Authored slot 3 is a payload mixed definition blending slots 1 and 3. + config.opt("filament_is_mixed")->values = { 0, 0, 0, 1 }; + config.opt("filament_mixed_components")->values = { "", "", "", "1,3" }; + config.opt("filament_mixed_sublayer_ratios")->values = { "", "", "", "0.6,0.4" }; + config.opt("filament_mixed_gradient")->values = { 0, 0, 0, 1 }; + config.opt("filament_mixed_gradient_range")->values = { "", "", "", "0.9,0.1" }; + config.opt("filament_mixed_gradient_curve")->values = { "", "", "", "0,0.1|1,0.9" }; + config.opt("filament_mixed_gradient_per_part")->values = { 0, 0, 0, 1 }; + + PublishedMaterialEntry mix; + mix.slot = 3; + mix.filament_type = "PLA"; + 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 = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#000000"), + make_real_entry(2, "#0000FF"), mix }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + CHECK_FALSE(is_mixed[0]); + CHECK_FALSE(is_mixed[1]); + CHECK_FALSE(is_mixed[2]); + CHECK(is_mixed[3]); // receiver's mix, displaced to slot 3 first + CHECK(is_mixed[4]); // payload's mix, appended after + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 5); + CHECK(components[3] == "1,2"); + CHECK(components[4] == "1,3"); + REQUIRE(pub.mixed_slot_relocations.size() == 2); + CHECK(pub.mixed_slot_relocations.at(2) == 3); + CHECK(pub.mixed_slot_relocations.at(3) == 4); + CHECK(pub.skipped_keys.empty()); + } +} + +// Multiple receiver mixed slots interleaved with multiple incoming physical slots all rebalance +// onto consecutive tail slots in index order (no cascade/overlap). +TEST_CASE("Published 3MF rebalances several receiver mixed slots past the physical region", "[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", "My PLA", "My PLA" }; + bundle.set_num_filaments(3, "#123456"); + bundle.printers.get_edited_preset().config.opt("single_extruder_multi_material", true)->value = true; + // Receiver: slot 1 and slot 2 are mixed. + bundle.project_config.opt("filament_is_mixed")->values[1] = 1; + bundle.project_config.opt("filament_is_mixed")->values[2] = 1; + bundle.project_config.opt("filament_mixed_components")->values[1] = "1,2"; + bundle.project_config.opt("filament_mixed_components")->values[2] = "1,3"; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[1] = "0.5,0.5"; + bundle.project_config.opt("filament_mixed_sublayer_ratios")->values[2] = "0.4,0.6"; + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.opt("filament_diameter")->values = { 1.75, 1.75, 1.75 }; + config.opt("filament_self_index")->values = { 1, 2, 3 }; + config.opt("filament_extruder_variant")->values = { + "Direct Drive Standard", "Direct Drive Standard", "Direct Drive Standard" + }; + config.opt("filament_colour")->values = { "#FF0000", "#00AA00", "#0000FF" }; + config.opt("filament_type")->values = { "PLA", "PLA", "PLA" }; + config.opt("filament_vendor")->values = { "Generic", "Generic", "Generic" }; + config.opt("filament_ids")->values = { "GFL99", "GFL99", "GFL99" }; + + auto make_real_entry = [](int slot, const char *color) { + PublishedMaterialEntry entry; + entry.slot = slot; + entry.filament_type = "PLA"; + entry.filament_vendor = "Generic"; + entry.publish_color = true; + entry.color = color; + return entry; + }; + + PublishedConfig pub; + pub.published = true; + pub.material_keys = { make_real_entry(0, "#FF0000"), make_real_entry(1, "#00AA00"), make_real_entry(2, "#0000FF") }; + Preset::normalize(config); + bundle.load_config_model("test.3mf", std::move(config), Semver(), &pub); + + REQUIRE(bundle.filament_presets.size() == 5); + const auto &is_mixed = bundle.project_config.opt("filament_is_mixed")->values; + REQUIRE(is_mixed.size() == 5); + for (size_t i = 0; i < 3; ++i) + CHECK_FALSE(is_mixed[i]); + CHECK(is_mixed[3]); + CHECK(is_mixed[4]); + const auto &components = bundle.project_config.opt("filament_mixed_components")->values; + REQUIRE(components.size() == 5); + CHECK(components[3] == "1,2"); + CHECK(components[4] == "1,3"); + REQUIRE(pub.mixed_slot_relocations.size() == 2); + CHECK(pub.mixed_slot_relocations.at(1) == 3); + CHECK(pub.mixed_slot_relocations.at(2) == 4); + CHECK(pub.skipped_keys.empty()); +} + // The receiver's printer gates how many PHYSICAL filament slots a published 3MF may add: a // non-SEMM tool-changer feeds filament N from nozzle N, so a published slot past the nozzle // count cannot become a physical filament. It becomes an empty mixed-filament placeholder From 9df23cab625d483f8746658d376d4607b10d2f11 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Fri, 4 Sep 2026 14:59:53 +0800 Subject: [PATCH 57/60] fix: opc updates being discarded --- src/slic3r/Utils/PresetUpdater.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index d05c7b9586..017deb0fc9 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -780,11 +780,15 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id) const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json"); const fs::path cached_vendor_folder = cache_profile_path / vendor_id; - if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) || - fs::is_empty(cached_vendor_folder)) { + + const fs::path cached_vendor_opc = cache_profile_path / (vendor_id + ".opc"); + + bool is_json_update = fs::is_regular_file(cached_vendor_json) && fs::is_directory(cached_vendor_folder) && !fs::is_empty(cached_vendor_folder); + bool is_opc_update = fs::is_regular_file(cached_vendor_opc); + if (!is_json_update && !is_opc_update) { BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id << ": expected " << vendor_id << ".json and a non-empty " - << vendor_id << " directory"; + << vendor_id << " directory, or OPC update format."; fs::remove_all(cached_vendor_folder, ec); fs::remove(cached_vendor_json, ec); return; From 7888452666c31bab80d1dd0675a642f2e41c5d31 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 10 Sep 2026 05:39:14 -0500 Subject: [PATCH 58/60] build: clear 7 warning categories across 26 sites (#15615) * build: clear 2 warnings - cast the NSTextField the class check already proved mainframe_text_field is NSTextField* and was assigned a bare NSView*, which Clang reports as -Wincompatible-pointer-types. Both assignments sit inside if ([viewObject class] == [NSTextField self]), so the runtime type is already guaranteed, and the line above the second one casts the same variable the same way to call setTextColor. macOS only, since nothing else compiles this file. * build: clear 6 warning categories from the clang-cl inventory -Wmissing-braces (9). Aggregates whose first member is itself an aggregate. GUID's fourth member is BYTE[8], so the trailing eight bytes take their own braces. The others were reaching for zero-initialization with {0} and say {} now. bbs_3mf's backup Task ends in an anonymous union, which needs braces of its own; those braces initialize the union's first member rather than the one named at the call site, so the RemoveBackup site says so in a comment. -Wmacro-redefined (11). SendMultiMachinePage.hpp defines five names that Preferences.hpp, PresetBundleDialog.hpp, ExportPresetBundleDialog.hpp and TroubleshootDialog.hpp also define with different values, so the value in force depended on include order. All nine of this file's DESIGN_ macros take the SEND_ prefix it already uses for its own macros, values unchanged, so a DESIGN_ name added elsewhere later cannot collide with it again. They read as one page-local palette, a 900 to 400 gray ramp plus sizes, so the four with no current readers stay: dropping them would leave gaps in a named scale. test_marchingsquares.cpp defines NOMINMAX, which libslic3r already passes as a PUBLIC compile definition, so it takes the #ifndef guard the other suites use. -Wbraced-scalar-init (3). Two PushStyleVar calls resolve to the float overload, so the braces were initializing a scalar. ConfigOptionFloatsNullable already takes an initializer_list, so the inner braces did the same thing. -Wmicrosoft-goto (2). Both gotos in copy_file_gui jump forward over the initialization of size, dwRead and dwWrite, which only MSVC accepts. Those declarations move up to join the others at the top of the function. -Wunused-private-field (3). Every use of ColourPicker's m_clrData and m_picker_widget is behind !defined(__linux__), so on Linux they are written and never read; the members now carry the same guard. ParamsPanel's m_size_move is read nowhere. Tab has its own, which is the one Tab.cpp uses. -Wnonportable-include-path (2). BaseException.h asked for "stackwalker.h" and the file on disk is StackWalker.h. --- src/dev-utils/BaseException.h | 2 +- src/dev-utils/StackWalker.cpp | 2 +- src/libslic3r/Format/bbs_3mf.cpp | 8 ++++---- src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/Field.hpp | 2 ++ src/slic3r/GUI/GUI_App.cpp | 2 +- src/slic3r/GUI/GUI_Utils.cpp | 5 +++-- .../GUI/Gizmos/GizmoObjectManipulation.cpp | 2 +- src/slic3r/GUI/IMSlider.cpp | 2 +- src/slic3r/GUI/MainFrame.cpp | 4 ++-- src/slic3r/GUI/ParamsPanel.hpp | 1 - src/slic3r/GUI/PartPlate.cpp | 2 +- src/slic3r/GUI/SendMultiMachinePage.cpp | 14 +++++++------- src/slic3r/GUI/SendMultiMachinePage.hpp | 18 +++++++++--------- src/slic3r/Utils/MacDarkMode.mm | 4 ++-- tests/libslic3r/test_marchingsquares.cpp | 2 ++ 16 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/dev-utils/BaseException.h b/src/dev-utils/BaseException.h index 2cb65d945e..20b6fb0c89 100644 --- a/src/dev-utils/BaseException.h +++ b/src/dev-utils/BaseException.h @@ -1,7 +1,7 @@ #pragma once #include #include -#include "stackwalker.h" +#include "StackWalker.h" #include class CBaseException : public CStackWalker diff --git a/src/dev-utils/StackWalker.cpp b/src/dev-utils/StackWalker.cpp index 6038196cb0..3ef983cd86 100644 --- a/src/dev-utils/StackWalker.cpp +++ b/src/dev-utils/StackWalker.cpp @@ -425,7 +425,7 @@ LPSTACKINFO CStackWalker::StackWalker(HANDLE hThread, const CONTEXT* context) else c = *context; - STACKFRAME64 sf = {0}; + STACKFRAME64 sf = {}; DWORD imageType; //intel X86 diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 96d98dbe9f..e2091da1db 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -8844,7 +8844,7 @@ public: auto model = object.get_model(); auto o = m_temp_model.add_object(object); int backup_id = model->get_object_backup_id(object); - push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 }); + push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } }); } void remove_object_mesh(ModelObject& object) { @@ -8854,7 +8854,7 @@ public: void backup_soon() { boost::lock_guard lock(m_mutex); m_other_changes_backup = true; - m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq }); + m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } }); m_cond.notify_all(); } @@ -8872,7 +8872,7 @@ public: m_ui_tasks.clear(); m_tasks.clear(); } - m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll }); + m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } }); ++m_task_seq; if (model.is_need_backup()) { m_other_changes = false; @@ -9087,7 +9087,7 @@ public: else m_cond.wait(lock); if (m_interval > 0 && boost::get_system_time() > m_next_backup) { - m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq }); + m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } }); m_next_backup += boost::posix_time::seconds(m_interval); // Maybe wakeup from power sleep if (m_next_backup < boost::get_system_time()) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1db476eede..4c27995ba0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5430,7 +5430,7 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->readonly = false; def->nullable = true; - def->set_default_value(new ConfigOptionFloatsNullable { {0.0} }); + def->set_default_value(new ConfigOptionFloatsNullable { 0.0 }); def = this->add("cooling_tube_retraction", coFloat); def->label = L("Cooling tube position"); diff --git a/src/slic3r/GUI/Field.hpp b/src/slic3r/GUI/Field.hpp index 5d5d549427..74011983c6 100644 --- a/src/slic3r/GUI/Field.hpp +++ b/src/slic3r/GUI/Field.hpp @@ -628,8 +628,10 @@ private: void on_button_click(wxCommandEvent &WXUNUSED(ev)); void save_colors_to_config(); private: +#if !defined(__linux__) && !defined(__LINUX__) wxColourData* m_clrData{nullptr}; wxColourPickerWidget* m_picker_widget{nullptr}; +#endif }; class PointCtrl : public Field { diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 99a829bdcc..6d08f052da 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -598,7 +598,7 @@ wxString file_wildcards(FileType file_type, const std::string &custom_extension) static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); } #ifdef WIN32 -static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; +static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; static void register_win32_device_notification_event() { diff --git a/src/slic3r/GUI/GUI_Utils.cpp b/src/slic3r/GUI/GUI_Utils.cpp index bc66d90ffd..10dd29c9c1 100644 --- a/src/slic3r/GUI/GUI_Utils.cpp +++ b/src/slic3r/GUI/GUI_Utils.cpp @@ -69,6 +69,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std HANDLE handlesrc = nullptr; HANDLE handledst = nullptr; CopyFileResult ret = SUCCESS; + DWORD size = 0; + DWORD dwRead = 0, dwWrite = 0; handlesrc = CreateFile(src.wc_str(), GENERIC_READ, @@ -96,9 +98,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std goto __finished; } - DWORD size=GetFileSize(handlesrc,NULL); + size = GetFileSize(handlesrc,NULL); buff = new char[size+1]; - DWORD dwRead=0,dwWrite; result = ReadFile(handlesrc, buff, size, &dwRead, NULL); if (!result) { DWORD errCode = GetLastError(); diff --git a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp index 7c4a2afd38..5d70fde833 100644 --- a/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp +++ b/src/slic3r/GUI/Gizmos/GizmoObjectManipulation.cpp @@ -702,7 +702,7 @@ bool GizmoObjectManipulation::reset_zero_button(ImGuiWrapper *imgui_wrapper, bo for (int i = 0; i < number; i++) { - char buf[3][64] = {0}; + char buf[3][64] = {}; float buf_size[3] = {0}; for (int j = 0; j < 3; j++) { ImGui::DataTypeFormatString(buf[j], IM_ARRAYSIZE(buf[j]), ImGuiDataType_Double, (void *) &vec[i][j], "%.2f"); diff --git a/src/slic3r/GUI/IMSlider.cpp b/src/slic3r/GUI/IMSlider.cpp index c008963646..0d0d6739f8 100644 --- a/src/slic3r/GUI/IMSlider.cpp +++ b/src/slic3r/GUI/IMSlider.cpp @@ -790,7 +790,7 @@ void IMSlider::draw_ticks(const ImRect& slideable_region) { void IMSlider::show_tooltip(const std::string tooltip) { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 6 * m_scale, 3 * m_scale }); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, { 3 * m_scale }); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * m_scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND); ImGui::PushStyleColor(ImGuiCol_Border, { 0,0,0,0 }); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f)); diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index 167b2b4cda..c734804c22 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1591,7 +1591,7 @@ void MainFrame::register_win32_callbacks() //static GUID GUID_DEVINTERFACE_USB_DEVICE = { 0xA5DCBF10, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED }; //static GUID GUID_DEVINTERFACE_DISK = { 0x53f56307, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b }; //static GUID GUID_DEVINTERFACE_VOLUME = { 0x71a27cdd, 0x812a, 0x11d0, 0xbe, 0xc7, 0x08, 0x00, 0x2b, 0xe2, 0x09, 0x2f }; - static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } }; // Register USB HID (Human Interface Devices) notifications to trigger the 3DConnexion enumeration. DEV_BROADCAST_DEVICEINTERFACE NotificationFilter = { 0 }; @@ -1631,7 +1631,7 @@ void MainFrame::register_win32_callbacks() { static constexpr int device_count = 1; - RAWINPUTDEVICE devices[device_count] = { 0 }; + RAWINPUTDEVICE devices[device_count] = {}; // multi-axis mouse (SpaceNavigator, etc.) devices[0].usUsagePage = 0x01; devices[0].usUsage = 0x08; diff --git a/src/slic3r/GUI/ParamsPanel.hpp b/src/slic3r/GUI/ParamsPanel.hpp index 0726db91d3..91bf3d2a7e 100644 --- a/src/slic3r/GUI/ParamsPanel.hpp +++ b/src/slic3r/GUI/ParamsPanel.hpp @@ -66,7 +66,6 @@ class ParamsPanel : public wxPanel { #if __WXOSX__ wxWindow* m_tmp_panel; - int m_size_move = -1; #endif // __WXOSX__ private: diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 90e2c96ab6..9826498fbd 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1112,7 +1112,7 @@ void PartPlate::show_tooltip(const std::string tooltip) { const auto scale = m_plater->get_current_canvas3D()->get_scale(); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, {3 * scale}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale); ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND); ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0}); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f)); diff --git a/src/slic3r/GUI/SendMultiMachinePage.cpp b/src/slic3r/GUI/SendMultiMachinePage.cpp index 3a52caec3f..2d1b713264 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.cpp +++ b/src/slic3r/GUI/SendMultiMachinePage.cpp @@ -814,13 +814,13 @@ wxBoxSizer* SendMultiMachinePage::create_item_title(wxString title, wxWindow* pa wxBoxSizer* m_sizer_title = new wxBoxSizer(wxHORIZONTAL); auto m_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); - m_title->SetForegroundColour(DESIGN_GRAY800_COLOR); + m_title->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR); m_title->SetFont(::Label::Head_13); m_title->Wrap(-1); m_title->SetToolTip(tooltip); auto m_line = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL); - m_line->SetBackgroundColour(DESIGN_GRAY400_COLOR); + m_line->SetBackgroundColour(SEND_DESIGN_GRAY400_COLOR); m_sizer_title->Add(m_title, 0, wxALIGN_CENTER | wxALL, 3); m_sizer_title->Add(0, 0, 0, wxLEFT, 9); @@ -843,7 +843,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_checkbox(wxString title, wxWindow* m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8); auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0); - checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + checkbox_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); checkbox_title->SetFont(::Label::Body_13); auto size = checkbox_title->GetTextExtent(title); @@ -867,12 +867,12 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin { wxBoxSizer* sizer_input = new wxBoxSizer(wxHORIZONTAL); auto input_title = new wxStaticText(parent, wxID_ANY, str_before); - input_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + input_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); input_title->SetFont(::Label::Body_13); input_title->SetToolTip(tooltip); input_title->Wrap(-1); - auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); + auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, SEND_DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER); StateColor input_bg(std::pair(wxColour("#F0F0F1"), StateColor::Disabled), std::pair(*wxWHITE, StateColor::Enabled)); input->SetBackgroundColor(input_bg); input->GetTextCtrl()->SetValue(app_config->get(param)); @@ -880,7 +880,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin input->GetTextCtrl()->SetValidator(validator); auto second_title = new wxStaticText(parent, wxID_ANY, str_after, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); - second_title->SetForegroundColour(DESIGN_GRAY900_COLOR); + second_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR); second_title->SetFont(::Label::Body_13); second_title->SetToolTip(tooltip); second_title->Wrap(-1); @@ -1337,7 +1337,7 @@ wxPanel* SendMultiMachinePage::create_page() m_tip_text->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1)); m_tip_text->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1)); m_tip_text->SetLabel(_L("Please select the devices you would like to manage here (up to 6 devices)")); - m_tip_text->SetForegroundColour(DESIGN_GRAY800_COLOR); + m_tip_text->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR); m_tip_text->SetFont(::Label::Head_20); m_tip_text->Wrap(-1); diff --git a/src/slic3r/GUI/SendMultiMachinePage.hpp b/src/slic3r/GUI/SendMultiMachinePage.hpp index 7d77849bf3..a63bc51bb0 100644 --- a/src/slic3r/GUI/SendMultiMachinePage.hpp +++ b/src/slic3r/GUI/SendMultiMachinePage.hpp @@ -22,15 +22,15 @@ namespace GUI { #define SEND_LEFT_DEV_STATUS 250 #define SEND_LEFT_TAKS_STATUS 180 -#define DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248) -#define DESIGN_GRAY900_COLOR wxColour(38, 46, 48) -#define DESIGN_GRAY800_COLOR wxColour(50, 58, 61) -#define DESIGN_GRAY600_COLOR wxColour(144, 144, 144) -#define DESIGN_GRAY400_COLOR wxColour(166, 169, 170) -#define DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1) -#define DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1) -#define DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1) -#define DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1) +#define SEND_DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248) +#define SEND_DESIGN_GRAY900_COLOR wxColour(38, 46, 48) +#define SEND_DESIGN_GRAY800_COLOR wxColour(50, 58, 61) +#define SEND_DESIGN_GRAY600_COLOR wxColour(144, 144, 144) +#define SEND_DESIGN_GRAY400_COLOR wxColour(166, 169, 170) +#define SEND_DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1) +#define SEND_DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1) +#define SEND_DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1) +#define SEND_DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1) diff --git a/src/slic3r/Utils/MacDarkMode.mm b/src/slic3r/Utils/MacDarkMode.mm index cecd90044b..2bce7835e8 100644 --- a/src/slic3r/Utils/MacDarkMode.mm +++ b/src/slic3r/Utils/MacDarkMode.mm @@ -57,7 +57,7 @@ void set_miniaturizable(void * window) while(viewObject = (NSView *)[viewEnum nextObject]) { if([viewObject class] == [NSTextField self]) { //[(NSTextField*)viewObject setTextColor : NSColor.whiteColor]; - mainframe_text_field = viewObject; + mainframe_text_field = (NSTextField*)viewObject; } } } @@ -74,7 +74,7 @@ void set_title_colour_after_set_title(void * window) while(viewObject = (NSView *)[viewEnum nextObject]) { if([viewObject class] == [NSTextField self]) { [(NSTextField*)viewObject setTextColor : NSColor.whiteColor]; - mainframe_text_field = viewObject; + mainframe_text_field = (NSTextField*)viewObject; } } diff --git a/tests/libslic3r/test_marchingsquares.cpp b/tests/libslic3r/test_marchingsquares.cpp index 6844ecb6ac..9a11f49faa 100644 --- a/tests/libslic3r/test_marchingsquares.cpp +++ b/tests/libslic3r/test_marchingsquares.cpp @@ -1,4 +1,6 @@ +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include "test_utils.hpp" From e8d35fadd45c537d578cedc4eacf548ad7a48920 Mon Sep 17 00:00:00 2001 From: Valerii Bokhan <80919135+valerii-bokhan@users.noreply.github.com> Date: Thu, 10 Sep 2026 13:03:50 +0200 Subject: [PATCH 59/60] Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill (#15206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill For patterns with curved/turning anchor lines (Hilbert Curve, Octagram Spiral), the bridge_over_infill algorithm produced incorrect results: 1. determine_bridging_angle: sampling curved anchor orientations produced noise across all turning directions (0/90/180/270°) instead of a single dominant one, yielding unstable bridge angles with 180° spread. Fix: use the configured infill_direction + 90° directly, bypassing the noisy sampling. The old blind +0.25*PI (Hilbert) and +1/16*PI (Octagram) offsets are removed. 2. construct_anchored_polygon: curved Hilbert/Octagram anchors intersected each vertical scan line many times at wildly different Y positions, producing chaotic polygon sections — holes in random places, bridges over air, rotated bridges. Fix: replace the curved infill polylines with synthetic straight lines parallel to infill_direction, spaced at the real infill line spacing (flow_spacing / density). Lines are centered on the limiting_area bbox center so that after rotation they span the full bridged_area. Anchors are left at full bbox length (not clipped) to guarantee every scan line finds an anchor. Rectilinear and other straight-line patterns are unaffected. Known limitation: some bridge edges may still terminate over air in edge cases where the nearest synthetic anchor line is more than one infill spacing away from the bridge boundary. This will be addressed in a follow-up. * fix: anchor internal bridges to actual sparse infill Preserve real anchors across regions and align plane-path anchor origins with printed infill. Respect lower-layer rotation templates and model alignment, and sample curved bridge boundaries more finely. Add regression coverage for anchor alignment, bridge angles and region isolation, with Orca comments explaining the geometry constraints. Verified 175 FFF tests before the comment-only follow-up; preserve CRLF in modified files. * Fix internal bridge support contacts and separated infill origins Restore anchor contact after bridge smoothing and share per-body pattern origins between anchors and printed infill. Recompute origins when preparation settings change. Cover multiline counts 1, 2 and 3 and add regressions for printed bridge support, separated infill alignment and reslicing. * Add explicit standard headers to PrintObject tests * test: cover surface centering when infill settings change Verify top and bottom Archimedean Chords and Octagram Spiral paths after switching centering modes or toggling separated infills. Compare reslicing against fresh slicing and document dependent infill invalidation. * test: preserve directional surface infill when settings change * perf: index layer islands for connected-body detection * test: use public print pipeline for body centering checks --- src/libslic3r/Fill/Fill.cpp | 75 +++-- src/libslic3r/Fill/Fill.hpp | 6 + src/libslic3r/PrintObject.cpp | 304 +++++++++++------- tests/fff_print/test_fill.cpp | 66 ++++ tests/fff_print/test_printobject.cpp | 442 +++++++++++++++++++++++++++ 5 files changed, 736 insertions(+), 157 deletions(-) diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index dc772580ca..f5386b085c 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -11,7 +11,7 @@ #include "AABBTreeLines.hpp" #include "ExtrusionEntity.hpp" -#include "FillBase.hpp" +#include "Fill.hpp" #include "FillRectilinear.hpp" #include "FillLightning.hpp" #include "FillConcentricInternal.hpp" @@ -1234,6 +1234,33 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p return surface_fills; } +// Orca: Anchors and printed infill must share the same body origin. Keep the choice +// here so per-model surface centering and separated sparse infill cannot drift apart. +static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox) +{ + const auto ¶ms = fill.params; + const auto &config = layer.regions()[fill.region_id]->region().config(); + const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; + const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && + (params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral); + const bool separate = !external && params.separated_infills && + (is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() || + !config.sparse_infill_rotate_template.value.empty()); + if (per_model || separate) { + double best_overlap = 0.; + for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) { + const double overlap = area(intersection_ex(layer.lslices[i], expoly)); + if (overlap > best_overlap) { + best_overlap = overlap; + const Point center = layer.lslices_separated_component_bboxes[i].center(); + bbox = layer.object()->bounding_box(); + bbox.translate(center.x(), center.y()); + } + } + } + return bbox; +} + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING void export_group_fills_to_svg(const char *path, const std::vector &fills) { @@ -1353,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // Orca: Checking the filling of a centered surface by drawing for each model parts bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; - bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral; if (is_top_or_bottom) { params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern } - // Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly - // fall through to the default (whole-object) bounding box below. - bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill; - bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills && - ( - is_separable_infill_pattern(surface_fill.params.pattern) || - params.config->solid_infill_rotate_template != "" || - params.config->sparse_infill_rotate_template != "" ); - if( surface_fill.params.pattern == ipLockedZag ) { params.locked_zag = true; params.infill_lock_depth = surface_fill.params.infill_lock_depth; @@ -1389,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.can_reverse = false; for (ExPolygon& expoly : surface_fill.expolygons) { - // Orca: separate infill / per-model pattern centering. - // - // Center the pattern on each connected body of the object independently, so every piece - // is filled exactly as if it were sliced on its own: touching/overlapping parts merge - // into one body sharing a center, while separate parts and disconnected islands (even - // interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each - // island belongs to, and its full bounding box, were resolved in 3D by PrintObject:: - // infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We - // match this fill region to the island it overlaps most, then re-use the whole-object - // bounding box (origin-centered — identical extent to the default, so coverage and cost - // are unchanged) re-centered on that body. - if (is_per_model_center || is_separate_infill) { - double best_overlap = 0.; - BoundingBox best_component; - for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) { - const double overlap = area(intersection_ex(this->lslices[r], expoly)); - if (overlap > best_overlap) { - best_overlap = overlap; - best_component = this->lslices_separated_component_bboxes[r]; - } - } - if (best_component.defined) { - const Point c = best_component.center(); - BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above) - part_bbox.translate(c.x(), c.y()); // re-center on this body - f->set_bounding_box(part_bbox); - } - } // - End: separate infill / per-model pattern centering + // Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface. + f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { @@ -1583,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc params.multiline = surface_fill.params.multiline; params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.smooth_factor = surface_fill.params.smooth_factor; + // Orca: Match make_fills() when choosing the origin of plane-path patterns. + // Without the sparse extrusion role, the filler uses each surface's bounds + // instead of the object's bounds, so bridge anchors shift away from printed infill. + params.extrusion_role = surface_fill.params.extrusion_role; for (ExPolygon &expoly : surface_fill.expolygons) { + // Orca: Match the per-body origin of make_fills() before generating physical anchors. + f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); // Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon. f->spacing = surface_fill.params.spacing; surface_fill.surface.expolygon = std::move(expoly); diff --git a/src/libslic3r/Fill/Fill.hpp b/src/libslic3r/Fill/Fill.hpp index e92ab2dee5..b183cf0253 100644 --- a/src/libslic3r/Fill/Fill.hpp +++ b/src/libslic3r/Fill/Fill.hpp @@ -14,6 +14,12 @@ namespace Slic3r { class ExtrusionEntityCollection; class LayerRegion; +class PrintObject; + +// Orca: Share the layer rotation calculation between infill generation and internal +// bridge angle selection so both interpret rotation templates in the same way. +double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id, + const double &fixed_infill_angle, const std::string &template_string); // An interface class to Perl, aggregating an instance of a Fill and a FillData. class Filler diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index a228bb7436..e147356ea6 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -21,9 +21,11 @@ #include "TriangleMeshSlicer.hpp" #include "Utils.hpp" #include "Fill/FillAdaptive.hpp" +#include "Fill/Fill.hpp" #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" #include "format.hpp" +#include "AABBTreeIndirect.hpp" #include "AABBTreeLines.hpp" #include @@ -672,6 +674,98 @@ void PrintObject::prepare_infill() } // for each region #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ + // Orca: precompute the object's 3D connected bodies for separated infills / per-model + // centering. Two islands belong to the same body when their slices overlap on adjacent + // layers; islands that only overlap in top-down projection but never touch (e.g. interleaved + // chain links) stay separate, matching "split to objects". Each layer island then records + // the full bounding box of its body, so its infill is centered on that body as if it were + // sliced alone. Compute this before bridges so anchors and extrusion share the same origin. + bool needs_separated_components = false; + for (size_t i = 0; i < this->num_printing_regions(); ++ i) { + const PrintRegionConfig &rc = this->printing_region(i).config(); + if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) { + needs_separated_components = true; + break; + } + } + // Orca: Fast path: the feature only changes anything when the object is made of more than one + // connected body. Detect that cheaply the same way as "Split to objects" — more than one + // model part, or a single part whose mesh is splittable (is_splittable() is cached). A single + // body already shares the object center, i.e. the default, so skip the connectivity pass. + if (needs_separated_components) { + int parts = 0; + const ModelVolume *first_part = nullptr; + for (const ModelVolume *v : this->model_object()->volumes) + if (v->is_model_part()) { ++ parts; first_part = v; } + if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) + needs_separated_components = false; + } + for (Layer *layer : m_layers) + layer->lslices_separated_component_bboxes.clear(); + if (needs_separated_components) { + const size_t nl = m_layers.size(); + std::vector offset(nl + 1, 0); // Orca: flat index of the first island of each layer + for (size_t i = 0; i < nl; ++ i) + offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); + const size_t nreg = offset[nl]; + // Orca: Union-find over every (layer, island). + std::vector parent(nreg); + for (size_t i = 0; i < nreg; ++ i) parent[i] = i; + auto find = [&parent](size_t x) { + while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } + return x; + }; + auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; + // Orca: Index the smaller of two consecutive layers instead of scanning every + // pair of islands. The tree prunes distant boxes on fragmented models; exact + // polygon intersections still decide connectivity for the remaining candidates. + for (size_t i = 0; i + 1 < nl; ++ i) { + m_print->throw_if_canceled(); + size_t layer_a = i, layer_b = i + 1; + if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size()) + std::swap(layer_a, layer_b); + const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b]; + if (lb->lslices.empty()) + continue; + + using IslandTree = AABBTreeIndirect::Tree<2, coord_t>; + std::vector bboxes; + bboxes.reserve(lb->lslices.size()); + for (size_t b = 0; b < lb->lslices.size(); ++ b) + bboxes.emplace_back(b, lb->lslices_bboxes[b]); + IslandTree tree; + tree.build_modify_input(bboxes); + for (size_t a = 0; a < la->lslices.size(); ++ a) { + const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max); + AABBTreeIndirect::traverse(tree, + [&query](const IslandTree::Node &node) { return node.bbox.intersects(query); }, + [&](const IslandTree::Node &node) { + const size_t b = node.idx; + // Orca: Tree boxes include an epsilon, so retain the original box + // filter. Already-connected islands cannot change the partition + // and need no further polygon intersection. + if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && + find(offset[layer_a] + a) != find(offset[layer_b] + b) && + ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) + unite(offset[layer_a] + a, offset[layer_b] + b); + return true; + }); + } + } + // Orca: Full bounding box of each body, indexed by its union-find root. + std::vector body_bbox(nreg); + for (size_t i = 0; i < nl; ++ i) + for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a) + body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]); + // Orca: Store the body bbox for every island. + for (size_t i = 0; i < nl; ++ i) { + Layer *layer = m_layers[i]; + layer->lslices_separated_component_bboxes.resize(layer->lslices.size()); + for (size_t a = 0; a < layer->lslices.size(); ++ a) + layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)]; + } + } + // the following step needs to be done before combination because it may need // to remove only half of the combined infill this->bridge_over_infill(); @@ -706,71 +800,6 @@ void PrintObject::infill() if (this->set_started(posInfill)) { m_print->set_status(35, L("Generating infill toolpath")); - // Orca: precompute the object's 3D connected bodies for separated infills / per-model - // centering. Two islands belong to the same body when their slices overlap on adjacent - // layers; islands that only overlap in top-down projection but never touch (e.g. interleaved - // chain links) stay separate, matching "split to objects". Each layer island then records - // the full bounding box of its body, so its infill is centered on that body as if it were - // sliced alone. Done once here, before the parallel fill, and only when a region needs it. - bool needs_separated_components = false; - for (size_t i = 0; i < this->num_printing_regions(); ++ i) { - const PrintRegionConfig &rc = this->printing_region(i).config(); - if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) { - needs_separated_components = true; - break; - } - } - // Fast path: the feature only changes anything when the object is made of more than one - // connected body. Detect that cheaply the same way as "Split to objects" — more than one - // model part, or a single part whose mesh is splittable (is_splittable() is cached). A single - // body already shares the object center, i.e. the default, so skip the connectivity pass. - if (needs_separated_components) { - int parts = 0; - const ModelVolume *first_part = nullptr; - for (const ModelVolume *v : this->model_object()->volumes) - if (v->is_model_part()) { ++ parts; first_part = v; } - if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) - needs_separated_components = false; - } - for (Layer *layer : m_layers) - layer->lslices_separated_component_bboxes.clear(); - if (needs_separated_components) { - const size_t nl = m_layers.size(); - std::vector offset(nl + 1, 0); // flat index of the first island of each layer - for (size_t i = 0; i < nl; ++ i) - offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); - const size_t nreg = offset[nl]; - // Union-find over every (layer, island). - std::vector parent(nreg); - for (size_t i = 0; i < nreg; ++ i) parent[i] = i; - auto find = [&parent](size_t x) { - while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } - return x; - }; - auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; - // Join islands that overlap between two consecutive layers. - for (size_t i = 0; i + 1 < nl; ++ i) { - const Layer *la = m_layers[i], *lb = m_layers[i + 1]; - for (size_t a = 0; a < la->lslices.size(); ++ a) - for (size_t b = 0; b < lb->lslices.size(); ++ b) - if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && - ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) - unite(offset[i] + a, offset[i + 1] + b); - } - // Full bounding box of each body, indexed by its union-find root. - std::vector body_bbox(nreg); - for (size_t i = 0; i < nl; ++ i) - for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a) - body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]); - // Store the body bbox for every island. - for (size_t i = 0; i < nl; ++ i) { - Layer *layer = m_layers[i]; - layer->lslices_separated_component_bboxes.resize(layer->lslices.size()); - for (size_t a = 0; a < layer->lslices.size(); ++ a) - layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)]; - } - } - const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first; const auto& support_fill_octree = this->m_adaptive_fill_octrees.second; @@ -1401,8 +1430,6 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_anchor_max" || opt_key == "top_surface_line_width" || opt_key == "bottom_surface_density" - || opt_key == "center_of_surface_pattern" - || opt_key == "separated_infills" || opt_key == "initial_layer_line_width" || opt_key == "small_area_infill_flow_compensation" || opt_key == "lateral_lattice_angle_1" @@ -1410,6 +1437,10 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "infill_overhang_angle") { steps.emplace_back(posInfill); } else if (opt_key == "sparse_infill_pattern" + // Orca: Body centering now also determines bridge anchors during preparation. + // Invalidating preparation also invalidates infill, including top/bottom surfaces. + || opt_key == "center_of_surface_pattern" + || opt_key == "separated_infills" || opt_key == "sparse_infill_smooth_factor" || opt_key == "symmetric_infill_y_axis" || opt_key == "infill_shift_step" @@ -3009,21 +3040,12 @@ void PrintObject::bridge_over_infill() return diff(layers_sparse_infill, not_sparse_infill); }; - // LAMBDA do determine optimal bridging angle - auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors, InfillPattern dominant_pattern, double infill_direction) { + // Orca: Derive the fallback bridge direction from the supplied anchor geometry. + // Pattern-specific angle selection belongs at the call site, where the supporting + // layer and region are known; this helper must not override it with a base config angle. + auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors) { AABBTreeLines::LinesDistancer lines_tree(anchors); - // Orca: since 3D Honeycomb was "fixed" by forcing coordf_t layerHeight = scale_(1.0), this is no longer needed. - // CorssHatch also does not need fixed angle. - // - // Check it the infill that require a fixed infill angle. - //switch (dominant_pattern) { - //case ip3DHoneycomb: - //case ipCrossHatch: - // return (infill_direction + 45.0) * 2.0 * M_PI / 360.; - //default: break; - //} - std::map counted_directions; for (const Polygon &p : bridged_area) { double acc_distance = 0; @@ -3089,18 +3111,15 @@ void PrintObject::bridge_over_infill() if (bridging_angle == 0) { bridging_angle = 0.001; } - switch (dominant_pattern) { - case ipHilbertCurve: bridging_angle += 0.25 * PI; break; - case ipOctagramSpiral: bridging_angle += (1.0 / 16.0) * PI; break; - default: break; - } return bridging_angle; }; - // LAMBDA that will fill given polygons with lines, exapand the lines to the nearest anchor, and reconstruct polygons from the newly - // generated lines - auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle) { + // Orca: Extend scan sections to the nearest anchors and reconstruct the bridge area. + // scan_spacing controls boundary sampling independently of the extrusion spacing; + // anchoring overlap and smoothing thresholds still use the physical bridging flow. + auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle, + coord_t scan_spacing, bool restore_anchors = false) { auto lines_rotate = [](Lines &lines, double cos_angle, double sin_angle) { for (Line &l : lines) { double ax = double(l.a.x()); @@ -3127,12 +3146,12 @@ void PrintObject::bridge_over_infill() BoundingBox bb_x = get_extents(bridged_area); BoundingBox bb_y = get_extents(anchors); - const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + bridging_flow.scaled_spacing() - 1) / bridging_flow.scaled_spacing(); + const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + scan_spacing - 1) / scan_spacing; std::vector vertical_lines(n_vlines); for (size_t i = 0; i < n_vlines; i++) { - // Orca: Make sure the line is placed in the middle of the extrusion - // coord_t x = bb_x.min.x() + i * bridging_flow.scaled_spacing(); - coord_t x = bb_x.min.x() + (i + 0.5) * bridging_flow.scaled_spacing(); + // Orca: Sample the center of each reconstructed strip. Its edges lie + // half a scan step away, even when the sampling is finer than extrusion. + coord_t x = bb_x.min.x() + (i + 0.5) * scan_spacing; coord_t y_min = bb_y.min.y() - bridging_flow.scaled_spacing(); coord_t y_max = bb_y.max.y() + bridging_flow.scaled_spacing(); vertical_lines[i].a = Point{x, y_min}; @@ -3155,7 +3174,11 @@ void PrintObject::bridge_over_infill() auto anchors_intersections = anchors_and_walls_tree.intersections_with_line(vertical_lines[i]); for (Line §ion : polygon_sections[i]) { - auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a, + // Orca: A repaired boundary may already overlap its anchor by one flow width. + // Include that overlap in the search so restoring rounded corners does not + // extend every already anchored section into the next sparse infill cell. + const coord_t overlap = restore_anchors ? bridging_flow.scaled_width() + SCALED_EPSILON : 0; + auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a + Point{0, overlap}, [](const Point &a, const std::pair &b) { return a.y() > b.first.y(); }); @@ -3164,7 +3187,7 @@ void PrintObject::bridge_over_infill() section.a.y() -= bridging_flow.scaled_width() * (0.5 + 0.5); } - auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b, + auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b - Point{0, overlap}, [](const Point &a, const std::pair &b) { return a.y() < b.first.y(); }); @@ -3194,7 +3217,9 @@ void PrintObject::bridge_over_infill() }); } - // reconstruct polygon from polygon sections + // Orca: Reconstruct the polygon from scan sections. At discontinuities and + // strip starts/ends, use half the scan step for the X offsets; using half an + // extrusion spacing would overlap the finer strips and distort curved anchors. struct TracedPoly { Points lows; @@ -3220,8 +3245,8 @@ void PrintObject::bridge_over_infill() 36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) { traced_poly.lows.push_back(candidate->a); } else { - traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.lows.push_back(candidate->a - Point{bridging_flow.scaled_spacing() / 2, 0}); + traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0}); + traced_poly.lows.push_back(candidate->a - Point{scan_spacing / 2, 0}); traced_poly.lows.push_back(candidate->a); } @@ -3229,8 +3254,8 @@ void PrintObject::bridge_over_infill() 36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) { traced_poly.highs.push_back(candidate->b); } else { - traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.highs.push_back(candidate->b - Point{bridging_flow.scaled_spacing() / 2, 0}); + traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0}); + traced_poly.highs.push_back(candidate->b - Point{scan_spacing / 2, 0}); traced_poly.highs.push_back(candidate->b); } segment_added = true; @@ -3238,9 +3263,9 @@ void PrintObject::bridge_over_infill() } if (!segment_added) { - // Zero overlapping segments, we just close this polygon - traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); - traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0}); + // Orca: No section continues this strip; close at its right edge. + traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0}); + traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0}); Polygon &new_poly = expanded_bridged_area.emplace_back(std::move(traced_poly.lows)); new_poly.points.insert(new_poly.points.end(), traced_poly.highs.rbegin(), traced_poly.highs.rend()); traced_poly.lows.clear(); @@ -3255,9 +3280,9 @@ void PrintObject::bridge_over_infill() for (const auto &segment : polygon_slice) { if (used_segments.find(&segment) == used_segments.end()) { TracedPoly &new_tp = current_traced_polys.emplace_back(); - new_tp.lows.push_back(segment.a - Point{bridging_flow.scaled_spacing() / 2, 0}); + new_tp.lows.push_back(segment.a - Point{scan_spacing / 2, 0}); new_tp.lows.push_back(segment.a); - new_tp.highs.push_back(segment.b - Point{bridging_flow.scaled_spacing() / 2, 0}); + new_tp.highs.push_back(segment.b - Point{scan_spacing / 2, 0}); new_tp.highs.push_back(segment.b); } } @@ -3364,7 +3389,10 @@ void PrintObject::bridge_over_infill() total_fill_area = closing(total_fill_area, float(SCALED_EPSILON)); expansion_area = closing(expansion_area, float(SCALED_EPSILON)); expansion_area = intersection(expansion_area, deep_infill_area); - Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing)); + // Orca: Preserve the real lower-layer anchors for every candidate in this + // layer. Replacing this shared set for one pattern also changes later regions, + // and synthetic straight lines can claim support where no infill is printed. + const Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing)); Polygons internal_unsupported_area = shrink(deep_infill_area, spacing * 4.5); #ifdef DEBUG_BRIDGE_OVER_INFILL @@ -3375,6 +3403,9 @@ void PrintObject::bridge_over_infill() std::vector expanded_surfaces; expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) { + const auto ®ion_config = candidate.region->region().config(); + const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || + region_config.sparse_infill_pattern == ipOctagramSpiral; const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing()); area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area); @@ -3403,20 +3434,40 @@ void PrintObject::bridge_over_infill() to_lines(area_to_be_bridge), to_lines(boundary_plines), to_lines(anchors), to_lines(expansion_area)); #endif - double bridging_angle = 0; - if (!anchors.empty()) { - bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors), - candidate.region->region().config().sparse_infill_pattern.value, - candidate.region->region().config().infill_direction.value); - } else { - // use expansion boundaries as anchors. - // Also, use Infill pattern that is neutral for angle determination, since there are no infill lines. - bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(boundary_plines), InfillPattern::ipLine, 0); + double bridging_angle = -1.; + if (!anchors.empty() && turning_pattern) { + // Orca: Keep adjacent bridges over Hilbert/Octagram aligned despite + // their many local turning directions. Use the lower layer's rotation, + // since that is the infill supporting the bridge, not the current layer's. + for (const LayerRegion *lower_region : layer->lower_layer->regions()) { + // Orca: Apply the configured direction only if the same region has + // sparse infill below this bridge. A height modifier may put another + // pattern underneath, requiring the geometry-based fallback below. + if (&lower_region->region() != &candidate.region->region() || + intersection(area_to_be_bridge, to_polygons(lower_region->fill_surfaces.filter_by_type(stInternal))).empty()) + continue; + bridging_angle = calculate_infill_rotation_angle(po, layer->lower_layer->id(), region_config.infill_direction.value, + region_config.sparse_infill_rotate_template.value) + 0.5 * PI; + // Orca: Apply model alignment as infill generation does, then normalize + // the undirected bridge angle to [0, PI), including negative rotations. + if (region_config.align_infill_direction_to_model) { + const auto &m = po->trafo().matrix(); + bridging_angle += std::atan2(double(m(1, 0)), double(m(0, 0))); + } + bridging_angle = std::fmod(bridging_angle, PI); + if (bridging_angle < 0.) + bridging_angle += PI; + break; + } } + // Orca: A different region below (e.g. a height modifier) needs the actual anchor + // directions. When there are no sparse anchors, use the expansion boundaries. + if (bridging_angle < 0.) + bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors.empty() ? boundary_plines : anchors)); - // ORCA: Internal bridge angle override + // Orca: Preserve the user's absolute or relative internal bridge angle + // override after automatic direction selection. if (candidate.region->region().config().internal_bridge_angle.value > 0) { - const auto ®ion_config = candidate.region->region().config(); const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value); if (region_config.relative_bridge_angle.value) bridging_angle += custom_angle_rad; @@ -3429,11 +3480,19 @@ void PrintObject::bridge_over_infill() } } + // Orca: Changing the bridge direction must not change its physical supports. + // Extend to actual sparse infill or the existing boundary anchors, never to + // a synthetic grid that merely has the same nominal angle and spacing. boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end()); if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) { boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10))); } - Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle); + // Orca: Use four samples per extrusion spacing for Hilbert/Octagram so the + // reconstructed boundary follows rounded anchors instead of cutting corners. + // Keep the original step for other patterns and at least one coordinate unit + // after integer division. This changes boundary accuracy, not infill density. + const coord_t scan_spacing = std::max(coord_t(1), flow.scaled_spacing() / (turning_pattern ? 4 : 1)); + Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing); // Check collision with other expanded surfaces { @@ -3447,7 +3506,9 @@ void PrintObject::bridge_over_infill() } } if (reconstruct) { - bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle); + // Orca: Retain the same sampling accuracy when matching a nearby + // bridge's direction; rebuilding must not lose the curved supports. + bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing); } } @@ -3455,6 +3516,13 @@ void PrintObject::bridge_over_infill() // bridging_area = opening(bridging_area, flow.scaled_spacing()); bridging_area = opening(bridging_area, flow.scaled_spacing() * 0.75); bridging_area = closing(bridging_area, flow.scaled_spacing()); + // Orca: Opening/closing can pull rounded bridge ends away from their real + // supports. Restore those contacts after smoothing, preserving the cleaned + // area and the selected angle; do not smooth the restored contacts again. + if (turning_pattern && !bridging_area.empty()) { + bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, + bridging_angle, scan_spacing, true)); + } bridging_area = intersection(bridging_area, limiting_area); bridging_area = intersection(bridging_area, total_fill_area); bridging_area = diff(bridging_area, total_top_area); diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 04e5b61831..aa81570e56 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -9,6 +9,7 @@ #include #include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" @@ -1229,3 +1230,68 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", " REQUIRE(smooth.point_count > sharp.point_count); REQUIRE(smooth.sharp_turns < sharp.sharp_turns); } + +TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][InternalBridge][Regression]") +{ + // Orca: Compare generated anchors with actual extrusion across plane-path patterns, + // smoothing, multiline and rotations; an origin shift must not pass as valid support. + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords"); + const std::string smoothing = GENERATE("0%", "100%"); + const int multiline = GENERATE(1, 2); + const bool rotated = GENERATE(false, true); + const bool separated = GENERATE(false, true); + CAPTURE(pattern, smoothing, multiline, rotated, separated); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", smoothing}, + {"fill_multiline", multiline}, + {"infill_direction", 45}, + {"sparse_infill_rotate_template", rotated ? "0,25,50" : ""}, + {"align_infill_direction_to_model", rotated}, + {"separated_infills", separated}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"resolution", 0.012}}); + Print print; + Model model; + TriangleMesh mesh = make_cube(30, 24, 1); + if (separated) { + // Orca: Two disconnected bodies in one object must each use their own infill origin. + TriangleMesh second = make_cube(30, 24, 1); + second.translate(50, 0, 0); + mesh.merge(second); + } + Slic3r::Test::init_print({mesh}, print, model, config, nullptr, false); + if (rotated) { + model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(23.))); + print.apply(model, config); + } + print.process(); + + const Layer &layer = *print.objects().front()->get_layer(4); + Polylines printed; + for (const LayerRegion *region : layer.regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == erInternalInfill) + entity->collect_polylines(printed); + REQUIRE_FALSE(printed.empty()); + const AABBTreeLines::LinesDistancer printed_tree(to_lines(printed)); + + // Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently. + const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr), + shrink(to_polygons(layer.lslices), scale_(3.))); + REQUIRE_FALSE(anchors.empty()); + double max_distance = 0.; + for (const Polyline &path : anchors) + for (const Point &point : path.equally_spaced_points(scale_(0.25))) + max_distance = std::max(max_distance, printed_tree.distance_from_lines(point)); + // Orca: Allow only the configured simplification tolerance; infill-scale offsets + // would hide anchors that no longer coincide with printed lines. + CHECK(unscale(max_distance) <= config.opt_float("resolution")); +} diff --git a/tests/fff_print/test_printobject.cpp b/tests/fff_print/test_printobject.cpp index fb7fe2c1fd..a373a1ad39 100644 --- a/tests/fff_print/test_printobject.cpp +++ b/tests/fff_print/test_printobject.cpp @@ -4,11 +4,18 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/GCodeReader.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/AABBTreeLines.hpp" #include "test_helpers.hpp" +#include #include +#include #include +#include +#include +#include using namespace Slic3r; using namespace Slic3r::Test; @@ -130,3 +137,438 @@ TEST_CASE("Initial layer height is honored", "[PrintObject]") REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4)); REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4)); } + +static TriangleMesh internal_bridge_step() +{ + // Orca: The smaller tower leaves a shoulder whose solid skin needs internal bridges + // over the sparse infill in the base, without relying on an external model file. + TriangleMesh mesh = make_cube(30, 24, 3); + TriangleMesh tower = make_cube(14, 10, 1); + tower.translate(8, 7, 3); + mesh.merge(tower); + return mesh; +} + +static DynamicPrintConfig internal_bridge_config(const std::string &pattern, int multiline) +{ + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"fill_multiline", multiline}, + {"sparse_infill_density", "15%"}, + {"sparse_infill_smooth_factor", "100%"}, + {"infill_direction", 45}, + {"internal_bridge_angle", 0}, + {"thick_internal_bridges", true}, + {"top_shell_layers", 3}, + {"bottom_shell_layers", 2}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}}); + return config; +} + +TEST_CASE("Internal bridge angles follow the lower infill layer and model rotation", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral"); + // Orca: Cover both a central line (odd counts) and offset pairs (even counts). + const int multiline = GENERATE(1, 2, 3); + CAPTURE(multiline); + const double rotation = GENERATE(23., -123.); + const std::vector cycle{10., 30., 70.}; + auto config = internal_bridge_config(pattern, multiline); + config.set_deserialize_strict({{"sparse_infill_rotate_template", "10,30,70"}, + {"align_infill_direction_to_model", true}, + {"separated_infills", false}}); + Print print; + Model model; + init_print({internal_bridge_step()}, print, model, config, nullptr, false); + model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(rotation))); + print.apply(model, config); + print.process(); + const PrintObject &object = *print.objects().front(); + size_t bridges = 0; + for (size_t i = 1; i < object.layer_count(); ++i) { + // Orca: The support is one layer below the bridge. Check the template and model + // rotation together, including normalization when the resulting angle is negative. + double expected = std::fmod(cycle[(i - 1) % cycle.size()] + 90. + rotation, 180.); + if (expected < 0.) expected += 180.; + for (const LayerRegion *region : object.get_layer(i)->regions()) + for (const Surface *surface : region->fill_surfaces.filter_by_type(stInternalBridge)) { + CAPTURE(pattern, rotation, i); + CHECK_THAT(Geometry::rad2deg(surface->bridge_angle), Catch::Matchers::WithinAbs(expected, 0.001)); + ++bridges; + } + } + REQUIRE(bridges > 0); +} + +TEST_CASE("Turning infill does not replace the anchors of another region", "[PrintObject][InternalBridge][Regression]") +{ + // Orca: Keep the right-hand region fixed while changing the left-hand pattern in the + // same object. Its bridge areas must be independent of a previous candidate's anchors. + const int multiline = GENERATE(1, 2, 3); + CAPTURE(multiline); + auto right_bridges = [multiline](const std::string &left_pattern) { + auto config = internal_bridge_config(left_pattern, multiline); + Print print; + Model model; + init_print({internal_bridge_step()}, print, model, config, nullptr, false); + TriangleMesh right = internal_bridge_step(); + right.translate(50, 0, 0); + ModelVolume *volume = model.objects.front()->add_volume(std::move(right)); + volume->config.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + volume->config.set_key_value("infill_direction", new ConfigOptionFloat(17.)); + print.apply(model, config); + print.process(); + std::map result; + const PrintObject &object = *print.objects().front(); + for (size_t i = 0; i < object.layer_count(); ++i) + for (const LayerRegion *region : object.get_layer(i)->regions()) + if (region->region().config().infill_direction == 17.) + polygons_append(result[i], to_polygons(region->fill_surfaces.filter_by_type(stInternalBridge))); + return result; + }; + const auto baseline = right_bridges("rectilinear"); + const auto actual = right_bridges(GENERATE("hilbertcurve", "octagramspiral")); + REQUIRE(actual.size() == baseline.size()); + double total_area = 0.; + for (const auto &[layer, expected] : baseline) { + CAPTURE(layer); + const auto &polys = actual.at(layer); + CHECK(area(diff(expected, polys)) < scaled(1.) * scaled(1.) * 1e-6); + CHECK(area(diff(polys, expected)) < scaled(1.) * scaled(1.) * 1e-6); + total_area += area(expected); + } + REQUIRE(total_area > 0.); +} + +TEST_CASE("Rounded internal bridges end on printed support", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral"); + const bool separated = GENERATE(false, true); + CAPTURE(pattern, separated); + auto config = internal_bridge_config(pattern, 1); + config.set_deserialize_strict({{"infill_wall_overlap", "0%"}, {"separated_infills", separated}}); + TriangleMesh mesh = internal_bridge_step(); + if (separated) { + TriangleMesh second = internal_bridge_step(); + second.translate(50, 0, 0); + mesh.merge(second); + } + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + + // Orca: Check final extrusion endpoints after polygon cleanup and fill generation. + // A correct bridge angle and correct sparse anchors alone do not guarantee contact. + const PrintObject &object = *print.objects().front(); + size_t checked = 0; + for (size_t i = 1; i < object.layer_count(); ++i) { + Polygons support; + Polylines walls; + for (const LayerRegion *region : object.get_layer(i - 1)->regions()) { + region->perimeters.polygons_covered_by_width(support, 0.f); + region->fills.polygons_covered_by_width(support, 0.f); + region->perimeters.collect_polylines(walls); + } + REQUIRE_FALSE(support.empty()); + const AABBTreeLines::LinesDistancer support_tree(to_lines(union_(support))); + const AABBTreeLines::LinesDistancer wall_tree(to_lines(walls)); + for (const LayerRegion *region : object.get_layer(i)->regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (entity->role() != erInternalBridgeInfill) + continue; + const auto *path = dynamic_cast(entity); + REQUIRE(path != nullptr); + for (const Line &line : path->polyline.to_polyline().lines()) { + // Orca: Sample span ends, excluding short connectors and wall overlap. + if (line.length() < scale_(std::max(0.7, 3. * path->width))) + continue; + for (const Point &point : {line.a, line.b}) { + if (wall_tree.distance_from_lines(point) <= scale_(0.5)) + continue; + CAPTURE(i, point.x(), point.y()); + const double gap = unscale(support_tree.distance_from_lines(point)) - 0.5 * path->width; + CHECK(gap <= 0.1); + ++checked; + } + } + } + } + REQUIRE(checked > 0); +} + +TEST_CASE("Enabling separated infill recomputes body origins", "[PrintObject][InternalBridge][Regression]") +{ + const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords"); + CAPTURE(pattern); + auto footprint = [&](bool reslice) { + auto config = internal_bridge_config(pattern, 2); + config.set_deserialize_strict({{"separated_infills", !reslice}}); + TriangleMesh mesh = internal_bridge_step(); + TriangleMesh second = internal_bridge_step(); + second.translate(50, 0, 0); + mesh.merge(second); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + if (reslice) { + // Orca: Enabling centering after a completed slice must rebuild the body + // origins now shared by bridge preparation and printed infill. + config.set_deserialize_strict({{"separated_infills", true}}); + print.apply(model, config); + print.process(); + } + Polygons result; + for (const LayerRegion *region : print.objects().front()->get_layer(4)->regions()) + region->fills.polygons_covered_by_width(result, 0.f); + return union_(result); + }; + const Polygons fresh = footprint(false); + const Polygons resliced = footprint(true); + REQUIRE_FALSE(fresh.empty()); + CHECK(area(diff(fresh, resliced)) < scaled(1.) * scaled(1.) * 1e-6); + CHECK(area(diff(resliced, fresh)) < scaled(1.) * scaled(1.) * 1e-6); +} + +TEST_CASE("Surface centering survives changes to separated infill settings", "[PrintObject][SurfaceInfill][Regression]") +{ + const std::string pattern = GENERATE("archimedeanchords", "octagramspiral"); + const std::string initial_center = GENERATE("each_surface", "each_model", "each_assembly"); + const std::string final_center = GENERATE("each_surface", "each_model", "each_assembly"); + const bool separated = GENERATE(false, true); + const std::string top_order = GENERATE("default", "outward", "inward"); + const std::string bottom_order = top_order == "outward" ? "inward" : top_order == "inward" ? "outward" : "default"; + const std::string density = GENERATE("80%", "100%"); + const bool change_center = initial_center != final_center; + CAPTURE(pattern, initial_center, final_center, separated, top_order, bottom_order, density); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"top_surface_pattern", pattern}, + {"bottom_surface_pattern", pattern}, + {"top_surface_fill_order", top_order}, + {"bottom_surface_fill_order", bottom_order}, + {"top_surface_density", density}, + {"bottom_surface_density", density}, + {"center_of_surface_pattern", initial_center}, + {"separated_infills", change_center ? separated : !separated}, + {"sparse_infill_pattern", "rectilinear"}, + {"sparse_infill_density", "15%"}, + {"top_shell_layers", 2}, + {"bottom_shell_layers", 2}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}}); + + // Orca: Two disconnected bodies exercise per-body centering. The offset tower also + // makes each-surface and each-model centering differ on the top surfaces. + TriangleMesh mesh = make_cube(30, 24, 2); + TriangleMesh tower = make_cube(12, 10, 1); + tower.translate(4, 3, 2); + mesh.merge(tower); + TriangleMesh second = mesh; + second.translate(50, 0, 0); + mesh.merge(second); + + // Orca: Equal footprints can hide reordered or reversed paths. Retain their point + // sequences and ordering protection to cover the directional surface behavior too. + struct SurfaceFillSnapshot { + std::map> paths; + bool protected_order = true; + }; + auto surface_fills = [](const Print &print) { + std::map, SurfaceFillSnapshot> result; + const PrintObject &object = *print.objects().front(); + for (size_t i = 0; i < object.layer_count(); ++i) { + auto collect = [&](const auto &self, const ExtrusionEntity &entity, bool no_sort) -> void { + if (const auto *collection = dynamic_cast(&entity)) { + for (const ExtrusionEntity *child : collection->entities) + self(self, *child, no_sort || collection->no_sort); + } else if (entity.role() == erTopSolidInfill || entity.role() == erBottomSurface) { + const auto *path = dynamic_cast(&entity); + REQUIRE(path != nullptr); + auto &snapshot = result[{i, entity.role()}]; + // Orca: The centered test model has one body on either side of X=0. + // Their traversal order may vary; preserve path order within each body. + Points points = path->polyline.to_polyline().points; + REQUIRE_FALSE(points.empty()); + snapshot.paths[points.front().x() > 0].push_back(std::move(points)); + snapshot.protected_order &= no_sort && !path->can_reverse(); + } + }; + for (const LayerRegion *region : object.get_layer(i)->regions()) + collect(collect, region->fills, false); + } + return result; + }; + + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + print.process(); + const auto initial = surface_fills(print); + config.set_deserialize_strict({{"center_of_surface_pattern", final_center}, {"separated_infills", separated}}); + print.apply(model, config); + // Orca: Preparation owns the body origins, and its invalidation must also force + // regeneration of top/bottom extrusion paths, even when sparse infill is unchanged. + CHECK_FALSE(print.objects().front()->is_step_done(posPrepareInfill)); + CHECK_FALSE(print.objects().front()->is_step_done(posInfill)); + print.process(); + const auto resliced = surface_fills(print); + + Print fresh_print; + Model fresh_model; + init_print({mesh}, fresh_print, fresh_model, config, nullptr, false); + fresh_print.process(); + const auto fresh = surface_fills(fresh_print); + REQUIRE_FALSE(fresh.empty()); + REQUIRE(resliced.size() == fresh.size()); + std::set roles; + bool changed_paths = false; + for (const auto &entry : fresh) { + CAPTURE(entry.first.first, entry.first.second); + REQUIRE_FALSE(entry.second.paths.empty()); + roles.insert(entry.first.second); + REQUIRE(resliced.count(entry.first) == 1); + REQUIRE(initial.count(entry.first) == 1); + const auto &actual = resliced.at(entry.first); + const auto &expected = entry.second; + const auto &before = initial.at(entry.first); + CHECK((actual.paths == expected.paths)); + if (!change_center) + CHECK((actual.paths == before.paths)); + if (top_order != "default") { + CHECK(expected.protected_order); + CHECK(actual.protected_order); + CHECK(before.protected_order); + } + changed_paths |= expected.paths != before.paths; + } + CHECK(roles.count(erTopSolidInfill) == 1); + CHECK(roles.count(erBottomSurface) == 1); + // Orca: Guard against a vacuous comparison: changing surface centering must change + // the printed pattern, while toggling separated sparse infill must leave it alone. + CHECK(changed_paths == change_center); +} + +TEST_CASE("Separated infill keeps fragmented and nested bodies independent", "[PrintObject][SurfaceInfill][Regression]") +{ + constexpr size_t grid_size = 8; + TriangleMesh mesh; + auto add_box = [&](double x, double y, double width, double depth) { + TriangleMesh box = make_cube(width, depth, 0.6); + box.translate(x, y, 0); + mesh.merge(box); + }; + // Orca: Many small islands exercise spatial pruning and the tree's original + // island indices. A pillar inside a frame also overlaps its bounding box, + // but must remain a separate body because it lies entirely inside the hole. + for (size_t x = 0; x < grid_size; ++ x) + for (size_t y = 0; y < grid_size; ++ y) + add_box(6 * x, 6 * y, 3, 3); + add_box(54, 0, 20, 4); + add_box(54, 16, 20, 4); + add_box(54, 0, 4, 20); + add_box(70, 0, 4, 20); + add_box(62, 8, 4, 4); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"separated_infills", true}, + {"center_of_surface_pattern", "each_surface"}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"elefant_foot_compensation", 0}, + {"wall_loops", 1}}); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + // Orca: Prepare body bounds through the public pipeline, then inspect the object read-only. + print.process(); + const PrintObject &object = *print.objects().front(); + REQUIRE(object.layer_count() > 1); + for (const Layer *layer : object.layers()) { + REQUIRE(layer->lslices.size() == grid_size * grid_size + 2); + REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + size_t holes = 0; + for (size_t i = 0; i < layer->lslices.size(); ++ i) { + const BoundingBox &body = layer->lslices_separated_component_bboxes[i]; + const BoundingBox &island = layer->lslices_bboxes[i]; + CHECK(body.min == island.min); + CHECK(body.max == island.max); + holes += layer->lslices[i].holes.size(); + } + CHECK(holes == 1); + } +} + +TEST_CASE("Body centering survives islands merging and splitting between layers", "[PrintObject][SurfaceInfill][Regression]") +{ + const bool separated = GENERATE(false, true); + CAPTURE(separated); + // Orca: Four posts join through horizontal then vertical rails, creating a + // cycle of overlaps before splitting into four islands again. This exercises + // redundant connections and indexing either adjacent layer. A fifth post + // stays separate at every height. + TriangleMesh mesh; + for (int x : {0, 8}) + for (int y : {0, 8}) { + TriangleMesh post = make_cube(4, 4, 1); + post.translate(x, y, 0); + mesh.merge(post); + } + for (int y : {0, 8}) { + TriangleMesh rail = make_cube(12, 4, 0.2); + rail.translate(0, y, 0.2); + mesh.merge(rail); + } + for (int x : {0, 8}) { + TriangleMesh rail = make_cube(4, 12, 0.2); + rail.translate(x, 0, 0.4); + mesh.merge(rail); + } + TriangleMesh isolated = make_cube(4, 4, 1); + isolated.translate(20, 0, 0); + mesh.merge(isolated); + + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"separated_infills", separated}, + {"center_of_surface_pattern", separated ? "each_surface" : "each_model"}, + {"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"elefant_foot_compensation", 0}, + {"wall_loops", 1}}); + Print print; + Model model; + init_print({mesh}, print, model, config, nullptr, false); + // Orca: Prepare body bounds through the public pipeline, then inspect the object read-only. + print.process(); + const PrintObject &object = *print.objects().front(); + REQUIRE(object.layer_count() == 5); + REQUIRE(object.get_layer(0)->lslices.size() == 5); + REQUIRE(object.get_layer(1)->lslices.size() == 3); + REQUIRE(object.get_layer(2)->lslices.size() == 3); + REQUIRE(object.get_layer(4)->lslices.size() == 5); + + BoundingBox isolated_bbox = object.get_layer(0)->lslices_bboxes.front(); + for (const BoundingBox &bbox : object.get_layer(0)->lslices_bboxes) + if (bbox.min.x() > isolated_bbox.min.x()) + isolated_bbox = bbox; + BoundingBox connected_bbox; + for (const Layer *layer : object.layers()) + for (const BoundingBox &bbox : layer->lslices_bboxes) + if (bbox.min.x() < isolated_bbox.min.x()) + connected_bbox.merge(bbox); + for (const Layer *layer : object.layers()) { + REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + for (size_t i = 0; i < layer->lslices.size(); ++ i) { + const BoundingBox &expected = layer->lslices_bboxes[i].min.x() < isolated_bbox.min.x() ? connected_bbox : isolated_bbox; + const BoundingBox &actual = layer->lslices_separated_component_bboxes[i]; + CHECK(actual.min == expected.min); + CHECK(actual.max == expected.max); + } + } +} From a93c6ea67b11376ed27c80acea7878b9fbfbf270 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Thu, 10 Sep 2026 19:29:58 +0800 Subject: [PATCH 60/60] hotfix: system bundles being copied from resources folder on every startup --- src/slic3r/Utils/PresetUpdater.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index b328c43cca..23957f6500 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1106,8 +1106,8 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE || (enabled_vendors.find(vendor_name) != enabled_vendors.end()); - if (enabled_config_update) { - if (is_vendor_installed(vendor_name)) { + if (is_vendor_installed(vendor_name)) { + if (enabled_config_update) { if (is_vendor_enabled) { // Orca: whichever form of the vendor resources ships at the newer // version is the one installing lays down, and the one to judge @@ -1122,17 +1122,12 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const << resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string(); bundles.insert(vendor_name); } - } - else { - //need to be removed because not installed + } else { + // need to be removed because not installed remove_installed_vendor(vendor_name); } } - else if (is_vendor_enabled) { - bundles.insert(vendor_name); - } - } - else if (is_vendor_enabled) { + } else if (is_vendor_enabled) { bundles.insert(vendor_name); } }