diff --git a/README.md b/README.md index 75d99f4b36..df55a89b8c 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ # Download ### Stable Release -📥 **[Download the Latest Stable Release](https://github.com/Snapmaker/OrcaSlicer/releases/latest)** +📥 **[Download the Latest Stable Release](https://github.com/ratdoux/OrcaSlicer-FullSpectrum/releases)** Visit our GitHub Releases page for the latest stable version of Full Spectrum, recommended for most users. # Features @@ -145,7 +145,10 @@ resolution: 0.1 Orca Slicer was originally forked from Bambu Studio, it was previously known as BambuStudio-SoftFever. Bambu Studio is forked from [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer) by Prusa Research, which is from [Slic3r](https://github.com/Slic3r/Slic3r) by Alessandro Ranellucci and the RepRap community. Orca Slicer incorporates a lot of features from SuperSlicer by @supermerill -Orca Slicer's logo is designed by community member Justin Levine(@freejstnalxndr) +Orca Slicer's logo is designed by community member Justin Levine(@freejstnalxndr) + +## Acknowledgements +Special thanks to [u/Aceman11100](https://www.reddit.com/user/Aceman11100/) for the inspiration and idea behind the mixed-color filament feature! # License diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 6db53f97ad..889c3258fe 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include @@ -24,6 +25,40 @@ namespace Slic3r { const static bool g_wipe_into_objects = false; +namespace { + +unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_mgr, + size_t num_physical, + unsigned int filament_id_1based, + int layer_index, + float layer_height_a, + float layer_height_b, + float base_layer_height) +{ + if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical))) + return filament_id_1based; + + if (layer_height_a > 0.f || layer_height_b > 0.f) { + const float safe_base = std::max(0.01f, base_layer_height); + const int ratio_a = std::max(1, int(std::lround((layer_height_a > 0.f ? layer_height_a : safe_base) / safe_base))); + const int ratio_b = std::max(1, int(std::lround((layer_height_b > 0.f ? layer_height_b : safe_base) / safe_base))); + const int cycle = ratio_a + ratio_b; + + if (cycle > 0) { + const size_t idx = static_cast(filament_id_1based - num_physical - 1); + const auto &mixed = mixed_mgr->mixed_filaments(); + if (idx < mixed.size()) { + const int pos = ((layer_index % cycle) + cycle) % cycle; + return pos < ratio_a ? mixed[idx].component_a : mixed[idx].component_b; + } + } + } + + return mixed_mgr->resolve(filament_id_1based, num_physical, layer_index); +} + +} // namespace + // Shortest hamilton path problem static std::vector solve_extruder_order(const std::vector>& wipe_volumes, std::vector all_extruders, std::optional start_extruder_id) @@ -149,9 +184,13 @@ bool LayerTools::is_extruder_order(unsigned int a, unsigned int b) const // Resolve a 1-based filament ID through the mixed-filament manager for this layer. unsigned int LayerTools::resolve_mixed_1based(unsigned int filament_id) const { - if (mixed_mgr && mixed_mgr->is_mixed(filament_id, num_physical)) - return mixed_mgr->resolve(filament_id, num_physical, this->layer_index); - return filament_id; + return resolve_mixed_with_layer_heights(mixed_mgr, + num_physical, + filament_id, + this->layer_index, + mixed_layer_height_a, + mixed_layer_height_b, + mixed_base_layer_height); } // Return a zero based extruder from the region, or extruder_override if overriden. @@ -223,6 +262,7 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude // Mixed filament support. m_mixed_mgr = &object.print()->mixed_filament_manager(); m_num_physical = object.print()->config().filament_diameter.size(); + update_mixed_layer_height_settings(); if (object.layers().empty()) return; @@ -289,6 +329,7 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool // Mixed filament support. m_mixed_mgr = &print.mixed_filament_manager(); m_num_physical = print.config().filament_diameter.size(); + update_mixed_layer_height_settings(); // Initialize the print layers for all objects and all layers. coordf_t object_bottom_z = 0.; @@ -363,6 +404,33 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool this->mark_skirt_layers(print.config(), max_layer_height); } +void ToolOrdering::update_mixed_layer_height_settings() +{ + const PrintConfig *cfg = m_print_config_ptr; + if (cfg == nullptr && m_print_object_ptr != nullptr) + cfg = &m_print_object_ptr->print()->config(); + + m_mixed_layer_height_a = 0.f; + m_mixed_layer_height_b = 0.f; + if (m_print_full_config != nullptr && + m_print_full_config->has("mixed_color_layer_height_a") && + m_print_full_config->has("mixed_color_layer_height_b")) { + m_mixed_layer_height_a = float(m_print_full_config->opt_float("mixed_color_layer_height_a")); + m_mixed_layer_height_b = float(m_print_full_config->opt_float("mixed_color_layer_height_b")); + } else if (cfg != nullptr) { + m_mixed_layer_height_a = cfg->mixed_color_layer_height_a.value; + m_mixed_layer_height_b = cfg->mixed_color_layer_height_b.value; + } + + float base_height = 0.2f; + if (m_print_object_ptr != nullptr) + base_height = float(m_print_object_ptr->config().layer_height.value); + else if (m_print_full_config != nullptr && m_print_full_config->has("layer_height")) + base_height = float(m_print_full_config->opt_float("layer_height")); + + m_mixed_base_layer_height = std::max(0.01f, base_height); +} + static void apply_first_layer_order(const DynamicPrintConfig* config, std::vector& tool_order) { const ConfigOptionInts* first_layer_print_sequence_op = config->option("first_layer_print_sequence"); if (first_layer_print_sequence_op) { @@ -492,6 +560,14 @@ void ToolOrdering::initialize_layers(std::vector &zs) // Collect extruders reuqired to print layers. void ToolOrdering::collect_extruders(const PrintObject &object, const std::vector> &per_layer_extruder_switches) { + for (LayerTools &layer_tools : m_layer_tools) { + layer_tools.mixed_mgr = m_mixed_mgr; + layer_tools.num_physical = m_num_physical; + layer_tools.mixed_layer_height_a = m_mixed_layer_height_a; + layer_tools.mixed_layer_height_b = m_mixed_layer_height_b; + layer_tools.mixed_base_layer_height = m_mixed_base_layer_height; + } + // Collect the support extruders. for (auto support_layer : object.support_layers()) { LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z); @@ -524,9 +600,7 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto for (auto layer : object.layers()) { LayerTools &layer_tools = this->tools_for_layer(layer->print_z); // Store the sequential layer index and mixed-filament context for resolution. - layer_tools.layer_index = layerCount; - layer_tools.mixed_mgr = m_mixed_mgr; - layer_tools.num_physical = m_num_physical; + layer_tools.layer_index = layerCount; // Override extruder with the next for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override) @@ -1441,9 +1515,13 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject // Resolve a 1-based filament ID through the mixed-filament manager. unsigned int ToolOrdering::resolve_mixed(unsigned int filament_id_1based, int layer_index) const { - if (m_mixed_mgr && m_mixed_mgr->is_mixed(filament_id_1based, m_num_physical)) - return m_mixed_mgr->resolve(filament_id_1based, m_num_physical, layer_index); - return filament_id_1based; + return resolve_mixed_with_layer_heights(m_mixed_mgr, + m_num_physical, + filament_id_1based, + layer_index, + m_mixed_layer_height_a, + m_mixed_layer_height_b, + m_mixed_base_layer_height); } } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 4be18af7f4..7b457f635b 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -144,6 +144,10 @@ public: // Mixed-filament resolution context (set by ToolOrdering during collect_extruders). const MixedFilamentManager *mixed_mgr = nullptr; size_t num_physical = 0; + // Optional mixed-layer cadence override from print settings. + float mixed_layer_height_a = 0.f; + float mixed_layer_height_b = 0.f; + float mixed_base_layer_height = 0.2f; private: // Resolve a 1-based filament ID through the mixed-filament manager for this layer. @@ -211,6 +215,7 @@ private: // BBS std::vector generate_first_layer_tool_order(const Print& print); std::vector generate_first_layer_tool_order(const PrintObject& object); + void update_mixed_layer_height_settings(); // Resolve a 1-based filament ID through the mixed-filament manager. // Returns the resolved physical extruder (1-based). If the ID is not a @@ -233,6 +238,9 @@ private: // number of physical extruders. const MixedFilamentManager* m_mixed_mgr = nullptr; size_t m_num_physical = 0; + float m_mixed_layer_height_a = 0.f; + float m_mixed_layer_height_b = 0.f; + float m_mixed_base_layer_height = 0.2f; }; } // namespace SLic3r diff --git a/src/libslic3r/MixedFilament.cpp b/src/libslic3r/MixedFilament.cpp index 33405ce1cb..52983b6f92 100644 --- a/src/libslic3r/MixedFilament.cpp +++ b/src/libslic3r/MixedFilament.cpp @@ -79,6 +79,27 @@ void MixedFilamentManager::auto_generate(const std::vector &filamen } } +void MixedFilamentManager::remove_physical_filament(unsigned int deleted_filament_id) +{ + if (deleted_filament_id == 0 || m_mixed.empty()) + return; + + std::vector filtered; + filtered.reserve(m_mixed.size()); + for (MixedFilament mf : m_mixed) { + if (mf.component_a == deleted_filament_id || mf.component_b == deleted_filament_id) + continue; + + if (mf.component_a > deleted_filament_id) + --mf.component_a; + if (mf.component_b > deleted_filament_id) + --mf.component_b; + + filtered.emplace_back(std::move(mf)); + } + m_mixed = std::move(filtered); +} + unsigned int MixedFilamentManager::resolve(unsigned int filament_id, size_t num_physical, int layer_index) const diff --git a/src/libslic3r/MixedFilament.hpp b/src/libslic3r/MixedFilament.hpp index 366b717500..676ff8d49f 100644 --- a/src/libslic3r/MixedFilament.hpp +++ b/src/libslic3r/MixedFilament.hpp @@ -61,6 +61,11 @@ public: // exists. void auto_generate(const std::vector &filament_colours); + // Remove a physical filament (1-based ID) from the mixed list. + // Any mixed filament that contains the removed component is deleted. + // Remaining component IDs are shifted down to stay aligned with physical IDs. + void remove_physical_filament(unsigned int deleted_filament_id); + // ---- Queries -------------------------------------------------------- // True when `filament_id` (1-based) refers to a mixed filament. diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index cf6004dae1..7d99da78d5 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2507,6 +2507,20 @@ void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_cou } } +void ModelVolume::remap_extruder_ids(size_t extruder_count, const EnforcerBlockerStateMap &state_map) +{ + std::vector used_extruders = get_extruders(); + for (int extruder_id : used_extruders) { + if (extruder_id <= 0) + continue; + if (extruder_id >= int(state_map.size()) || extruder_id > int(extruder_count) || + state_map[size_t(extruder_id)] != EnforcerBlockerType(extruder_id)) { + mmu_segmentation_facets.remap_enforcer_block_types(*this, EnforcerBlockerType(extruder_count), state_map); + break; + } + } +} + void ModelVolume::center_geometry_after_creation(bool update_source_offset) { Vec3d shift = this->mesh().bounding_box().center(); @@ -3411,6 +3425,15 @@ void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume& mv, this->set(selector); } +void FacetsAnnotation::remap_enforcer_block_types(const ModelVolume& mv, + EnforcerBlockerType max_type, + const EnforcerBlockerStateMap &state_map) +{ + TriangleSelector selector(mv.mesh()); + selector.deserialize(m_data, false, max_type, EnforcerBlockerType::NONE, EnforcerBlockerType::NONE, &state_map); + this->set(selector); +} + indexed_triangle_set FacetsAnnotation::get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const { TriangleSelector selector(mv.mesh()); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index bf3d129bdf..1555119379 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -738,6 +738,9 @@ public: EnforcerBlockerType max_type, EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE, EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE); + void remap_enforcer_block_types(const ModelVolume& mv, + EnforcerBlockerType max_type, + 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(); } @@ -919,6 +922,7 @@ public: std::vector get_extruders() const; void update_extruder_count(size_t extruder_count); void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1); + void remap_extruder_ids(size_t extruder_count, const EnforcerBlockerStateMap &state_map); // Split this volume, append the result to the object owning this volume. // Return the number of volumes created from this one. diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 6870bbd4b8..09301959b0 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -3231,6 +3231,10 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam #else size_t num_filaments = this->filament_presets.size(); #endif + const bool deleting_filament = (to_delete_filament_id != size_t(-1)); + const size_t old_num_filaments = deleting_filament ? (num_filaments + 1) : num_filaments; + const std::vector old_mixed = this->mixed_filaments.mixed_filaments(); + m_last_filament_id_remap.clear(); // 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; @@ -3259,6 +3263,12 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam this->project_config.option("flush_volumes_matrix")->values = new_matrix; } + // Keep mixed (virtual) combinations in sync with physical filament deletion. + // Mixed entries containing the deleted physical filament are removed, while + // remaining component IDs are shifted. + if (deleting_filament) + this->mixed_filaments.remove_physical_filament(unsigned(to_delete_filament_id + 1)); + // Keep project colours aligned to physical filaments, then regenerate mixed // (virtual) entries from the physical set only. { @@ -3268,6 +3278,54 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam this->mixed_filaments.auto_generate(color_opt->values); } } + + // Build old->new filament ID remap for painted facet data normalization. + if (deleting_filament) { + const unsigned int deleted_1based = unsigned(to_delete_filament_id + 1); + + size_t old_enabled_mixed = 0; + for (const auto &mf : old_mixed) + if (mf.enabled) + ++old_enabled_mixed; + + const size_t old_total_filaments = old_num_filaments + old_enabled_mixed; + m_last_filament_id_remap.assign(old_total_filaments + 1, 0); + + for (unsigned int old_id = 1; old_id <= unsigned(old_num_filaments); ++old_id) { + if (old_id == deleted_1based) + m_last_filament_id_remap[old_id] = 0; + else + m_last_filament_id_remap[old_id] = old_id > deleted_1based ? old_id - 1 : old_id; + } + + std::map, unsigned int> new_pair_to_id; + unsigned int next_virtual_id = unsigned(num_filaments + 1); + for (const auto &mf : this->mixed_filaments.mixed_filaments()) { + if (!mf.enabled) + continue; + new_pair_to_id[{mf.component_a, mf.component_b}] = next_virtual_id++; + } + + unsigned int old_virtual_id = unsigned(old_num_filaments + 1); + for (const auto &mf : old_mixed) { + if (!mf.enabled) + continue; + + unsigned int a = mf.component_a; + unsigned int b = mf.component_b; + if (a == deleted_1based || b == deleted_1based) { + m_last_filament_id_remap[old_virtual_id] = 0; + } else { + if (a > deleted_1based) + --a; + if (b > deleted_1based) + --b; + auto it = new_pair_to_id.find({a, b}); + m_last_filament_id_remap[old_virtual_id] = (it == new_pair_to_id.end()) ? 0 : it->second; + } + ++old_virtual_id; + } + } } void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible) diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 612aefcda8..ea524106f3 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -253,6 +253,9 @@ public: // Read out the number of extruders from an active printer preset, // update size and content of filament_presets. void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1)); + // Mapping generated during the latest filament deletion. + // Index is old 1-based filament ID, value is new 1-based filament ID (0 = removed). + const std::vector& last_filament_id_remap() const { return m_last_filament_id_remap; } // Update the is_compatible flag of all print and filament presets depending on whether they are marked // as compatible with the currently selected printer (and print in case of filament presets). @@ -324,6 +327,7 @@ private: bool validation_mode = false; std::string vendor_to_validate = ""; int m_errors = 0; + std::vector m_last_filament_id_remap; }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 2011a58636..01016d8ad5 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -245,6 +245,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "filament_shrinkage_compensation_z" || opt_key == "resolution" || opt_key == "precise_z_height" + || opt_key == "dithering_z_step_size" + || opt_key == "dithering_step_painted_zones_only" // Spiral Vase forces different kind of slicing than the normal model: // In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer. // Therefore toggling the Spiral Vase on / off requires complete reslicing. @@ -1130,7 +1132,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons* layer_height_profiles.assign(m_objects.size(), std::vector()); std::vector &profile = layer_height_profiles[print_object_idx]; if (profile.empty()) - PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile); + PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object); return profile; }; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 8d83a55f41..df789c2596 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -410,7 +410,10 @@ public: // Initialize the layer_height_profile from the model_object's layer_height_profile, from model_object's layer height table, or from slicing parameters. // Returns true, if the layer_height_profile was changed. - static bool update_layer_height_profile(const ModelObject &model_object, const SlicingParameters &slicing_parameters, std::vector &layer_height_profile); + static bool update_layer_height_profile(const ModelObject &model_object, + const SlicingParameters &slicing_parameters, + std::vector &layer_height_profile, + const PrintObject *print_object = nullptr); // Collect the slicing parameters, to be used by variable layer thickness algorithm, // by the interactive layer height editor and by the printing process itself. diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 7450d3ed80..fa619c8a09 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1092,6 +1092,13 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ new_full_config.option("print_settings_id", true); new_full_config.option("filament_settings_id", true); new_full_config.option("printer_settings_id", true); + // Ensure newly introduced dithering keys are present so in-session updates are detected. + new_full_config.option("dithering_z_step_size", true); + new_full_config.option("dithering_step_painted_zones_only", true); + m_config.option("dithering_z_step_size", true); + m_config.option("dithering_step_painted_zones_only", true); + m_default_object_config.option("dithering_z_step_size", true); + m_default_object_config.option("dithering_step_painted_zones_only", true); // BBS int used_filaments = this->extruders(true).size(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4bcb6be574..9dd9c750b0 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4120,6 +4120,44 @@ void PrintConfigDef::init_fff_params() def->height = 6; def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings()); + + def = this->add("mixed_color_layer_height_a", coFloat); + def->label = L("Dithering cadence height A"); + def->category = L("Others"); + def->tooltip = L("Layer height contribution of component A for dithering virtual filaments. " + "Set to 0 to use normal 1-layer A / 1-layer B alternation."); + def->sidetext = "mm"; + def->min = 0.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("mixed_color_layer_height_b", coFloat); + def->label = L("Dithering cadence height B"); + def->category = L("Others"); + def->tooltip = L("Layer height contribution of component B for dithering virtual filaments. " + "Set to 0 to use normal 1-layer A / 1-layer B alternation."); + def->sidetext = "mm"; + def->min = 0.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("dithering_z_step_size", coFloat); + def->label = L("Dithering Z step size"); + def->category = L("Others"); + def->tooltip = L("Layer height used in Z zones painted with dithering (mixed virtual filaments). " + "Set to 0 to keep normal layer height in those zones."); + def->sidetext = "mm"; + def->min = 0.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(0.0)); + + def = this->add("dithering_step_painted_zones_only", coBool); + def->label = L("Use step size in painted zones only"); + def->category = L("Others"); + def->tooltip = L("When enabled, dithering Z step size is applied only where mixed filament is painted. " + "Unpainted zones keep their original layer height."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(true)); def = this->add("printer_model", coString); def->label = L("Printer type"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 33c21a98da..7fb49aa982 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1351,6 +1351,10 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBool, ooze_prevention)) ((ConfigOptionString, filename_format)) ((ConfigOptionStrings, post_process)) + ((ConfigOptionFloat, mixed_color_layer_height_a)) + ((ConfigOptionFloat, mixed_color_layer_height_b)) + ((ConfigOptionFloat, dithering_z_step_size)) + ((ConfigOptionBool, dithering_step_painted_zones_only)) ((ConfigOptionString, printer_model)) ((ConfigOptionFloat, resolution)) ((ConfigOptionFloats, retraction_minimum_travel)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0bad0efd91..308f1b5545 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -946,6 +946,8 @@ bool PrintObject::invalidate_state_by_config_options( steps.emplace_back(posPerimeters); } else if ( opt_key == "layer_height" + || opt_key == "dithering_z_step_size" + || opt_key == "dithering_step_painted_zones_only" || opt_key == "mmu_segmented_region_max_width" || opt_key == "mmu_segmented_region_interlocking_depth" || opt_key == "raft_layers" @@ -3331,10 +3333,263 @@ std::vector PrintObject::object_extruders() const return extruders; } -bool PrintObject::update_layer_height_profile(const ModelObject &model_object, const SlicingParameters &slicing_parameters, std::vector &layer_height_profile) +namespace { + +struct LayerHeightRangeOverride { + coordf_t lo { 0.f }; + coordf_t hi { 0.f }; + coordf_t height { 0.f }; +}; + +static void sort_and_merge_layer_ranges(std::vector &ranges) +{ + if (ranges.empty()) + return; + + std::sort(ranges.begin(), ranges.end(), [](const t_layer_height_range &a, const t_layer_height_range &b) { + return a.first < b.first || (a.first == b.first && a.second < b.second); + }); + + std::vector merged; + merged.reserve(ranges.size()); + for (const t_layer_height_range &range : ranges) { + if (range.second <= range.first + EPSILON) + continue; + + if (merged.empty() || range.first > merged.back().second + EPSILON) { + merged.emplace_back(range); + } else { + merged.back().second = std::max(merged.back().second, range.second); + } + } + ranges = std::move(merged); +} + +static std::vector collect_mixed_painted_z_ranges(const PrintObject &print_object, coordf_t object_height) +{ + std::vector mixed_ranges; + if (object_height <= EPSILON) + return mixed_ranges; + + const Print *print = print_object.print(); + if (print == nullptr) + return mixed_ranges; + + const size_t num_physical = print->config().filament_colour.size(); + const size_t num_total = print->mixed_filament_manager().total_filaments(num_physical); + if (num_total <= num_physical) + return mixed_ranges; + + const size_t max_state = std::min(num_total, size_t(EnforcerBlockerType::ExtruderMax)); + const Transform3d object_to_print = print_object.trafo_centered(); + + for (const ModelVolume *mv : print_object.model_object()->volumes) { + if (mv == nullptr || !mv->is_model_part() || mv->mmu_segmentation_facets.empty()) + continue; + + const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states; + if (used_states.empty()) + continue; + + const Transform3d volume_to_print = object_to_print * mv->get_matrix(); + constexpr coordf_t thin_band = 0.01f; + for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) { + if (state_idx >= used_states.size() || !used_states[state_idx]) + continue; + + const auto facets = mv->mmu_segmentation_facets.get_facets_strict(*mv, static_cast(state_idx)); + if (facets.indices.empty() || facets.vertices.empty()) + continue; + + for (const auto &face : facets.indices) { + double tri_z_min = DBL_MAX; + double tri_z_max = -DBL_MAX; + for (int i = 0; i < 3; ++i) { + const size_t vertex_idx = size_t(face[i]); + if (vertex_idx >= facets.vertices.size()) + continue; + const Vec3d p = volume_to_print * facets.vertices[vertex_idx].cast(); + tri_z_min = std::min(tri_z_min, p.z()); + tri_z_max = std::max(tri_z_max, p.z()); + } + + if (tri_z_min == DBL_MAX || tri_z_max == -DBL_MAX) + continue; + + coordf_t lo = std::max(0.f, coordf_t(tri_z_min)); + coordf_t hi = std::min(object_height, coordf_t(tri_z_max)); + if (hi <= lo + EPSILON) { + const coordf_t center = std::max(0.f, std::min(object_height, lo)); + lo = std::max(0.f, center - thin_band * 0.5f); + hi = std::min(object_height, center + thin_band * 0.5f); + } + if (lo + EPSILON < hi) + mixed_ranges.emplace_back(lo, hi); + } + } + } + + sort_and_merge_layer_ranges(mixed_ranges); + return mixed_ranges; +} + +static std::vector base_layer_height_overrides(const t_layer_config_ranges &ranges, coordf_t object_height) +{ + std::vector out; + out.reserve(ranges.size()); + + coordf_t last_hi = 0.f; + for (const auto &[range, config] : ranges) { + coordf_t lo = std::max(range.first, last_hi); + coordf_t hi = std::min(range.second, object_height); + if (lo + EPSILON >= hi) + continue; + + const ConfigOption *layer_height_opt = config.option("layer_height"); + if (layer_height_opt == nullptr) + continue; + + out.push_back({ lo, hi, coordf_t(layer_height_opt->getFloat()) }); + last_hi = hi; + } + return out; +} + +static bool contains_z(const std::vector &ranges, coordf_t z) +{ + for (const t_layer_height_range &range : ranges) { + if (z + EPSILON < range.first) + break; + if (z + EPSILON >= range.first && z < range.second - EPSILON) + return true; + } + return false; +} + +static bool get_override_height(const std::vector &ranges, coordf_t z, coordf_t &height_out) +{ + for (const LayerHeightRangeOverride &range : ranges) { + if (z + EPSILON < range.lo) + break; + if (z + EPSILON >= range.lo && z < range.hi - EPSILON) { + height_out = range.height; + return true; + } + } + return false; +} + +static t_layer_config_ranges layer_ranges_with_dithering(const t_layer_config_ranges &base_ranges_map, + coordf_t object_height, + coordf_t default_layer_height, + const std::vector &mixed_ranges, + coordf_t dithering_step) +{ + if (object_height <= EPSILON || mixed_ranges.empty() || dithering_step <= EPSILON) + return base_ranges_map; + + const std::vector base_ranges = base_layer_height_overrides(base_ranges_map, object_height); + + std::vector boundaries; + boundaries.reserve(2 + base_ranges.size() * 2 + mixed_ranges.size() * 2); + boundaries.emplace_back(0.f); + boundaries.emplace_back(object_height); + for (const LayerHeightRangeOverride &range : base_ranges) { + boundaries.emplace_back(range.lo); + boundaries.emplace_back(range.hi); + } + for (const t_layer_height_range &range : mixed_ranges) { + boundaries.emplace_back(range.first); + boundaries.emplace_back(range.second); + } + + std::sort(boundaries.begin(), boundaries.end()); + boundaries.erase(std::unique(boundaries.begin(), boundaries.end(), [](coordf_t a, coordf_t b) { + return std::abs(a - b) <= EPSILON; + }), + boundaries.end()); + + std::vector merged_ranges; + merged_ranges.reserve(boundaries.size()); + for (size_t i = 1; i < boundaries.size(); ++i) { + const coordf_t lo = boundaries[i - 1]; + const coordf_t hi = boundaries[i]; + if (hi <= lo + EPSILON) + continue; + + const coordf_t z_mid = 0.5f * (lo + hi); + + coordf_t target_height = default_layer_height; + coordf_t base_height = 0.f; + if (contains_z(mixed_ranges, z_mid)) { + target_height = dithering_step; + } else if (get_override_height(base_ranges, z_mid, base_height)) { + target_height = base_height; + } + + if (std::abs(target_height - default_layer_height) <= EPSILON) + continue; + + if (!merged_ranges.empty() && + std::abs(merged_ranges.back().height - target_height) <= EPSILON && + std::abs(merged_ranges.back().hi - lo) <= EPSILON) { + merged_ranges.back().hi = hi; + } else { + merged_ranges.push_back({ lo, hi, target_height }); + } + } + + t_layer_config_ranges out; + for (const LayerHeightRangeOverride &range : merged_ranges) { + if (range.hi <= range.lo + EPSILON) + continue; + ModelConfig cfg; + cfg.set_key_value("layer_height", new ConfigOptionFloat(range.height)); + out.emplace(t_layer_height_range(range.lo, range.hi), std::move(cfg)); + } + return out; +} + +} // namespace + +bool PrintObject::update_layer_height_profile(const ModelObject &model_object, + const SlicingParameters &slicing_parameters, + std::vector &layer_height_profile, + const PrintObject *print_object) { bool updated = false; + const t_layer_config_ranges *ranges_to_use = &model_object.layer_config_ranges; + t_layer_config_ranges dithering_ranges; + if (print_object != nullptr && print_object->print() != nullptr) { + const DynamicPrintConfig &full_cfg = print_object->print()->full_print_config(); + coordf_t dithering_step = coordf_t(print_object->print()->config().dithering_z_step_size.value); + bool painted_zones_only = print_object->print()->config().dithering_step_painted_zones_only.value; + if (full_cfg.has("dithering_z_step_size")) + dithering_step = coordf_t(full_cfg.opt_float("dithering_z_step_size")); + if (full_cfg.has("dithering_step_painted_zones_only")) + painted_zones_only = full_cfg.opt_bool("dithering_step_painted_zones_only"); + + if (dithering_step > EPSILON) { + const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height(); + std::vector mixed_ranges; + if (painted_zones_only) + mixed_ranges = collect_mixed_painted_z_ranges(*print_object, object_height); + else if (object_height > EPSILON) + mixed_ranges.emplace_back(0.f, object_height); + + if (!mixed_ranges.empty()) { + dithering_ranges = layer_ranges_with_dithering(model_object.layer_config_ranges, + object_height, + slicing_parameters.layer_height, + mixed_ranges, + dithering_step); + ranges_to_use = &dithering_ranges; + } + } + } + const bool has_dithering_ranges = (ranges_to_use != &model_object.layer_config_ranges); + if (layer_height_profile.empty()) { // use the constructor because the assignement is crashing on ASAN OsX layer_height_profile = std::vector(model_object.layer_height_profile.get()); @@ -3352,9 +3607,9 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_object, c std::abs(layer_height_profile[layer_height_profile.size() - 2] - slicing_parameters.object_print_z_uncompensated_max + slicing_parameters.object_print_z_min) > 1e-3)) layer_height_profile.clear(); - if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height) { + if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height || has_dithering_ranges) { //layer_height_profile = layer_height_profile_adaptive(slicing_parameters, model_object.layer_config_ranges, model_object.volumes); - layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, model_object.layer_config_ranges); + layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, *ranges_to_use); // The layer height profile is already compressed. updated = true; } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 3735cd95d1..9ab8ad226a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -793,7 +793,7 @@ void PrintObject::slice() //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; - this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile); + this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile, this); m_print->throw_if_canceled(); m_typed_slices = false; this->clear_layers(); diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 30fca733b7..b12b0bbc51 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1724,7 +1724,8 @@ void TriangleSelector::deserialize(const TriangleSplittingData& data, bool needs_reset, EnforcerBlockerType max_ebt, EnforcerBlockerType to_delete_filament, - EnforcerBlockerType replace_filament) + EnforcerBlockerType replace_filament, + const EnforcerBlockerStateMap* state_map) { if (needs_reset) reset(); // dump any current state @@ -1790,11 +1791,15 @@ void TriangleSelector::deserialize(const TriangleSplittingData& data, } } - - if (state == to_delete_filament) - state = replace_filament; - else if (to_delete_filament != EnforcerBlockerType::NONE && state != EnforcerBlockerType::NONE) { - state = state > to_delete_filament ? EnforcerBlockerType((int) state - 1) : state; + if (state_map != nullptr && state != EnforcerBlockerType::NONE) { + const size_t state_idx = static_cast(state); + state = state_idx < state_map->size() ? (*state_map)[state_idx] : EnforcerBlockerType::NONE; + } else { + if (state == to_delete_filament) + state = replace_filament; + else if (to_delete_filament != EnforcerBlockerType::NONE && state != EnforcerBlockerType::NONE) { + state = state > to_delete_filament ? EnforcerBlockerType((int) state - 1) : state; + } } if (state > max_ebt) { diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index b7a536e932..37f4b6c3b1 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -360,7 +360,8 @@ public: bool needs_reset = true, EnforcerBlockerType max_ebt = EnforcerBlockerType::ExtruderMax, EnforcerBlockerType to_delete_filament = EnforcerBlockerType::NONE, - EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE); + EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE, + const EnforcerBlockerStateMap *state_map = nullptr); // Extract all used facet states from the given TriangleSplittingData. static std::vector extract_used_facet_states(const TriangleSplittingData &data); diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 5b6b619e86..31c33c6e11 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -108,13 +108,21 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt { try{ - if (config.def()->get(opt_key)->type == coBools && config.def()->get(opt_key)->nullable) { + const ConfigOptionDef *opt_def = config.def()->get(opt_key); + if (opt_def == nullptr) + throw Slic3r::RuntimeError("Unknown config option key: " + opt_key); + + // Some older presets may not carry newly introduced keys. Ensure the + // option exists before mutating it through typed accessors below. + if (!config.has(opt_key)) + config.set_key_value(opt_key, opt_def->create_default_option()); + + if (opt_def->type == coBools && opt_def->nullable) { ConfigOptionBoolsNullable* vec_new = new ConfigOptionBoolsNullable{ boost::any_cast(value) }; config.option(opt_key)->set_at(vec_new, opt_index, 0); return; } - const ConfigOptionDef *opt_def = config.def()->get(opt_key); switch (opt_def->type) { case coFloatOrPercent:{ std::string str = boost::any_cast(value); diff --git a/src/slic3r/GUI/OptionsGroup.cpp b/src/slic3r/GUI/OptionsGroup.cpp index ba6ff14f37..5330ccf40d 100644 --- a/src/slic3r/GUI/OptionsGroup.cpp +++ b/src/slic3r/GUI/OptionsGroup.cpp @@ -981,10 +981,29 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config case coPercents: case coFloats: case coFloat:{ - double val = opt->type == coFloats ? + double val = 0.0; + if (config.has(opt_key)) { + val = opt->type == coFloats ? config.opt_float(opt_key, idx) : opt->type == coFloat ? config.opt_float(opt_key) : config.option(opt_key)->get_at(idx); + } else if (opt->default_value) { + if (opt->type == coFloats) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + val = defaults->values[safe_idx]; + } + } else if (opt->type == coFloat) { + val = opt->default_value->getFloat(); + } else { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + val = defaults->values[safe_idx]; + } + } + } ret = double_to_string(val); } break; @@ -1009,10 +1028,29 @@ boost::any ConfigOptionsGroup::get_config_value(const DynamicPrintConfig& config ret = from_u8(config.opt_string(opt_key, static_cast(idx))); break; case coBool: - ret = config.opt_bool(opt_key); + if (config.has(opt_key)) { + ret = config.opt_bool(opt_key); + } else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : false; + } else { + ret = false; + } break; case coBools: - ret = config.opt_bool(opt_key, idx); + if (config.has(opt_key)) { + ret = config.opt_bool(opt_key, idx); + } else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = defaults->values[safe_idx]; + } else { + ret = false; + } + } else { + ret = false; + } break; case coInt: ret = config.opt_int(opt_key); @@ -1139,10 +1177,27 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi ret = config.opt_string(opt_key, static_cast(idx)); break; case coBool: - ret = config.opt_bool(opt_key); + if (config.has(opt_key)) + ret = config.opt_bool(opt_key); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + ret = defaults != nullptr ? defaults->value : false; + } else + ret = false; break; case coBools: - ret = static_cast(config.opt_bool(opt_key, idx)); + if (config.has(opt_key)) + ret = static_cast(config.opt_bool(opt_key, idx)); + else if (opt->default_value) { + const auto *defaults = dynamic_cast(opt->default_value.get()); + if (defaults != nullptr && !defaults->values.empty()) { + const size_t safe_idx = std::min(idx, defaults->values.size() - 1); + ret = static_cast(defaults->values[safe_idx]); + } else { + ret = static_cast(false); + } + } else + ret = static_cast(false); break; case coInt: ret = config.opt_int(opt_key); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index aa83078e77..2121b88da1 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1436,7 +1436,7 @@ Sidebar::Sidebar(Plater *parent) p->m_sizer_mixed_filaments = new wxBoxSizer(wxVERTICAL); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); // Title - auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Mixed Colors")); + auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Dithering")); mixed_title->SetFont(Label::Head_14); p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16)); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); @@ -2205,7 +2205,7 @@ void Sidebar::update_mixed_filament_panel() // Recreate header. p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); - auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Mixed Colors")); + auto *mixed_title = new wxStaticText(p->m_panel_mixed_filaments, wxID_ANY, _L("Dithering")); mixed_title->SetFont(Label::Head_14); p->m_sizer_mixed_filaments->Add(mixed_title, 0, wxLEFT | wxRIGHT, FromDIP(16)); p->m_sizer_mixed_filaments->AddSpacer(FromDIP(4)); @@ -2355,6 +2355,7 @@ void Sidebar::on_filaments_delete(size_t filament_id) p->m_panel_filament_title->Refresh(); update_ui_from_settings(); dynamic_filament_list.update(); + update_mixed_filament_panel(); } void Sidebar::edit_filament() { @@ -14317,10 +14318,40 @@ void Plater::on_filaments_delete(size_t num_filaments, size_t filament_id, int r }*/ // update mmu info + PresetBundle *preset_bundle = wxGetApp().preset_bundle; + + size_t total_filaments = num_filaments; + if (preset_bundle != nullptr) { + const size_t current_num_physical = preset_bundle->filament_presets.size(); + total_filaments = preset_bundle->mixed_filaments.total_filaments(current_num_physical); + } + + EnforcerBlockerStateMap state_map; + for (size_t i = 0; i < state_map.size(); ++i) + state_map[i] = EnforcerBlockerType(i); + + const bool can_use_remap = replace_filament_id < 0; + static const std::vector empty_remap; + const std::vector &id_remap = + preset_bundle != nullptr ? preset_bundle->last_filament_id_remap() : empty_remap; + if (can_use_remap && !id_remap.empty()) { + for (size_t i = 1; i < state_map.size(); ++i) { + const unsigned int mapped = i < id_remap.size() ? id_remap[i] : 0; + if (mapped == 0 || mapped >= state_map.size() || mapped > total_filaments) + state_map[i] = EnforcerBlockerType::NONE; + else + state_map[i] = EnforcerBlockerType(mapped); + } + } + 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); // this function is 1 base + if (can_use_remap && !id_remap.empty()) { + mv->remap_extruder_ids(total_filaments, state_map); + } else { + mv->update_extruder_count_when_delete_filament(total_filaments, filament_id + 1, + replace_filament_id + 1); // this function is 1 base + } } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index ee5890d3da..9a8224beb9 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2488,6 +2488,13 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops" optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); + // Use default (no icon) here to avoid runtime bitmap load failures. + optgroup = page->new_optgroup(L("Dithering")); + optgroup->append_single_option_line("mixed_color_layer_height_a", "others_settings_mixed_colors#layer-height-a"); + optgroup->append_single_option_line("mixed_color_layer_height_b", "others_settings_mixed_colors#layer-height-b"); + optgroup->append_single_option_line("dithering_z_step_size", "others_settings_mixed_colors#z-step-size"); + optgroup->append_single_option_line("dithering_step_painted_zones_only", "others_settings_mixed_colors#painted-zones-only"); + optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode");