From dedfd9d4ed473772eaaf8c76d16e2367673683bf Mon Sep 17 00:00:00 2001 From: nilshasler <99462038+nilshasler@users.noreply.github.com> Date: Wed, 29 Oct 2025 09:30:23 +0100 Subject: [PATCH] Replace all with stl (#11145) * Add "Replace all from STL" command this operates on the currently selected plate or on the selection * Add more translations for "Replace all from STL" * build fix * "Replace all with STL" also works with multiple selected plates * fix build * replace all from stl: better error handling and a nicer status message box --------- Co-authored-by: Nils Hasler --- localization/i18n/OrcaSlicer.pot | 24 +++++ localization/i18n/de/OrcaSlicer_de.po | 24 +++++ src/slic3r/GUI/GUI_Factories.cpp | 30 +++++- src/slic3r/GUI/GUI_Factories.hpp | 1 + src/slic3r/GUI/Plater.cpp | 142 +++++++++++++++++++++++++- src/slic3r/GUI/Plater.hpp | 2 + 6 files changed, 219 insertions(+), 4 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index ddd557fb9b..0f2ceee497 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -1949,9 +1949,15 @@ msgstr "" msgid "Replace with STL" msgstr "" +msgid "Replace all with STL" +msgstr "" + msgid "Replace the selected part with new STL" msgstr "" +msgid "Replace all selected parts with STL from folder" +msgstr "" + msgid "Change filament" msgstr "" @@ -2271,6 +2277,18 @@ msgid "" "cut information first." msgstr "" +msgid "✖ Skipped %1%: %2%, same file\n" +msgstr "" + +msgid "✖ Skipped %1%: %2% does not exist.\n" +msgstr "" + +msgid "✖ Skipped %1%: failed to replace.\n" +msgstr "" + +msgid "✔ Replaced %1% with %2%\n" +msgstr "" + msgid "Delete all connectors" msgstr "" @@ -6697,6 +6715,12 @@ msgstr "" msgid "File for the replace wasn't selected" msgstr "" +msgid "Select folder to replace from" +msgstr "" + +msgid "Directory for the replace wasn't selected" +msgstr "" + msgid "Please select a file" msgstr "" diff --git a/localization/i18n/de/OrcaSlicer_de.po b/localization/i18n/de/OrcaSlicer_de.po index 633da27f2d..8eaf852d8e 100644 --- a/localization/i18n/de/OrcaSlicer_de.po +++ b/localization/i18n/de/OrcaSlicer_de.po @@ -2065,6 +2065,12 @@ msgstr "Durch STL Datei austauschen" msgid "Replace the selected part with new STL" msgstr "Ausgewähltes Teil durch eine neue STL ersetzen." +msgid "Replace all with STL" +msgstr "Alle durch STL Dateien austauschen" + +msgid "Replace all selected parts with STL from folder" +msgstr "Ausgewählte Teile durch neue STL aus Ordner ersetzen." + msgid "Change filament" msgstr "Filament wechseln" @@ -2432,6 +2438,18 @@ msgstr "" "Um mit massiven Teilen oder negativen Volumen zu arbeiten, \n" "müssen Sie zuerst die Schnittinformationen ungültig machen." +msgid "✖ Skipped %1%: %2%, same file\n" +msgstr "✖ %1% übersprungen: %2%, gleiche Datei\n" + +msgid "✖ Skipped %1%: %2% does not exist.\n" +msgstr "✖ %1% übersprungen: %2% existiert nicht.\n" + +msgid "✔ Replaced %1% with %2%\n" +msgstr "✔ %1% durch %2% ersetzt\n" + +msgid "✖ Skipped %1%: failed to replace.\n" +msgstr "✖ %1% übersprungen: Ersetzen fehlgeschlagen.\n" + msgid "Delete all connectors" msgstr "Lösche alle Verbinder" @@ -7400,6 +7418,12 @@ msgstr "Wählen Sie eine neue Datei aus" msgid "File for the replace wasn't selected" msgstr "Datei für das Ersetzen wurde nicht ausgewählt" +msgid "Select folder to replace from" +msgstr "Wählen Sie ein Verzeichnis aus um daraus zu ersetzen" + +msgid "Directory for the replace wasn't selected" +msgstr "Verzeichnis um daraus zu ersetzen wurde nicht ausgewählt" + msgid "Please select a file" msgstr "Bitte wählen Sie eine Datei" diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 6a7ed26c34..a8133315d3 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -869,6 +869,13 @@ void MenuFactory::append_menu_item_replace_with_stl(wxMenu *menu) []() { return plater()->can_replace_with_stl(); }, m_parent); } +void MenuFactory::append_menu_item_replace_all_with_stl(wxMenu *menu) +{ + append_menu_item(menu, wxID_ANY, _L("Replace all with STL"), _L("Replace all selected parts with STL from folder"), + [](wxCommandEvent &) { plater()->replace_all_with_stl(); }, "", menu, + []() { return plater()->can_replace_all_with_stl(); }, m_parent); +} + void MenuFactory::append_menu_item_change_extruder(wxMenu* menu) { // BBS @@ -1350,6 +1357,7 @@ void MenuFactory::create_extra_object_menu() m_object_menu.AppendSeparator(); append_menu_item_reload_from_disk(&m_object_menu); append_menu_item_replace_with_stl(&m_object_menu); + append_menu_item_replace_all_with_stl(&m_object_menu); append_menu_item_export_stl(&m_object_menu); } @@ -1465,6 +1473,7 @@ void MenuFactory::create_bbl_part_menu() append_menu_item_change_type(menu); append_menu_item_reload_from_disk(menu); append_menu_item_replace_with_stl(menu); + append_menu_item_replace_all_with_stl(menu); } void MenuFactory::create_bbl_assemble_part_menu() @@ -1615,6 +1624,7 @@ void MenuFactory::create_plate_menu() [](wxCommandEvent&) { plater()->add_file(); }, "", menu, []() {return wxGetApp().plater()->can_add_model(); }, m_parent); #endif + append_menu_item_replace_all_with_stl(menu); return; @@ -1721,14 +1731,26 @@ wxMenu* MenuFactory::multi_selection_menu() wxDataViewItemArray sels; obj_list()->GetSelections(sels); bool multi_volume = true; + bool undefined_type = false; + bool all_plates = true; for (const wxDataViewItem& item : sels) { - multi_volume = list_model()->GetItemType(item) & itVolume; - if (!(list_model()->GetItemType(item) & (itVolume | itObject | itInstance))) + Slic3r::GUI::ItemType item_type = list_model()->GetItemType(item); + if ((item_type & itPlate) == 0) + all_plates = false; + multi_volume = item_type & itVolume; + if (!(item_type & (itVolume | itObject | itInstance))) // show this menu only for Objects(Instances mixed with Objects)/Volumes selection - return nullptr; + undefined_type = true; } + if (all_plates) { + wxMenu* menu = new MenuWithSeparators(); + append_menu_item_replace_all_with_stl(menu); + return menu; + } + if (undefined_type) + return nullptr; wxMenu* menu = new MenuWithSeparators(); if (!multi_volume) { int index = 0; @@ -1747,6 +1769,7 @@ wxMenu* MenuFactory::multi_selection_menu() append_menu_item_per_object_process(menu); menu->AppendSeparator(); append_menu_items_convert_unit(menu); + append_menu_item_replace_all_with_stl(menu); //BBS append_menu_item_change_filament(menu); menu->AppendSeparator(); @@ -1759,6 +1782,7 @@ wxMenu* MenuFactory::multi_selection_menu() //append_menu_item_simplify(menu); append_menu_item_delete(menu); append_menu_items_convert_unit(menu); + append_menu_item_replace_all_with_stl(menu); append_menu_item_change_filament(menu); wxMenu* split_menu = new wxMenu(); if (split_menu) { diff --git a/src/slic3r/GUI/GUI_Factories.hpp b/src/slic3r/GUI/GUI_Factories.hpp index 17afce4fce..00924f502d 100644 --- a/src/slic3r/GUI/GUI_Factories.hpp +++ b/src/slic3r/GUI/GUI_Factories.hpp @@ -143,6 +143,7 @@ private: void append_menu_item_export_stl(wxMenu* menu, bool is_mulity_menu = false); void append_menu_item_reload_from_disk(wxMenu* menu); void append_menu_item_replace_with_stl(wxMenu* menu); + void append_menu_item_replace_all_with_stl(wxMenu* menu); void append_menu_item_change_extruder(wxMenu* menu); void append_menu_item_set_visible(wxMenu* menu); void append_menu_item_delete(wxMenu* menu); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5cc1c21c62..83fd25e2c7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -4171,6 +4171,7 @@ struct Plater::priv void reload_from_disk(); bool replace_volume_with_stl(int object_idx, int volume_idx, const fs::path& new_path, const std::string& snapshot = ""); void replace_with_stl(); + void replace_all_with_stl(); void reload_all_from_disk(); //BBS: add no_slice option @@ -4284,6 +4285,7 @@ struct Plater::priv bool can_fillcolor() const; bool has_assemble_view() const; bool can_replace_with_stl() const; + bool can_replace_all_with_stl() const; bool can_split(bool to_objects) const; #if ENABLE_ENHANCED_PRINT_VOLUME_FIT bool can_scale_to_print_volume() const; @@ -7634,6 +7636,132 @@ void Plater::priv::replace_with_stl() } } +void Plater::priv::replace_all_with_stl() +{ + if (! q->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::EType::Undefined)) + return; + + const Selection& selection = get_selection(); + + if (selection.is_wipe_tower()) + return; + + fs::path input_path; + Selection::IndicesList volume_idxs = selection.get_volume_idxs(); + + // when plates are selected instead of volumes + // then selection is inaccurate, we need to + // find volumes contained in selected plates + + if (selection.is_empty() || volume_idxs.empty()) { + std::vector selected_plate_idxs; + + wxDataViewItemArray sels; + wxGetApp().obj_list()->GetSelections(sels); + for (const wxDataViewItem& item : sels) { + Slic3r::GUI::ItemType item_type = wxGetApp().obj_list()->GetModel()->GetItemType(item); + if (item_type & itPlate) { + if (item.IsOk()) { + ObjectDataViewModelNode *node = static_cast(item.GetID()); + selected_plate_idxs.push_back(node->GetPlateIdx()); + } + } + } + PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list(); + for (int obj_idx = 0; obj_idx < selection.get_model()->objects.size(); obj_idx++) { + for (int plate_idx : selected_plate_idxs) { + PartPlate* plate = plate_list.get_plate(plate_idx); + if (plate && plate->contain_instance_totally(obj_idx, 0)) { + std::vector indices = selection.get_volume_idxs_from_object(obj_idx); + volume_idxs.insert(indices.begin(), indices.end()); + } + } + } + } + + // find path for initializing the file selection dialog + + for (unsigned int idx : volume_idxs) { + const GLVolume* v = selection.get_volume(idx); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); + + const ModelObject* object = model.objects[object_idx]; + const ModelVolume* volume = object->volumes[volume_idx]; + + if (!volume->source.input_file.empty() && fs::exists(volume->source.input_file)) { + input_path = volume->source.input_file; + break; + } + } + + wxString title = _L("Select folder to replace from"); + title += ":"; + wxDirDialog dialog(q, title, from_u8(input_path.parent_path().string()), wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST); + if (dialog.ShowModal() != wxID_OK) + return; + + fs::path out_path = dialog.GetPath().ToUTF8().data(); + if (out_path.empty()) { + MessageDialog dlg(q, _L("Directory for the replace wasn't selected"), _L("Error during replace"), wxOK | wxOK_DEFAULT | wxICON_WARNING); + dlg.ShowModal(); + return; + } + + std::string status = _L("Replaced with STLs from directory:\n").ToStdString() + out_path.string() + "\n\n"; + + for (unsigned int idx : volume_idxs) { + const GLVolume* v = selection.get_volume(idx); + int object_idx = v->object_idx(); + int volume_idx = v->volume_idx(); + + const ModelObject* object = model.objects[object_idx]; + const ModelVolume* volume = object->volumes[volume_idx]; + + if (volume->source.input_file.empty()) + continue; + + input_path = volume->source.input_file; + + fs::path new_path = out_path / input_path.filename(); + + std::string volume_name = volume->name; + + if (new_path == input_path) { + status += boost::str(boost::format(_L("✖ Skipped %1%: same file.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " skipping replace volume : same filename " << new_path; + continue; + } + + if (!fs::exists(new_path)) { + status += boost::str(boost::format(_L("✖ Skipped %1%: file does not exist.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : filen does not exist " << new_path; + continue; + } + + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " replacing volume : " << input_path << " with " << new_path; + + if (!replace_volume_with_stl(object_idx, volume_idx, new_path, "Replace with STL")) { + status += boost::str(boost::format(_L("✖ Skipped %1%: failed to replace.\n").ToStdString()) % volume_name); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : failed to replace with " << new_path; + continue; + } + + status += boost::str(boost::format(_L("✔ Replaced %1%.\n").ToStdString()) % volume_name); + } + + // update 3D scene + update(); + + // new GLVolumes have been created at this point, so update their printable state + for (size_t i = 0; i < model.objects.size(); ++i) { + view3D->get_canvas3d()->update_instance_printable_state_for_object(i); + } + + MessageDialog dlg(q, status, _L("Replaced volumes"), wxOK | wxOK_DEFAULT | wxICON_INFORMATION); + dlg.ShowModal(); +} + #if ENABLE_RELOAD_FROM_DISK_REWORK static std::vector> reloadable_volumes(const Model &model, const Selection &selection) { @@ -10198,6 +10326,12 @@ bool Plater::priv::can_replace_with_stl() const && get_selection().get_volume_idxs().size() == 1; } +bool Plater::priv::can_replace_all_with_stl() const +{ + return !sidebar->obj_list()->has_selected_cut_object() + && get_selection().get_volume_idxs().size() != 1; +} + bool Plater::priv::can_reload_from_disk() const { if (sidebar->obj_list()->has_selected_cut_object()) @@ -14036,7 +14170,7 @@ bool Plater::check_printer_initialized(MachineObject *obj, bool only_warning, bo break; } } - if (extruder.GetNozzleFlowType() == NozzleType::ntUndefine) { + if (extruder.GetNozzleFlowType() == NozzleFlowType::NONE_FLOWTYPE) { has_been_initialized = false; break; } @@ -14721,6 +14855,11 @@ void Plater::replace_with_stl() p->replace_with_stl(); } +void Plater::replace_all_with_stl() +{ + p->replace_all_with_stl(); +} + void Plater::reload_all_from_disk() { p->reload_all_from_disk(); @@ -17317,6 +17456,7 @@ bool Plater::can_reload_from_disk() const { return p->can_reload_from_disk(); } bool Plater::can_fillcolor() const { return p->can_fillcolor(); } bool Plater::has_assmeble_view() const { return p->has_assemble_view(); } bool Plater::can_replace_with_stl() const { return p->can_replace_with_stl(); } +bool Plater::can_replace_all_with_stl() const { return p->can_replace_all_with_stl(); } bool Plater::can_mirror() const { return p->can_mirror(); } bool Plater::can_split(bool to_objects) const { return p->can_split(to_objects); } #if ENABLE_ENHANCED_PRINT_VOLUME_FIT diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index 412ad275c3..9a3e6c5c94 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -491,6 +491,7 @@ public: void reload_from_disk(); void replace_with_stl(); + void replace_all_with_stl(); void reload_all_from_disk(); bool has_toolpaths_to_export() const; void export_toolpaths_to_obj() const; @@ -667,6 +668,7 @@ public: bool can_redo() const; bool can_reload_from_disk() const; bool can_replace_with_stl() const; + bool can_replace_all_with_stl() const; bool can_mirror() const; bool can_split(bool to_objects) const; #if ENABLE_ENHANCED_PRINT_VOLUME_FIT