Replace all with stl (#11145)
Some checks are pending
Build all / Build All (push) Waiting to run
Build all / Flatpak (push) Waiting to run

* 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 <hasler@thecaptury.com>
This commit is contained in:
nilshasler 2025-10-29 09:30:23 +01:00 committed by GitHub
parent e3f049829b
commit dedfd9d4ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 219 additions and 4 deletions

View file

@ -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 ""

View file

@ -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"

View file

@ -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) {

View file

@ -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);

View file

@ -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<int> 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<ObjectDataViewModelNode *>(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<unsigned int> 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<std::pair<int, int>> 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

View file

@ -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