From 836e18b0727ab0e479a3aef9cd97c31a9c9ab06f Mon Sep 17 00:00:00 2001 From: Alexandre Folle de Menezes Date: Mon, 6 Apr 2026 16:27:18 -0300 Subject: [PATCH 01/10] Update pt-BR translations (#12891) --- localization/i18n/pt_BR/OrcaSlicer_pt_BR.po | 82 +++++++-------------- 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po index b84b0bf417..9e136b4e09 100644 --- a/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po +++ b/localization/i18n/pt_BR/OrcaSlicer_pt_BR.po @@ -4,7 +4,7 @@ msgstr "" "Project-Id-Version: Orca Slicer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-20 23:26+0800\n" -"PO-Revision-Date: 2026-03-08 21:15-0300\n" +"PO-Revision-Date: 2026-03-22 17:15-0300\n" "Last-Translator: Alexandre Folle de Menezes\n" "Language-Team: Portuguese, Brazilian\n" "Language: pt_BR\n" @@ -5025,7 +5025,7 @@ msgid "Actual Speed: " msgstr "Velocidade Real: " msgid "PA: " -msgstr "PA: " +msgstr "AP: " msgid "Statistics of All Plates" msgstr "Estatísticas de Todas as Placas" @@ -5436,7 +5436,7 @@ msgid "Return" msgstr "Retornar" msgid "Canvas Toolbar" -msgstr "Barra de ferramentas da tela" +msgstr "Barra de Ferramentas de Tela" msgid "Fit camera to scene or selected object." msgstr "Ajustar câmera à cena ou objeto selecionado." @@ -6061,10 +6061,10 @@ msgid "Retraction test" msgstr "Teste de retração" msgid "Cornering" -msgstr "Curvas" +msgstr "Quinas" msgid "Cornering calibration" -msgstr "Calibração de curvas" +msgstr "Calibração de quinas" msgid "Input Shaping Frequency" msgstr "Frequência da Modelagem de Entrada" @@ -7226,13 +7226,13 @@ msgid "Enable AI monitoring of printing" msgstr "Ativar monitoramento de IA da impressão" msgid "Pausing Sensitivity:" -msgstr "Sensibilidade de pausa:" +msgstr "Sensibilidade de Pausa:" msgid "Spaghetti Detection" msgstr "Detecção de Emaranhado" msgid "Detect spaghetti failures (scattered lose filament)." -msgstr "Detectar falhas de espaguete (filamento solto espalhado)." +msgstr "Detectar falhas de emaranhado (filamento solto espalhado)." msgid "Purge Chute Pile-Up Detection" msgstr "Detecção de Acúmulo na Calha de Purga" @@ -9826,8 +9826,8 @@ msgid "" "Enabling both precise Z height and the prime tower may cause slicing errors. " "Do you still want to enable?" msgstr "" -"Habilitar tanto a altura Z precisa quanto a torre de preparação pode causar " -"erros de fatiamento. Deseja continuar habilitando?" +"Habilitar a altura Z precisa e a torre de preparação juntas pode causar " +"erros de fatiamento. Deseja habilitar mesmo assim?" msgid "" "A prime tower is required for clumping detection. There may be flaws on the " @@ -9841,8 +9841,8 @@ msgid "" "Enabling both precise Z height and the prime tower may cause slicing errors. " "Do you still want to enable precise Z height?" msgstr "" -"Habilitar tanto a altura Z precisa quanto a torre de preparação pode causar " -"erros de fatiamento. Deseja continuar habilitando a altura Z precisa?" +"Habilitar a altura Z precisa e a torre de preparação juntas pode causar " +"erros de fatiamento. Deseja habilitar a altura Z precisa mesmo assim?" msgid "" "A prime tower is required for smooth timelapse. There may be flaws on the " @@ -11154,7 +11154,7 @@ msgid "Configuration package changed" msgstr "Pacote de configuração alterado" msgid "Toolbar" -msgstr "Barra de ferramentas" +msgstr "Barra de Ferramentas" msgid "Objects list" msgstr "Lista de objetos" @@ -11885,7 +11885,7 @@ msgstr "" msgid "" "Enabling both precise Z height and the prime tower may cause slicing errors." msgstr "" -"Habilitar tanto a altura Z precisa quanto a torre de preparação pode causar " +"Habilitar a altura Z precisa e a torre de preparação juntas pode causar " "erros de fatiamento." msgid "" @@ -13947,7 +13947,7 @@ msgstr "" "adaptativa deve ser expandida nas direções XY." msgid "Grab length" -msgstr "Comprimento de agarre" +msgstr "Comprimento de pega" msgid "Extruder Color" msgstr "Cor da Extrusora" @@ -14631,7 +14631,7 @@ msgstr "" "suporte." msgid "Filament ramming length" -msgstr "Comprimento de compactação do filamento" +msgstr "Comprimento de moldeamento do filamento" msgid "" "When changing the extruder, it is recommended to extrude a certain length of " @@ -17427,7 +17427,7 @@ msgid "Purge remaining filament into prime tower." msgstr "Purgar o filamento restante na torre de preparo." msgid "Enable filament ramming" -msgstr "Habilitar compactação de filamento" +msgstr "Habilitar moldeamento de filamento" msgid "No sparse layers (beta)" msgstr "Sem camadas esparsas (beta)" @@ -19904,13 +19904,13 @@ msgid "Record Factor" msgstr "Registrar Fator" msgid "We found the best flow ratio for you" -msgstr "Encontramos o melhor fluxo para você" +msgstr "Encontramos a melhor taxa de fluxo para você" msgid "Flow Ratio" -msgstr "Fluxo" +msgstr "Taxa de Fluxo" msgid "Please input a valid value (0.0 < flow ratio < 2.0)" -msgstr "Por favor insira um valor válido (0.0 < fluxo < 2.0)" +msgstr "Por favor insira um valor válido (0.0 < taxa de fluxo < 2.0)" msgid "Please enter the name of the preset you want to save." msgstr "Por favor insira o nome da predefinição que você deseja salvar." @@ -19932,7 +19932,7 @@ msgstr "Pular Calibração2" #, c-format, boost-format msgid "flow ratio : %s " -msgstr "fluxo: %s " +msgstr "taxa de fluxo: %s " msgid "Please choose a block with smoothest top surface." msgstr "Por favor escolha um bloco com a superfície superior mais lisa." @@ -19948,7 +19948,7 @@ msgid "Complete Calibration" msgstr "Calibração Completa" msgid "Fine Calibration based on flow ratio" -msgstr "Calibração Fina baseada no fluxo" +msgstr "Calibração Fina baseada na taxa de fluxo" msgid "Title" msgstr "Título" @@ -20404,7 +20404,7 @@ msgstr "" "DampingEnd <= 1)" msgid "Cornering test" -msgstr "Teste de curvas" +msgstr "Teste de quinas" msgid "SCV-V2" msgstr "SCV-V2" @@ -20416,11 +20416,11 @@ msgid "End: " msgstr "Final: " msgid "Cornering settings" -msgstr "Configurações de curvas" +msgstr "Configurações de quinas" msgid "Note: Lower values = sharper corners but slower speeds." msgstr "" -"Nota: Valores mais baixos = cantos mais nítidos, mas velocidades mais lentas." +"Nota: Valores mais baixos = quinas mais nítidas, mas velocidades mais lentas." msgid "" "Marlin 2 Junction Deviation detected:\n" @@ -20453,7 +20453,7 @@ msgid "" "(0 <= Cornering <= %s)" msgstr "" "Por favor, insira valores válidos:\n" -"(0 <= Curvas <= %s)" +"(0 <= Quinas <= %s)" #, c-format, boost-format msgid "NOTE: High values may cause Layer shift (>%s)" @@ -22317,7 +22317,7 @@ msgid "Restart Later" msgstr "Reiniciar Depois" msgid "NO RAMMING AT ALL" -msgstr "SEM COMPACTAÇÃO" +msgstr "SEM NENHUM MOLDEAMENTO" msgid "Volumetric speed" msgstr "Velocidade volumétrica" @@ -22770,9 +22770,6 @@ msgstr "" "aumentar adequadamente a temperatura da mesa aquecida pode reduzir a " "probabilidade de empenamento?" -#~ msgid "BambuStudio warning" -#~ msgstr "Aviso do Bambu Studio" - #~ msgid "" #~ "Zero first layer height is invalid.\n" #~ "\n" @@ -22789,30 +22786,12 @@ msgstr "" #~ "A posição da torre de preparo ultrapassou os limites da placa de " #~ "impressão e foi reposicionada para a borda válida mais próxima." -#~ msgid "Check for Update" -#~ msgstr "Verificar Atualizações" - -#~ msgid "Detect spaghetti failure(scattered lose filament)." -#~ msgstr "Detectar falha de emaranhado (filamentos soltos e dispersos)." - -#~ msgid "Rotate of view" -#~ msgstr "Rotação da vista" - -#~ msgid "Move of view" -#~ msgstr "Movimento da vista" - -#~ msgid "Zoom of view" -#~ msgstr "Zoom da vista" - #~ msgid "" #~ "Enabling both precise Z height and the prime tower may cause the size of " #~ "prime tower to increase. Do you still want to enable?" #~ msgstr "" -#~ "Habilitar a altura Z precisa e a torre de preparo pode fazer com que o " -#~ "tamanho da torre de preparo aumente. Deseja habilitar mesmo assim?" - -#~ msgid "Extruders count" -#~ msgstr "Número de extrusoras" +#~ "Habilitar a altura Z precisa e a torre de preparação juntas pode causar " +#~ "o aumento de tamanho da torre de preparo. Deseja habilitar mesmo assim?" #~ msgid "" #~ "Missing BambuSource component registered for media playing! Please re-" @@ -23100,11 +23079,6 @@ msgstr "" #~ msgid "Junction Deviation settings" #~ msgstr "Configurações de Desvio de Junção" -#~ msgid "Note: Lower values = sharper corners but slower speeds" -#~ msgstr "" -#~ "Nota: Valores mais baixos = cantos mais nítidos, mas velocidades mais " -#~ "lentas" - #~ msgid "" #~ "Please input valid values:\n" #~ "(0 <= Junction Deviation < 1)" From 9282d2de1df175e5aa9e648b6b95903226b86d9d Mon Sep 17 00:00:00 2001 From: Vovodroid Date: Tue, 7 Apr 2026 17:20:33 +0300 Subject: [PATCH 02/10] Fix arc fitting time progress with firmware retraction. (#12888) --- src/libslic3r/GCode/GCodeProcessor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index f7c9d51cb5..8876cbeede 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4772,17 +4772,21 @@ void GCodeProcessor::process_G29(const GCodeReader::GCodeLine& line) void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line) { + // Emulate G1 retract, decrement G1 count as it's will be incremented in process_G1, but it's fake G1 GCodeReader::GCodeLine g10; g10.set(Axis::E, -this->m_parser.config().retraction_length.get_at(m_extruder_id)); g10.set(Axis::F, this->m_parser.config().retraction_speed.get_at(m_extruder_id) * 60); + --m_g1_line_id; process_G1(g10); } void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line) { + // Emulate G1 unretract, decrement G1 count as it's will be incremented in process_G1, but it's fake G1 GCodeReader::GCodeLine g11; g11.set(Axis::E, this->m_parser.config().retraction_length.get_at(m_extruder_id) + this->m_parser.config().retract_restart_extra.get_at(m_extruder_id)); g11.set(Axis::F, this->m_parser.config().deretraction_speed.get_at(m_extruder_id) * 60); + --m_g1_line_id; process_G1(g11); } From 2829b8eabbb370edf91054438f5146ff78571c88 Mon Sep 17 00:00:00 2001 From: mosfet80 <10235105+mosfet80@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:21:26 +0200 Subject: [PATCH 03/10] Update CMake policy version and minimum requirements (#12905) Removed the old control code for cmake inside the CmakeList.txt file --- CMakeLists.txt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88cb23129e..3b673708a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,9 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0") - set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE) + set(CMAKE_POLICY_VERSION_MINIMUM 3.13 CACHE STRING "" FORCE) endif() cmake_minimum_required(VERSION 3.13) -# Verify that your CMake version is exactly 3.5 series or higher on windows -if ( (MSVC OR WIN32) AND (${CMAKE_VERSION} VERSION_LESS "3.5") ) - message(FATAL_ERROR "CMake current version ${CMAKE_VERSION} is too old. Minimum required is 3.5.") -endif() # The following line used to be in tests/CMakeLists.txt # Having it there causes rebuilds of all targets on any CMakeLists.txt change under tests/ @@ -377,11 +373,6 @@ endif () if (CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(PkgConfig REQUIRED) - if (CMAKE_VERSION VERSION_LESS "3.1") - # Workaround for an old CMake, which does not understand CMAKE_CXX_STANDARD. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") - endif() - # Boost on Raspberry-Pi does not link to pthreads. set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) From 9478af93e454d398a9263efad4b82b08862c34cb Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:22:43 -0300 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=92=A5Fix=20Crash=20with=20painted?= =?UTF-8?q?=20Line=20in=20=20some=20corrupted=20models=20(#12806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safeguard EdgeGrid.hpp --- src/libslic3r/EdgeGrid.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libslic3r/EdgeGrid.hpp b/src/libslic3r/EdgeGrid.hpp index 744a23e186..6bbed1e9fa 100644 --- a/src/libslic3r/EdgeGrid.hpp +++ b/src/libslic3r/EdgeGrid.hpp @@ -224,6 +224,8 @@ public: iy += 1; assert(iy <= iyb); } + if (ix < 0 || iy < 0 || ix >= (int64_t)m_cols || iy >= (int64_t)m_rows) + return; if (! visitor(iy, ix)) return; } while (ix != ixb || iy != iyb); @@ -245,6 +247,8 @@ public: iy -= 1; assert(iy >= iyb); } + if (ix < 0 || iy < 0 || ix >= (int64_t)m_cols || iy >= (int64_t)m_rows) + return; if (! visitor(iy, ix)) return; } while (ix != ixb || iy != iyb); @@ -270,6 +274,8 @@ public: iy += 1; assert(iy <= iyb); } + if (ix < 0 || iy < 0 || ix >= (int64_t)m_cols || iy >= (int64_t)m_rows) + return; if (! visitor(iy, ix)) return; } while (ix != ixb || iy != iyb); @@ -307,6 +313,8 @@ public: iy -= 1; assert(iy >= iyb); } + if (ix < 0 || iy < 0 || ix >= (int64_t)m_cols || iy >= (int64_t)m_rows) + return; if (! visitor(iy, ix)) return; } while (ix != ixb || iy != iyb); From 208ebfc70316c6e540fcd9c2b00c960c10c4230d Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:24:46 -0300 Subject: [PATCH 05/10] Combine brims option (Prusa style brims) (#12343) * Union ex brims Revert "Union ex brims" This reverts commit bbc9a39faf318dc2df093eb2bdcebf19a4162fe9. Update Brim.cpp * dont repeat paths * Update Brim.cpp * multimaterial brim independiente * Normal brim if is by object * fix print order * cleaning 1 * cleaning 2 * Normal brim if multimaterial on first layer * fix artifact * combine_brim optional refactoring * refactoring gcode.cpp * refactoring brim.cpp Update Brim.cpp * Remove multimaterial first-layer check for brims Stop detecting extruders used on the first layer and remove the is_multimaterial_first_layer guard. Simplify can_combine_brims to only consider combine_brims and whether printing is ByObject, allowing brims to be combined across extruders unless printing by object or combine_brims is disabled. Cleans up unused code and simplifies brim-generation conditions. * Remove material specification from unified brim comment * Update PrintConfig.cpp --- src/libslic3r/Brim.cpp | 67 +++++++++++++++++++++++---- src/libslic3r/GCode.cpp | 33 +++++++++++++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 7 +++ src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 1 + src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 1 + 8 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index 35d971a465..8b245e82c5 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -928,16 +928,63 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_ for (size_t iia = 0; iia < islands_area.size(); ++iia) islands_area[iia].translate(plate_shift); - for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) { - if (!iter->second.empty()) { - brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area))); - }; - } - for (auto iter = supportBrimAreaMap.begin(); iter != supportBrimAreaMap.end(); ++iter) { - if (!iter->second.empty()) { - supportBrimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area))); - }; + const bool combine_brims = print.config().combine_brims.value; + const bool is_by_object = (print.config().print_sequence == PrintSequence::ByObject); + const bool can_combine_brims = combine_brims && !is_by_object; + + if (!can_combine_brims) { + // Orca: Generate brims separately for each object when multiple extruders are used + for (auto iter = brimAreaMap.begin(); iter != brimAreaMap.end(); ++iter) { + if (!iter->second.empty()) { + brimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area))); + }; + } + for (auto iter = supportBrimAreaMap.begin(); iter != supportBrimAreaMap.end(); ++iter) { + if (!iter->second.empty()) { + supportBrimMap.insert(std::make_pair(iter->first, makeBrimInfill(iter->second, print, islands_area))); + }; + } + } else { + // Orca: Unified brim mode (non-sequential printing) + ExPolygons all_brims_merged; + std::vector brim_object_ids; + + // Add all object brims + for (auto& [obj_id, brims] : brimAreaMap) { + if (!brims.empty()) { + expolygons_append(all_brims_merged, brims); + brim_object_ids.push_back(obj_id); + } + } + + if (!all_brims_merged.empty()) { + // Merge all brims into a single continuous area + all_brims_merged = union_ex(all_brims_merged); + + // Apply a tiny morphological cleanup to reduce boolean-union micro-artifacts. + const float brim_cleanup_delta = std::max(float(scaled_resolution), float(SCALED_EPSILON)); + all_brims_merged = offset2_ex(all_brims_merged, brim_cleanup_delta, -brim_cleanup_delta, jtRound, scaled_resolution); + + // Generate infill once for the merged brim area. + ExtrusionEntityCollection merged_brim = makeBrimInfill(all_brims_merged, print, islands_area); + + // In unified mode, assign the merged brim to a deterministic carrier object. + // Pick the first object in print order that actually contributed brim area. + ObjectID carrier_id; + bool carrier_found = false; + for (const auto& [obj_id, _extruder] : objPrintVec) { + if (std::find(brim_object_ids.begin(), brim_object_ids.end(), obj_id) != brim_object_ids.end()) { + carrier_id = obj_id; + carrier_found = true; + break; + } + } + + if (!carrier_found) + carrier_id = brim_object_ids.front(); + + brimMap[carrier_id] = std::move(merged_brim); + } } } - } // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fe10cbcd41..ba47fac081 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -4993,6 +4993,39 @@ LayerResult GCode::process_layer( bool has_insert_wrapping_detection_gcode = false; // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. + // Orca: Print unified global brim before any object. + // Only do this if `combine_brims` is enabled and we are printing by layer. + if (first_layer && sequence_by_layer && m_config.combine_brims && !print.m_brimMap.empty()) { + const ObjectID unified_object_id = [&]() -> ObjectID { + ObjectID id; + bool found = false; + for (const auto& [obj_id, brim] : print.m_brimMap) { + const bool has_printable_entities = std::any_of(brim.entities.begin(), brim.entities.end(), + [](const ExtrusionEntity* ee) { return ee != nullptr; }); + if (!has_printable_entities) + continue; + if (found) + return ObjectID(); + id = obj_id; + found = true; + } + return found ? id : ObjectID(); + }(); + + if (unified_object_id.valid()) { + const auto it = print.m_brimMap.find(unified_object_id); + if (it != print.m_brimMap.end()) { + this->set_origin(0., 0.); + for (const ExtrusionEntity* ee : it->second.entities) + if (ee != nullptr) + gcode += this->extrude_entity(*ee, "brim", m_config.support_speed.value); + + // Mark brim as printed for this object to avoid per-object brim emission later. + this->m_objsWithBrim.erase(unified_object_id); + } + } + } + for (unsigned int extruder_id : layer_tools.extruders) { if (print.config().skirt_type == stCombined && !print.skirt().empty()) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 826d8a1a19..f8001d2806 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -904,7 +904,7 @@ static std::vector s_Preset_print_options { "top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed", "bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed", "outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield", - "brim_width", "brim_object_gap", "brim_use_efc_outline", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", + "brim_width", "brim_object_gap", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers", "raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion", "support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style", // BBS diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index c4b3c28ad9..d609fdb454 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1594,6 +1594,13 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("combine_brims", coBool); + def->label = L("Combine brims"); + def->category = L("Support"); + def->tooltip = L("Combine multiple brims into one when they are close to each other. This can improve brim adhesion."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("brim_ears", coBool); def->label = L("Brim ears"); def->category = L("Support"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 54c26c312a..425603bd08 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1550,6 +1550,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionString, thumbnails)) // BBS: move from PrintObjectConfig ((ConfigOptionBool, independent_support_layer_height)) + ((ConfigOptionBool, combine_brims)) // SoftFever ((ConfigOptionPercents, filament_shrink)) ((ConfigOptionPercents, filament_shrinkage_compensation_z)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index f3f98a6fad..377f6f5ae6 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -706,6 +706,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool have_brim = (config->opt_enum("brim_type") != btNoBrim); toggle_field("brim_object_gap", have_brim); toggle_field("brim_use_efc_outline", have_brim); + toggle_field("combine_brims", have_brim); bool have_brim_width = (config->opt_enum("brim_type") != btNoBrim) && config->opt_enum("brim_type") != btAutoBrim && config->opt_enum("brim_type") != btPainted; toggle_field("brim_width", have_brim_width); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 64274adf19..86144a227f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4809,7 +4809,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "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_use_efc_outline", "brim_type", "nozzle_diameter", "single_extruder_multi_material", "preferred_orientation", + "brim_width", "brim_object_gap", "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", "material_colour", "printable_height", "extruder_printable_height", "printer_model", "printer_technology", diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c94fd1c7a4..c2cac5519f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2658,6 +2658,7 @@ void TabPrint::build() optgroup->append_single_option_line("brim_width", "others_settings_brim#width"); optgroup->append_single_option_line("brim_object_gap", "others_settings_brim#brim-object-gap"); optgroup->append_single_option_line("brim_use_efc_outline", "others_settings_brim#brim-use-efc-outline"); + optgroup->append_single_option_line("combine_brims", "others_settings_brim#combine-brim"); optgroup->append_single_option_line("brim_ears_max_angle", "others_settings_brim#ear-max-angle"); optgroup->append_single_option_line("brim_ears_detection_length", "others_settings_brim#ear-detection-radius"); From 6975b5d84cd2000365237e1cbb384ab40b8f47ba Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:25:28 -0300 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=92=A5Fix=20crash=20-=20inconsisten?= =?UTF-8?q?t=20draft=20shield=20layer=20height=20(#12937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix crash-inconsistent draft shield * Skip empty skirt G-code and fix skirt_done handling Track whether a skirt was actually generated for the current print Z and avoid appending empty skirt G-code. Only pop m_skirt_done when a skirt was emitted to prevent corrupting skirt state across multiple instances per layer. Also use a temporary string (moved into the main gcode) to detect emptiness before updating state. Preserves existing first-layer clearing behavior. --- src/libslic3r/GCode.cpp | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ba47fac081..ed7f153ab3 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5137,19 +5137,37 @@ LayerResult GCode::process_layer( if (is_anything_overridden && print_wipe_extrusions == 0) gcode+="; PURGING FINISHED\n"; + bool skirt_generated_for_current_print_z = false; + for (InstanceToPrint &instance_to_print : instances_to_print) { if (print.config().skirt_type == stPerObject && !instance_to_print.print_object.object_skirt().empty() && - print.config().print_sequence == PrintSequence::ByLayer - && - (layer.id() < print.config().skirt_height || print.config().draft_shield == DraftShield::dsEnabled)) + print.config().print_sequence == PrintSequence::ByLayer) { - if (first_layer) - m_skirt_done.clear(); - const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; - gcode += generate_skirt(print, instance_to_print.print_object.object_skirt(), offset, instance_to_print.print_object.config().skirt_start_angle, layer_tools, layer, extruder_id); - if (instances_to_print.size() > 1 && &instance_to_print != &*(instances_to_print.end() - 1)) - m_skirt_done.pop_back(); + const LayerToPrint& layer_to_print = layers[instance_to_print.layer_id]; + const Layer* skirt_layer = layer_to_print.object_layer; + if (skirt_layer == nullptr && layer_to_print.support_layer != nullptr && + layer_to_print.support_layer->id() < layer_to_print.support_layer->object()->slicing_parameters().raft_layers()) { + skirt_layer = layer_to_print.support_layer; + } + + if (skirt_layer != nullptr && + (skirt_layer->id() < print.config().skirt_height || print.config().draft_shield == DraftShield::dsEnabled)) { + const bool skirt_first_layer = (skirt_layer->id() == 0 && std::abs(skirt_layer->bottom_z()) < EPSILON); + if (skirt_first_layer) + m_skirt_done.clear(); + + if (skirt_generated_for_current_print_z && !m_skirt_done.empty()) + m_skirt_done.pop_back(); + + const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; + std::string skirt_gcode = generate_skirt(print, instance_to_print.print_object.object_skirt(), offset, + instance_to_print.print_object.config().skirt_start_angle, layer_tools, + *skirt_layer, extruder_id); + if (!skirt_gcode.empty()) + skirt_generated_for_current_print_z = true; + gcode += std::move(skirt_gcode); + } } const auto& inst = instance_to_print.print_object.instances()[instance_to_print.instance_id]; From e1d6cb1764e167d55781601f9dc807a3ed5212ee Mon Sep 17 00:00:00 2001 From: Vovodroid Date: Tue, 7 Apr 2026 17:33:32 +0300 Subject: [PATCH 07/10] Remove "auto" wall direction option (#6193) Remove "auto" wall direction --- src/libslic3r/GCode.cpp | 7 ++++-- src/libslic3r/PerimeterGenerator.cpp | 32 ++++++++++++++------------- src/libslic3r/PrintConfig.cpp | 8 +++---- src/libslic3r/PrintConfig.hpp | 1 - src/slic3r/GUI/ConfigManipulation.cpp | 7 +----- src/slic3r/GUI/PartPlate.cpp | 1 - 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ed7f153ab3..04a2df48db 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5600,7 +5600,10 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou if (m_config.spiral_mode && !is_hole) { // if spiral vase, we have to ensure that all contour are in the same orientation. - loop.make_counter_clockwise(); + if (m_config.wall_direction == WallDirection::CounterClockwise) + loop.make_counter_clockwise(); + else + loop.make_clockwise(); } //if (loop.loop_role() == elrSkirt && (this->m_layer->id() % 2 == 1)) // loop.reverse(); @@ -5662,7 +5665,7 @@ std::string GCode::extrude_loop(ExtrusionLoop loop, std::string description, dou // 1 - the currently printed external perimeter and 2 - the neighbouring internal perimeter. if (m_config.wipe_before_external_loop.value && !paths.empty() && paths.front().size() > 1 && paths.back().size() > 1 && paths.front().role() == erExternalPerimeter && region_perimeters.size() > 1) { const bool is_full_loop_ccw = loop.polygon().is_counter_clockwise(); - bool is_hole_loop = (loop.loop_role() & ExtrusionLoopRole::elrHole) != 0; // loop.make_counter_clockwise(); + bool is_hole_loop = (loop.loop_role() & ExtrusionLoopRole::elrHole) != 0; const double nozzle_diam = nozzle_diameter; // note: previous & next are inverted to extrude "in the opposite direction, and we are "rewinding" diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index cc055c9d8a..7da2f20725 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -253,7 +253,10 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime ExtrusionLoop *eloop = static_cast(coll.entities[idx.first]); coll.entities[idx.first] = nullptr; - eloop->make_counter_clockwise(); + if (perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) + eloop->make_counter_clockwise(); + else + eloop->make_clockwise(); eloop->inset_idx = loop.depth; if (loop.is_contour) { out.append(std::move(children.entities)); @@ -511,7 +514,10 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p if (!paths.empty()) { if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); - extrusion_loop.make_counter_clockwise(); + if (perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) + extrusion_loop.make_counter_clockwise(); + else + extrusion_loop.make_clockwise(); // TODO: it seems in practice that ExtrusionLoops occasionally have significantly disconnected paths, // triggering the asserts below. Is this a problem? for (auto it = std::next(extrusion_loop.paths.begin()); it != extrusion_loop.paths.end(); ++it) { @@ -1106,7 +1112,7 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_ } if (need_reverse && !isExternal) { - eloop->make_clockwise(); + eloop->reverse(); } } } @@ -1414,18 +1420,17 @@ void PerimeterGenerator::process_classic() // at this point, all loops should be in contours[0] bool steep_overhang_contour = false; bool steep_overhang_hole = false; - const WallDirection wall_direction = config->wall_direction; - if (wall_direction != WallDirection::Auto) { - // Skip steep overhang detection if wall direction is specified + if (!config->overhang_reverse) { + // Skip steep overhang detection no reverse is specified steep_overhang_contour = true; steep_overhang_hole = true; } ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole); // All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want - if (wall_direction != WallDirection::CounterClockwise) { + if (config->overhang_reverse) { reorient_perimeters(entities, steep_overhang_contour, steep_overhang_hole, // Reverse internal only if the wall direction is auto - this->config->overhang_reverse_internal_only && wall_direction == WallDirection::Auto); + this->config->overhang_reverse_internal_only); } // if brim will be printed, reverse the order of perimeters so that @@ -2444,18 +2449,15 @@ void PerimeterGenerator::process_arachne() bool steep_overhang_contour = false; bool steep_overhang_hole = false; - const WallDirection wall_direction = config->wall_direction; - if (wall_direction != WallDirection::Auto) { - // Skip steep overhang detection if wall direction is specified + if (!config->overhang_reverse) { + // Skip steep overhang detection no reverse is specified steep_overhang_contour = true; steep_overhang_hole = true; } if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { - // All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want - if (wall_direction != WallDirection::CounterClockwise) { + if (config->overhang_reverse) { reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, - // Reverse internal only if the wall direction is auto - this->config->overhang_reverse_internal_only && wall_direction == WallDirection::Auto); + this->config->overhang_reverse_internal_only); } this->loops->append(extrusion_coll); } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d609fdb454..d83a0aeba3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -262,7 +262,6 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(WallSequence) //Orca static t_config_enum_values s_keys_map_WallDirection{ - { "auto", int(WallDirection::Auto) }, { "ccw", int(WallDirection::CounterClockwise) }, { "cw", int(WallDirection::Clockwise)}, }; @@ -2028,14 +2027,12 @@ void PrintConfigDef::init_fff_params() def->category = L("Quality"); def->tooltip = L("The direction which the wall loops are extruded when looking down from the top.\n\nBy default all walls are extruded in counter-clockwise, unless Reverse on even is enabled. Set this to any option other than Auto will force the wall direction regardless of the Reverse on even.\n\nThis option will be disabled if spiral vase mode is enabled."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); - def->enum_values.push_back("auto"); def->enum_values.push_back("ccw"); def->enum_values.push_back("cw"); - def->enum_labels.push_back(L("Auto")); def->enum_labels.push_back(L("Counter clockwise")); def->enum_labels.push_back(L("Clockwise")); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionEnum(WallDirection::Auto)); + def->set_default_value(new ConfigOptionEnum(WallDirection::CounterClockwise)); def = this->add("extruder", coInt); def->gui_type = ConfigOptionDef::GUIType::i_enum_open; @@ -7692,6 +7689,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "machine_switch_extruder_time") { opt_key = "machine_tool_change_time"; } + else if (opt_key == "wall_direction" && value == "auto") { + value = "ccw"; + } // Ignore the following obsolete configuration keys: static std::set ignore = { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 425603bd08..7d40ef0706 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -126,7 +126,6 @@ enum class WallSequence { // Orca enum class WallDirection { - Auto, CounterClockwise, Clockwise, Count, diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 377f6f5ae6..e41886008b 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -327,7 +327,6 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con config->opt_int("enforce_support_layers") == 0 && ! config->opt_bool("detect_thin_wall") && ! config->opt_bool("overhang_reverse") && - config->opt_enum("wall_direction") == WallDirection::Auto && config->opt_enum("timelapse_type") == TimelapseType::tlTraditional && !config->opt_bool("enable_wrapping_detection"))) { @@ -342,7 +341,6 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con new_conf.set_key_value("enforce_support_layers", new ConfigOptionInt(0)); new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false)); new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false)); - new_conf.set_key_value("wall_direction", new ConfigOptionEnum(WallDirection::Auto)); new_conf.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); new_conf.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); sparse_infill_density = 0; @@ -661,8 +659,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co toggle_field("top_shell_thickness", ! has_spiral_vase && has_top_shell); toggle_field("bottom_shell_thickness", ! has_spiral_vase && has_bottom_shell); - toggle_field("wall_direction", !has_spiral_vase); - // Gap fill is newly allowed in between perimeter lines even for empty infill (see GH #1476). toggle_field("gap_infill_speed", have_perimeters); @@ -906,8 +902,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall"); bool has_overhang_reverse = config->opt_bool("overhang_reverse"); - bool force_wall_direction = config->opt_enum("wall_direction") != WallDirection::Auto; - bool allow_overhang_reverse = !has_spiral_vase && !force_wall_direction; + bool allow_overhang_reverse = !has_spiral_vase; toggle_line("overhang_reverse", allow_overhang_reverse); toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse); bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only"); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 98c285f46a..37baa25a5f 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2812,7 +2812,6 @@ void PartPlate::set_vase_mode_related_object_config(int obj_id) { new_conf.set_key_value("detect_thin_wall", new ConfigOptionBool(false)); new_conf.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); new_conf.set_key_value("overhang_reverse", new ConfigOptionBool(false)); - new_conf.set_key_value("wall_direction", new ConfigOptionEnum(WallDirection::Auto)); auto applying_keys = global_config->diff(new_conf); for (ModelObject* object : obj_ptrs) { From 4d1efad5e9f18cb72b67b28f64dfe87796f78d8b Mon Sep 17 00:00:00 2001 From: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:46:39 -0300 Subject: [PATCH 08/10] Inverse print direction for holes (#12669) * Remove "auto" wall direction * hole direction reverse Co-Authored-By: Ian Bassi <12130714+ianalexis@users.noreply.github.com> Co-Authored-By: discip <53649486+discip@users.noreply.github.com> * Update description Co-Authored-By: Ian Bassi <12130714+ianalexis@users.noreply.github.com> * Improve description * XNOR logic * Classic wall generator XNOR * thin wall holes arachne * thin wall holes classic * better handling of thin wall holes arachne Co-Authored-By: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> * Invert print order for thin wall holes * Reverse path order for thin wall holes under condition * avoid reverse Co-Authored-By: Ian Bassi <12130714+ianalexis@users.noreply.github.com> Co-Authored-By: Ian Bassi --------- Co-authored-by: vovodroid Co-authored-by: Ian Bassi <12130714+ianalexis@users.noreply.github.com> Co-authored-by: discip <53649486+discip@users.noreply.github.com> Co-authored-by: Ian Bassi Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Co-authored-by: SoftFever --- src/libslic3r/PerimeterGenerator.cpp | 27 ++++++++++++++++++++------- src/libslic3r/PrintConfig.cpp | 2 +- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 7da2f20725..1fa4d58bb0 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -98,7 +98,7 @@ static bool detect_steep_overhang(const PrintRegionConfig *config, } static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls, - bool &steep_overhang_contour, bool &steep_overhang_hole) + bool &steep_overhang_contour, bool &steep_overhang_hole, bool reverse_thin_wall_hole) { // loops is an arrayref of ::Loop objects // turn each one into an ExtrusionLoop object @@ -248,15 +248,24 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime } else { const PerimeterGeneratorLoop &loop = loops[idx.first]; assert(thin_walls.empty()); - ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour, steep_overhang_hole); + const bool reverse_children_thin_wall_hole = loops.size() == 1 && loop.is_contour && loop.children.size() == 1 && + (!loop.children.front().is_contour) && loop.children.front().children.empty(); + ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour, + steep_overhang_hole, reverse_children_thin_wall_hole); out.entities.reserve(out.entities.size() + children.entities.size() + 1); ExtrusionLoop *eloop = static_cast(coll.entities[idx.first]); coll.entities[idx.first] = nullptr; - if (perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) + if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (loop.is_contour || reverse_thin_wall_hole)) eloop->make_counter_clockwise(); else eloop->make_clockwise(); + + // Orca: Reverse print order for thin wall holes. + if (reverse_thin_wall_hole) { + std::reverse(out.entities.begin(), out.entities.end()); + } + eloop->inset_idx = loop.depth; if (loop.is_contour) { out.append(std::move(children.entities)); @@ -514,10 +523,11 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p if (!paths.empty()) { if (extrusion->is_closed) { ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); - if (perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) + if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == + (pg_extrusion.is_contour || pg_extrusions.size() == 2)) extrusion_loop.make_counter_clockwise(); else - extrusion_loop.make_clockwise(); + extrusion_loop.make_clockwise(); // TODO: it seems in practice that ExtrusionLoops occasionally have significantly disconnected paths, // triggering the asserts below. Is this a problem? for (auto it = std::next(extrusion_loop.paths.begin()); it != extrusion_loop.paths.end(); ++it) { @@ -525,8 +535,11 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p assert(std::prev(it)->polyline.last_point() == it->polyline.first_point()); } assert(extrusion_loop.paths.front().first_point() == extrusion_loop.paths.back().last_point()); - extrusion_coll.append(std::move(extrusion_loop)); + // Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter. + const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2; + if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner) + std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end()); } else { // Because we are processing one ExtrusionLine all ExtrusionPaths should form one connected path. @@ -1425,7 +1438,7 @@ void PerimeterGenerator::process_classic() steep_overhang_contour = true; steep_overhang_hole = true; } - ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole); + ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole, false); // All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want if (config->overhang_reverse) { reorient_perimeters(entities, steep_overhang_contour, steep_overhang_hole, diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d83a0aeba3..4428241e22 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2025,7 +2025,7 @@ void PrintConfigDef::init_fff_params() def = this->add("wall_direction", coEnum); def->label = L("Wall loop direction"); def->category = L("Quality"); - def->tooltip = L("The direction which the wall loops are extruded when looking down from the top.\n\nBy default all walls are extruded in counter-clockwise, unless Reverse on even is enabled. Set this to any option other than Auto will force the wall direction regardless of the Reverse on even.\n\nThis option will be disabled if spiral vase mode is enabled."); + def->tooltip = L("The direction which the contour wall loops are extruded when looking down from the top.\nHoles are printed in the opposite direction to the contour to maintain alignment with layers whose contour polygons are incomplete and change direction, also partially forming the contour of a hole.. \n\nThis option will be disabled if spiral vase mode is enabled."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("ccw"); def->enum_values.push_back("cw"); From f685891b6e8f2d17ea8f4df5469d7f214983fbef Mon Sep 17 00:00:00 2001 From: Ian Bassi Date: Tue, 7 Apr 2026 16:06:20 -0300 Subject: [PATCH 09/10] Fix: Wiki + Wall loop direction desc (#13143) * Combine brims wiki fix * Typo fix --- src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 4428241e22..d4711a0da9 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2025,7 +2025,7 @@ void PrintConfigDef::init_fff_params() def = this->add("wall_direction", coEnum); def->label = L("Wall loop direction"); def->category = L("Quality"); - def->tooltip = L("The direction which the contour wall loops are extruded when looking down from the top.\nHoles are printed in the opposite direction to the contour to maintain alignment with layers whose contour polygons are incomplete and change direction, also partially forming the contour of a hole.. \n\nThis option will be disabled if spiral vase mode is enabled."); + def->tooltip = L("The direction which the contour wall loops are extruded when looking down from the top.\nHoles are printed in the opposite direction to the contour to maintain alignment with layers whose contour polygons are incomplete and change direction, also partially forming the contour of a hole.\n\nThis option will be disabled if spiral vase mode is enabled."); def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values.push_back("ccw"); def->enum_values.push_back("cw"); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c2cac5519f..35c85b36c1 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2658,7 +2658,7 @@ void TabPrint::build() optgroup->append_single_option_line("brim_width", "others_settings_brim#width"); optgroup->append_single_option_line("brim_object_gap", "others_settings_brim#brim-object-gap"); optgroup->append_single_option_line("brim_use_efc_outline", "others_settings_brim#brim-use-efc-outline"); - optgroup->append_single_option_line("combine_brims", "others_settings_brim#combine-brim"); + optgroup->append_single_option_line("combine_brims", "others_settings_brim#combine-brims"); optgroup->append_single_option_line("brim_ears_max_angle", "others_settings_brim#ear-max-angle"); optgroup->append_single_option_line("brim_ears_detection_length", "others_settings_brim#ear-detection-radius"); From 5fa6480f6188f94a2789b122347048818b85a6f4 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Tue, 7 Apr 2026 23:25:22 -0400 Subject: [PATCH 10/10] feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes - Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants) - Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items - Mode changes are recorded in the undo/redo snapshot system - Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown - Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon) - Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook - iXex mode persisted in 3MF project files via existing plate metadata serialization Co-Authored-By: Claude Sonnet 4.6 --- resources/images/plate_ixex_mode.svg | 10 ++ resources/images/plate_ixex_mode_dark.svg | 10 ++ resources/images/plate_ixex_mode_hover.svg | 10 ++ .../images/plate_ixex_mode_hover_dark.svg | 10 ++ src/libslic3r/Format/bbs_3mf.cpp | 10 ++ src/libslic3r/PrintConfig.cpp | 16 ---- src/libslic3r/PrintConfig.hpp | 2 - src/slic3r/GUI/GCodeViewer.cpp | 8 +- src/slic3r/GUI/GLCanvas3D.cpp | 7 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 17 +++- src/slic3r/GUI/PartPlate.cpp | 96 +++++++++++++++++-- src/slic3r/GUI/PartPlate.hpp | 11 ++- src/slic3r/GUI/Plater.cpp | 54 +++++++++++ src/slic3r/GUI/Tab.cpp | 54 ----------- src/slic3r/GUI/Tab.hpp | 2 - 15 files changed, 226 insertions(+), 91 deletions(-) create mode 100644 resources/images/plate_ixex_mode.svg create mode 100644 resources/images/plate_ixex_mode_dark.svg create mode 100644 resources/images/plate_ixex_mode_hover.svg create mode 100644 resources/images/plate_ixex_mode_hover_dark.svg diff --git a/resources/images/plate_ixex_mode.svg b/resources/images/plate_ixex_mode.svg new file mode 100644 index 0000000000..30a67cdf35 --- /dev/null +++ b/resources/images/plate_ixex_mode.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_dark.svg b/resources/images/plate_ixex_mode_dark.svg new file mode 100644 index 0000000000..981fd88447 --- /dev/null +++ b/resources/images/plate_ixex_mode_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover.svg b/resources/images/plate_ixex_mode_hover.svg new file mode 100644 index 0000000000..9463e767df --- /dev/null +++ b/resources/images/plate_ixex_mode_hover.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/resources/images/plate_ixex_mode_hover_dark.svg b/resources/images/plate_ixex_mode_hover_dark.svg new file mode 100644 index 0000000000..301fa995ab --- /dev/null +++ b/resources/images/plate_ixex_mode_hover_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index dcbf638483..b7513ea240 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -333,6 +333,7 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence"; static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums"; static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode"; +static constexpr const char* IXEX_PARALLEL_MODE_ATTR = "ixex_parallel_mode"; static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode"; static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps"; static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps"; @@ -4292,6 +4293,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::istringstream(value) >> std::boolalpha >> spiral_mode; m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode)); } + else if (key == IXEX_PARALLEL_MODE_ATTR) { + m_curr_plater->config.set_key_value("ixex_parallel_mode", new ConfigOptionString(value)); + } else if (key == FILAMENT_MAP_MODE_ATTR) { FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush; @@ -7773,6 +7777,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) if (spiral_mode_opt) stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n"; + { + auto* ixex_mode_opt = plate_data->config.option("ixex_parallel_mode"); + if (ixex_mode_opt && !ixex_mode_opt->value.empty() && ixex_mode_opt->value != "primary") + stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IXEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << ixex_mode_opt->value << "\"/>\n"; + } + //filament map related ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode"); t_config_enum_names filament_map_mode_names = ConfigOptionEnum::get_enum_names(); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 67d2340a9a..30b2ead516 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5510,22 +5510,6 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionInt(2)); - def = this->add("ixex_primary_col", coInt); - def->label = L("Primary Tool Column"); - def->tooltip = L("X index (0-based) of the primary toolhead. 0 = leftmost column. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - - def = this->add("ixex_primary_row", coInt); - def->label = L("Primary Tool Row"); - def->tooltip = L("Y index (0-based) of the primary toolhead's gantry. 0 = front gantry. Determines which zone is the active print area."); - def->min = 0; - def->max = 3; - def->mode = comAdvanced; - def->set_default_value(new ConfigOptionInt(0)); - def = this->add("ixex_tool_layout", coString); def->label = L("Tool 0 Corner"); def->tooltip = L("Physical corner of the bed where tool T0 (index 0) is located. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 83d8b1ab97..bdc384b023 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1338,8 +1338,6 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, is_ixex)) ((ConfigOptionInt, ixex_gantry_count)) ((ConfigOptionInt, ixex_tools_per_gantry)) - ((ConfigOptionInt, ixex_primary_col)) - ((ConfigOptionInt, ixex_primary_row)) ((ConfigOptionFloat, ixex_nozzle_clearance_x)) ((ConfigOptionFloat, ixex_nozzle_clearance_y)) ((ConfigOptionStrings, ixex_mode_names)) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index b4e2a9627c..5163580c30 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1533,7 +1533,13 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin) if (is_ixex_opt && is_ixex_opt->value) { const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config; auto* mode_opt = process_cfg.opt("ixex_parallel_mode"); - const std::string mode = mode_opt ? mode_opt->value : "primary"; + std::string mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode overrides the process preset. + if (auto* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate()) { + std::string plate_mode = plate->get_ixex_mode(); + if (plate_mode != "primary") + mode = plate_mode; + } if (mode != m_ixex_last_mode) { m_sequential_view.m_ixex_secondary_markers.clear(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 5e23a9d4ae..66dc76661d 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4648,9 +4648,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) } //BBS change plate selection + bool plate_icon_popup_shown = false; if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) { int hover_idx = m_hover_plate_idxs.front(); - wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true); + plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1); if (m_hover_volume_idxs.empty()) deselect_all(); render(); @@ -4670,10 +4671,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) { //BBS post right click event - if (!m_hover_plate_idxs.empty()) { + if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) { post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() })); } - else { + else if (!plate_icon_popup_shown) { // do not post the event if the user is panning the scene // or if right click was done over the wipe tower bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 9d1d778504..b4ec570d00 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -542,9 +542,16 @@ void ArrangeJob::process(Ctl &ctl) // When an iXex parallel mode is active, constrain auto-arrange to the primary zone only // and treat carriage collision strips as hard excluded regions. + // NOTE: m_ixex_primary_zone_box and ixex_collision_zones() are in global (world) coordinates + // because they are derived from m_shape which includes the plate origin offset. The arranger + // always works in plate-local space (origin = 0,0), so we subtract the plate origin here. if (PartPlate* curr_plate = partplate_list.get_curr_plate()) { if (auto pz = curr_plate->ixex_primary_zone()) { - BoundingBox scaled_pz = scaled(*pz); + Vec3d plate_origin = curr_plate->get_origin(); + double ox = plate_origin.x(), oy = plate_origin.y(); + BoundingBoxf pz_local(Vec2d(pz->min.x() - ox, pz->min.y() - oy), + Vec2d(pz->max.x() - ox, pz->max.y() - oy)); + BoundingBox scaled_pz = scaled(pz_local); bedpts = { { scaled_pz.min.x(), scaled_pz.min.y() }, { scaled_pz.max.x(), scaled_pz.min.y() }, @@ -553,10 +560,10 @@ void ArrangeJob::process(Ctl &ctl) }; for (const BoundingBoxf3& cz : curr_plate->ixex_collision_zones()) { Polygon poly({ - { scaled(cz.min.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.min.y()) }, - { scaled(cz.max.x()), scaled(cz.max.y()) }, - { scaled(cz.min.x()), scaled(cz.max.y()) }, + { scaled(cz.min.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.min.y() - oy) }, + { scaled(cz.max.x() - ox), scaled(cz.max.y() - oy) }, + { scaled(cz.min.x() - ox), scaled(cz.max.y() - oy) }, }); arrangement::ArrangePolygon ap; ap.poly.contour = poly; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 9d7b6d4e5f..9ea79e3643 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -365,6 +365,34 @@ std::vector PartPlate::get_plate_wrapping_detection_area() const return std::vector(); } +std::string PartPlate::get_ixex_mode() const +{ + if (m_config.has("ixex_parallel_mode")) { + auto* opt = m_config.option("ixex_parallel_mode"); + if (opt && !opt->value.empty()) + return opt->value; + } + return "primary"; +} + +void PartPlate::set_ixex_mode(const std::string& mode) +{ + if (mode.empty() || mode == "primary") { + m_config.erase("ixex_parallel_mode"); + } else { + m_config.set_key_value("ixex_parallel_mode", new ConfigOptionString(mode)); + } + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; // force zone rebuild +} + +void PartPlate::reset_ixex_mode() +{ + m_config.erase("ixex_parallel_mode"); + update_slice_result_valid_state(false); + m_ixex_zones_mode_cache = "\x01"; +} + void PartPlate::set_spiral_vase_mode(bool spiral_mode, bool as_global) { std::string key = "spiral_mode"; @@ -475,9 +503,14 @@ void PartPlate::calc_ixex_zones() if (!is_ixex_opt || !is_ixex_opt->value) return; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); - std::string active_mode = mode_opt ? mode_opt->value : "primary"; + // Per-plate mode takes priority over the process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } if (active_mode == "primary" || active_mode.empty()) return; @@ -767,7 +800,8 @@ void PartPlate::calc_ixex_zones() } // Build a cache key from the current iXex config options, or "" if iXex is off. -static std::string build_ixex_cache_key() +// Reads per-plate mode from m_config first, falling back to the process preset. +std::string PartPlate::build_ixex_cache_key() const { if (!wxGetApp().preset_bundle) return ""; @@ -775,14 +809,20 @@ static std::string build_ixex_cache_key() auto* is_ixex_opt = printer_cfg.option("is_ixex"); if (!is_ixex_opt || !is_ixex_opt->value) return ""; - const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + // Per-plate mode takes priority over process preset. + std::string active_mode = get_ixex_mode(); + if (active_mode == "primary") { + const DynamicPrintConfig& process_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto* mode_opt = process_cfg.option("ixex_parallel_mode"); + if (mode_opt && !mode_opt->value.empty()) + active_mode = mode_opt->value; + } auto* n_col_opt = printer_cfg.option("ixex_tools_per_gantry"); auto* n_row_opt = printer_cfg.option("ixex_gantry_count"); auto* cw_opt = printer_cfg.option("ixex_nozzle_clearance_x"); auto* ch_opt = printer_cfg.option("ixex_nozzle_clearance_y"); auto* mgn_opt = printer_cfg.option("ixex_carriage_margin"); - return (mode_opt ? mode_opt->value : "primary") + return active_mode + "|" + std::to_string(n_col_opt ? n_col_opt->value : 2) + "x" + std::to_string(n_row_opt ? n_row_opt->value : 1) + "|cw" + std::to_string(cw_opt ? (int)cw_opt->value : 0) @@ -1643,6 +1683,20 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id) } else render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture); + // iXex mode icon — only when is_ixex is active + { + PresetBundle* pb = wxGetApp().preset_bundle; + auto* is_ixex_opt = pb ? pb->printers.get_edited_preset().config.option("is_ixex") : nullptr; + if (is_ixex_opt && is_ixex_opt->value) { + if (hover_id == (int)PLATE_IXEX_MODE_ID) { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_hovered_texture); + std::string cur = get_ixex_mode(); + show_tooltip(_u8L("iXex mode: ") + cur + _u8L(" (left-click to cycle, right-click for menu)")); + } else { + render_icon_texture(m_ixex_mode_icon.model, m_partplate_list->m_ixex_mode_texture); + } + } + } if (m_partplate_list->render_plate_settings) { bool has_plate_settings = get_bed_type() != BedType::btDefault || get_print_seq() != PrintSequence::ByDefault || !get_first_layer_print_sequence().empty() || !get_other_layers_print_sequence().empty() || has_spiral_mode_config(); @@ -1952,6 +2006,13 @@ void PartPlate::register_raycasters_for_picking(GLCanvas3D &canvas) bool dual_bbl = (preset && preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); if (dual_bbl) register_model_for_picking(canvas, m_plate_filament_map_icon, picking_id_component(PLATE_FILAMENT_MAP_ID)); + + // Register iXex mode icon only when iXex is active on the current printer preset. + if (preset) { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + register_model_for_picking(canvas, m_ixex_mode_icon, picking_id_component(PLATE_IXEX_MODE_ID)); + } } int PartPlate::picking_id_component(int idx) const @@ -3604,6 +3665,11 @@ bool PartPlate::set_shape(const Pointfs& shape, const Pointfs& exclude_areas, co dual_bbl = (preset->is_bbl_vendor() && preset->get_printer_extruder_count() == 2); calc_vertex_for_icons(dual_bbl ? 5 : 6, m_plate_filament_map_icon); calc_vertex_for_icons(dual_bbl ? 6 : 5, m_move_front_icon); + { + auto* is_ixex_opt = preset->printers.get_edited_preset().config.option("is_ixex"); + if (is_ixex_opt && is_ixex_opt->value) + calc_vertex_for_icons(dual_bbl ? 7 : 6, m_ixex_mode_icon); + } calc_vertex_for_number(0, false, m_plate_idx_icon); // calc vertex for plate name @@ -4473,6 +4539,20 @@ void PartPlateList::generate_icon_textures() } } + // iXex mode icon textures (fall back gracefully if SVG not present yet) + { + file_name = path + (m_is_dark ? "plate_ixex_mode_dark.svg" : "plate_ixex_mode.svg"); + if (!m_ixex_mode_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode icon)") % file_name; + } + } + { + file_name = path + (m_is_dark ? "plate_ixex_mode_hover_dark.svg" : "plate_ixex_mode_hover.svg"); + if (!m_ixex_mode_hovered_texture.load_from_svg_file(file_name, true, false, false, icon_size)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":load file %1% failed (iXex mode hover icon)") % file_name; + } + } + std::string text_str = "01"; // ORCA also scale font size to prevent low res texture int size = wxGetApp().em_unit() * PARTPLATE_ICON_SIZE; @@ -4516,6 +4596,8 @@ void PartPlateList::release_icon_textures() m_plate_set_filament_map_hovered_texture.reset(); m_plate_name_edit_texture.reset(); m_plate_name_edit_hovered_texture.reset(); + m_ixex_mode_texture.reset(); + m_ixex_mode_hovered_texture.reset(); for (int i = 0;i < MAX_PLATE_COUNT; i++) { m_idx_textures[i].reset(); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 4954e6f3b5..773524c77c 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -155,6 +155,7 @@ private: PickingModel m_plate_filament_map_icon; PickingModel m_plate_name_edit_icon; PickingModel m_move_front_icon; + PickingModel m_ixex_mode_icon; GLModel m_plate_idx_icon; GLTexture m_texture; @@ -188,6 +189,7 @@ private: void calc_height_limit(); void calc_ixex_zones(); void ensure_ixex_zones(); + std::string build_ixex_cache_key() const; void render_ixex_zones(bool force_default_color); void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); @@ -220,7 +222,8 @@ private: public: static constexpr unsigned int PLATE_NAME_HOVER_ID = 6; static constexpr unsigned int PLATE_FILAMENT_MAP_ID = 8; - static constexpr unsigned int GRABBER_COUNT = 9; + static constexpr unsigned int PLATE_IXEX_MODE_ID = 9; + static constexpr unsigned int GRABBER_COUNT = 10; static ColorRGBA SELECT_COLOR; static ColorRGBA UNSELECT_COLOR; @@ -280,6 +283,10 @@ public: bool get_spiral_vase_mode() const; void set_spiral_vase_mode(bool spiral_mode, bool as_global); + std::string get_ixex_mode() const; + void set_ixex_mode(const std::string& mode); + void reset_ixex_mode(); + std::vector get_plate_wrapping_detection_area() const; //static const int plate_x_offset = 20; //mm @@ -627,6 +634,8 @@ class PartPlateList : public ObjectBase GLTexture m_plate_set_filament_map_hovered_texture; GLTexture m_plate_name_edit_texture; GLTexture m_plate_name_edit_hovered_texture; + GLTexture m_ixex_mode_texture; + GLTexture m_ixex_mode_hovered_texture; GLTexture m_idx_textures[MAX_PLATE_COUNT]; // set render option bool render_bedtype_logo = true; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6c19a0ae43..f1b985120a 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17640,7 +17640,61 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi update(); p->partplate_list.select_plate(0); } + else if (action == (int)PartPlate::PLATE_IXEX_MODE_ID) + { + ret = select_plate(plate_index); + if (!ret) { + PartPlate* curr_plate = p->partplate_list.get_curr_plate(); + // Build ordered mode list: "primary" first, then all named modes. + std::vector modes; + modes.push_back("primary"); + const DynamicPrintConfig& printer_cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + auto* names_opt = printer_cfg.option("ixex_mode_names"); + if (names_opt) { + for (const auto& n : names_opt->values) + if (!n.empty()) modes.push_back(n); + } + if (right_click) { + // Show a popup menu with all modes. + wxMenu menu; + std::string current = curr_plate->get_ixex_mode(); + for (size_t i = 0; i < modes.size(); ++i) { + wxMenuItem* item = menu.AppendRadioItem(wxID_HIGHEST + (int)i, from_u8(modes[i])); + if (modes[i] == current) + item->Check(true); + } + menu.Bind(wxEVT_MENU, [this, curr_plate, &modes](wxCommandEvent& e) { + int idx = e.GetId() - wxID_HIGHEST; + if (idx >= 0 && idx < (int)modes.size()) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(modes[idx]); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + }); + p->view3D->get_canvas3d()->get_wxglcanvas()->PopupMenu(&menu); + ret = 1; // signal to caller: popup was shown, suppress plate context menu + } else { + // Left-click: cycle to next mode. + std::string current = curr_plate->get_ixex_mode(); + auto it = std::find(modes.begin(), modes.end(), current); + size_t next_idx = (it == modes.end()) ? 0 : ((it - modes.begin() + 1) % modes.size()); + std::string next_mode = modes[next_idx]; + if (next_mode != current) { + take_snapshot("set ixex mode"); + curr_plate->set_ixex_mode(next_mode); + update_project_dirty_from_presets(); + set_plater_dirty(true); + update(); + } + } + } else { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "can not select plate %1%" << plate_index; + ret = -1; + } + } else { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "invalid action %1%, with right_click=%2%" << action << right_click; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 6a3b0a7444..7e02c63114 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2675,26 +2675,6 @@ void TabPrint::build() optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse"); optgroup->append_single_option_line("enable_wrapping_detection"); - // Dynamic combo box — choices come from the printer preset's ixex_mode_names. - // Use create_line_with_widget so the undo bitmaps are set (has_undo_ui()=true), - // ensuring the widget x-position matches other option fields in OG_CustomCtrl. - create_line_with_widget(optgroup.get(), "ixex_parallel_mode", "", [this](wxWindow* parent) -> wxSizer* { - m_ixex_mode_combo = new ComboBox(parent, wxID_ANY, wxEmptyString, - wxDefaultPosition, - wxSize(12 * wxGetApp().em_unit(), -1), - 0, nullptr, wxCB_READONLY); - m_ixex_mode_combo->GetDropDown().SetUseContentWidth(true); - refresh_ixex_mode_combo(); - m_ixex_mode_combo->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { - if (!m_ixex_mode_combo || !m_config) return; - std::string val = m_ixex_mode_combo->GetStringSelection().ToStdString(); - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString(val)); - on_value_change("ixex_parallel_mode", val); - }); - auto* s = new wxBoxSizer(wxHORIZONTAL); - s->Add(m_ixex_mode_combo, 0, wxALIGN_CENTER_VERTICAL); - return s; - }); optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin"); optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin"); @@ -2753,36 +2733,6 @@ void TabPrint::reload_config() { this->compatible_widget_reload(m_compatible_printers); Tab::reload_config(); - refresh_ixex_mode_combo(); -} - -void TabPrint::refresh_ixex_mode_combo() -{ - if (!m_ixex_mode_combo) return; - // Rebuild choices: "primary" + names from the active printer preset - m_ixex_mode_combo->Clear(); - m_ixex_mode_combo->Append("primary"); - if (m_preset_bundle) { - auto* names = m_preset_bundle->printers.get_edited_preset().config - .option("ixex_mode_names"); - if (names) { - for (const auto& n : names->values) - if (!n.empty()) m_ixex_mode_combo->Append(wxString::FromUTF8(n)); - } - } - // Restore current selection from process config (safe: option may not exist in older presets) - std::string cur = "primary"; - if (m_config) { - auto* opt = m_config->option("ixex_parallel_mode"); - if (opt) cur = opt->value; - } - if (!m_ixex_mode_combo->SetStringSelection(cur)) { - m_ixex_mode_combo->SetSelection(0); // fall back to "primary" - // The active mode no longer exists — reset config so stale name doesn't crash downstream. - if (m_config) - m_config->set_key_value("ixex_parallel_mode", new ConfigOptionString("primary")); - on_value_change("ixex_parallel_mode", std::string("primary")); - } } void TabPrint::update_description_lines() @@ -2904,7 +2854,6 @@ void TabPrint::clear_pages() m_recommended_thin_wall_thickness_description_line = nullptr; m_top_bottom_shell_thickness_explanation = nullptr; - m_ixex_mode_combo = nullptr; } //BBS: GUI refactor @@ -5060,9 +5009,6 @@ void TabPrinter::build_fff() m_config->set_key_value("ixex_mode_gcodes", new ConfigOptionStrings(gcodes)); update_dirty(); on_value_change("ixex_mode_names", std::string("")); - // Refresh the process tab's mode dropdown immediately - if (auto* print_tab = dynamic_cast(wxGetApp().get_tab(Preset::TYPE_PRINT))) - print_tab->refresh_ixex_mode_combo(); }; auto* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(m_ixex_modes_ctrl, 1, wxEXPAND); diff --git a/src/slic3r/GUI/Tab.hpp b/src/slic3r/GUI/Tab.hpp index 687cfeea73..7d86278db7 100644 --- a/src/slic3r/GUI/Tab.hpp +++ b/src/slic3r/GUI/Tab.hpp @@ -464,12 +464,10 @@ public: void update() override; void clear_pages() override; bool supports_printer_technology(const PrinterTechnology tech) const override { return tech == ptFFF; } - void refresh_ixex_mode_combo(); private: ogStaticText* m_recommended_thin_wall_thickness_description_line = nullptr; ogStaticText* m_top_bottom_shell_thickness_explanation = nullptr; - ComboBox* m_ixex_mode_combo {nullptr}; }; class TabPrintModel : public TabPrint