Allow 'Change Type' to be used with Multiple Parts Selected (#11544)

* Initial changes allowing you to change the type of multiple parts at once by selecting them all.

* Removed second occurance of Change type in right click menu

* Ready to go feature change

* Remove accidental file creation

* Removing excessive std::cerr

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
Matthew
2025-12-14 01:36:47 +11:00
committed by GitHub
parent 54c876222e
commit 7ec3d85a02
2 changed files with 184 additions and 24 deletions

View File

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

View File

@@ -24,6 +24,10 @@
#include "SingleChoiceDialog.hpp"
#include "StepMeshDialog.hpp"
#include <vector>
#include <unordered_map>
#include <functional>
#include <boost/algorithm/string.hpp>
#include <wx/progdlg.h>
#include <wx/listbook.h>
@@ -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<Target> 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<int, int> 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<int, int> 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<int, std::vector<ModelVolume*>> 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<const ModelVolume*> 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)