diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 7686356bfb..a5e09271c6 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -751,8 +751,14 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu) return append_menu_item(menu, wxID_ANY, _L("Change type"), "", [](wxCommandEvent&) { obj_list()->change_part_type(); }, "", menu, []() { - wxDataViewItem item = obj_list()->GetSelection(); - return item.IsOk() || obj_list()->GetModel()->GetItemType(item) == itVolume; + wxDataViewItemArray selections; + obj_list()->GetSelections(selections); + if (selections.empty()) return false; + for (const auto& it : selections) { + if (!(obj_list()->GetModel()->GetItemType(it) & itVolume)) + return false; // non-volume present -> disable + } + return true; }, m_parent); } @@ -1798,6 +1804,7 @@ wxMenu* MenuFactory::multi_selection_menu() } append_menu_item_per_object_process(menu); menu->AppendSeparator(); + append_menu_item_change_type(menu); append_menu_item_change_filament(menu); } return menu; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 5b5e0bd77a..1cea62ece8 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -24,6 +24,10 @@ #include "SingleChoiceDialog.hpp" #include "StepMeshDialog.hpp" + +#include +#include +#include #include #include #include @@ -5337,26 +5341,32 @@ ModelVolume* ObjectList::get_selected_model_volume() void ObjectList::change_part_type() { - ModelVolume* volume = get_selected_model_volume(); - if (!volume) - return; + wxDataViewItemArray selections; + GetSelections(selections); - const int obj_idx = get_selected_obj_idx(); - if (obj_idx < 0) return; + if (selections.size() <= 1) { + int obj_idx = get_selected_obj_idx(); + if (obj_idx < 0) { + return; + } + + ModelVolume* volume = get_selected_model_volume(); + if (!volume) { + return; + } const ModelVolumeType type = volume->type(); - if (type == ModelVolumeType::MODEL_PART) - { - int model_part_cnt = 0; - for (auto vol : (*m_objects)[obj_idx]->volumes) { - if (vol->type() == ModelVolumeType::MODEL_PART) - ++model_part_cnt; - } + if (type == ModelVolumeType::MODEL_PART) { + int model_part_cnt = 0; + for (auto vol : (*m_objects)[obj_idx]->volumes) { + if (vol->type() == ModelVolumeType::MODEL_PART) + ++model_part_cnt; + } - if (model_part_cnt == 1) { - Slic3r::GUI::show_error(nullptr, _(L("The type of the last solid object part is not to be changed."))); - return; - } + if (model_part_cnt == 1) { + Slic3r::GUI::show_error(nullptr, _(L("The type of the last solid object part is not to be changed."))); + return; + } } // ORCA: Fix crash when changing type of svg / text modifier @@ -5365,22 +5375,165 @@ void ObjectList::change_part_type() names.Add(_L("Negative Part")); names.Add(_L("Modifier")); if (!volume->is_svg() && !volume->is_text()) { - names.Add(_L("Support Blocker")); - names.Add(_L("Support Enforcer")); + names.Add(_L("Support Blocker")); + names.Add(_L("Support Enforcer")); } SingleChoiceDialog dlg(_L("Type:"), _L("Choose part type"), names, int(type)); auto new_type = ModelVolumeType(dlg.GetSingleChoiceIndex()); - if (new_type == type || new_type == ModelVolumeType::INVALID) - return; + if (new_type == type || new_type == ModelVolumeType::INVALID) { + return; + } take_snapshot("Change part type"); volume->set_type(new_type); wxDataViewItemArray sel = reorder_volumes_and_get_selection(obj_idx, [volume](const ModelVolume* vol) { return vol == volume; }); - if (!sel.IsEmpty()) - select_item(sel.front()); + if (!sel.IsEmpty()) { + select_item(sel.front()); + } + + return; + } + + // --- Multi Selection --- + struct Target { int obj_idx; Slic3r::ModelVolume* vol; }; + std::vector targets; + targets.reserve(selections.size()); + bool any_text_or_svg = false; + + for (const auto& item : selections) { + auto typeMask = m_objects_model->GetItemType(item); + if (!(typeMask & itVolume)) { + continue; + } + + int obj_idx = -1, vol_idx = -1; + get_selected_item_indexes(obj_idx, vol_idx, item); + if (obj_idx < 0 || vol_idx < 0) { + continue; + } + + ModelVolume* vol = (*m_objects)[obj_idx]->volumes[vol_idx]; + if (!vol) { + continue; + } + + targets.push_back({ obj_idx, vol }); + if (vol->is_svg() || vol->is_text()) + any_text_or_svg = true; + } + + if (targets.empty()) { + return; + } + + wxArrayString names; + names.Add(_L("Part")); + names.Add(_L("Negative Part")); + names.Add(_L("Modifier")); + if (!any_text_or_svg) { + names.Add(_L("Support Blocker")); + names.Add(_L("Support Enforcer")); + } + + // Preselect current type of the first selected volume + ModelVolumeType initial_type = targets.front().vol->type(); + SingleChoiceDialog dlg(_L("Type:"), _L("Choose part type"), names, int(initial_type)); + auto new_type = ModelVolumeType(dlg.GetSingleChoiceIndex()); + if (new_type == ModelVolumeType::INVALID) { + return; + } + + if (new_type != ModelVolumeType::MODEL_PART) { + // Count initial MODEL_PARTs per object + std::unordered_map parts_initial; + for (const auto& t : targets) { + int cnt = 0; + for (auto v : (*m_objects)[t.obj_idx]->volumes) + if (v->type() == ModelVolumeType::MODEL_PART) ++cnt; + parts_initial[t.obj_idx] = cnt; + } + + // Count how many selected MODEL_PARTs would be converted away, per object + std::unordered_map parts_to_remove; + for (const auto& t : targets) { + if (t.vol->type() == ModelVolumeType::MODEL_PART) { + ++parts_to_remove[t.obj_idx]; + } + } + + // If for any object: initial_parts > 0 and removals == initial_parts => would remove all + bool would_remove_all_for_any = false; + for (const auto& kv : parts_to_remove) { + const int obj_idx = kv.first; + const int removing = kv.second; + const int initial = parts_initial[obj_idx]; + if (initial > 0 && removing == initial) { + would_remove_all_for_any = true; + break; + } + } + + if (would_remove_all_for_any) { + Slic3r::GUI::show_error(nullptr, _(L("The type of the last solid object part is not to be changed."))); + return; + } + } + + take_snapshot("Change part type (multi)"); + + // Apply changes + size_t applied = 0, skipped_same = 0; + std::unordered_map> changed_per_object; + for (const auto& t : targets) { + const auto current = t.vol->type(); + if (current == new_type) { + ++skipped_same; + continue; + } + t.vol->set_type(new_type); + changed_per_object[t.obj_idx].push_back(t.vol); + ++applied; + } + + if (applied == 0) { + // Nothing changed; keep the original selection as-is + select_items(selections); + return; + } + + // Reorder per object and rebuild selection to follow changed volumes + wxDataViewItemArray new_selection; + for (const auto& kv : changed_per_object) { + const int obj_idx = kv.first; + const auto& changed_vols = kv.second; + + std::unordered_set changed_set; + changed_set.reserve(changed_vols.size()); + for (const auto* v : changed_vols) { + changed_set.insert(v); + } + + wxDataViewItemArray sel = reorder_volumes_and_get_selection( + obj_idx, + [&changed_set](const ModelVolume* v) -> bool { + return changed_set.find(v) != changed_set.end(); + } + ); + + // Append to new_selection + for (const auto& it : sel) new_selection.Add(it); + } + + if (!new_selection.IsEmpty()) { + select_items(new_selection); + } else { + select_items(selections); + } + + return; } void ObjectList::last_volume_is_deleted(const int obj_idx)