From 113660b4456fef5193811052d848a7493207d74d Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Fri, 25 Jul 2025 15:33:12 -0500 Subject: [PATCH 1/6] Add arrows to the part quantity, to indicate whether it is greater than or less than the base quantity --- src/pstack/gui/main_window.cpp | 2 +- src/pstack/gui/parts_list.cpp | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index fcb01b7..0c4accc 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -328,7 +328,7 @@ wxMenuBar* main_window::make_menu_bar() { auto preferences_menu = new wxMenu(); preferences_menu->AppendCheckItem((int)menu_item::pref_scroll, "Invert &scroll", "Change the viewport scroll direction"); - preferences_menu->AppendCheckItem((int)menu_item::pref_extra, "Display &extra parts", "Display the extra part quantity separately"); + preferences_menu->AppendCheckItem((int)menu_item::pref_extra, "Display &extra parts", "Display the quantity of extra parts separately"); menu_bar->Append(preferences_menu, "&Preferences"); auto help_menu = new wxMenu(); diff --git a/src/pstack/gui/parts_list.cpp b/src/pstack/gui/parts_list.cpp index b417b97..fe8847c 100644 --- a/src/pstack/gui/parts_list.cpp +++ b/src/pstack/gui/parts_list.cpp @@ -48,13 +48,21 @@ calc::part make_part(std::string mesh_file, bool mirrored) { } wxString quantity_string(const calc::part& part, const bool show_extra) { - if (show_extra and part.base_quantity.has_value()) { - const int diff = part.quantity - *part.base_quantity; - if (diff > 0) { - return wxString::Format("%d + %d", *part.base_quantity, diff); - } + if (not part.base_quantity.has_value()) { + return wxString::Format("%d", part.quantity); + } + + static const wxString up_arrow = wxString::FromUTF8(" \xe2\x86\x91"); // ↑ + static const wxString down_arrow = wxString::FromUTF8(" \xe2\x86\x93"); // ↓ + static const wxString empty = ""; + + const int diff = part.quantity - *part.base_quantity; + if (diff > 0 and show_extra) { + return wxString::Format("%d + %d%s", *part.base_quantity, diff, up_arrow); + } else { + auto& arrow_suffix = (diff > 0) ? up_arrow : (diff < 0) ? down_arrow : empty; + return wxString::Format("%d%s", part.quantity, arrow_suffix); } - return wxString::Format("%d", part.quantity); } } // namespace From 053c523cd6ed3e9a26cf0ab17acd14943c77cb86 Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Sat, 26 Jul 2025 00:17:03 -0500 Subject: [PATCH 2/6] Remove a call to enable_part_settings(false) --- src/pstack/gui/controls.cpp | 6 ++++++ src/pstack/gui/main_window.cpp | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pstack/gui/controls.cpp b/src/pstack/gui/controls.cpp index 1f74128..ac194f5 100644 --- a/src/pstack/gui/controls.cpp +++ b/src/pstack/gui/controls.cpp @@ -54,17 +54,23 @@ void controls::initialize(main_window* parent) { minimize_text = new wxStaticText(panel, wxID_ANY, "Minimize box:"); quantity_spinner = new wxSpinCtrl(panel); quantity_spinner->SetRange(0, 200); + quantity_spinner->Disable(); min_hole_spinner = new wxSpinCtrl(panel); min_hole_spinner->SetRange(0, 100); + min_hole_spinner->Disable(); minimize_checkbox = new wxCheckBox(panel, wxID_ANY, ""); + minimize_checkbox->Disable(); wxArrayString rotation_choices; rotation_choices.Add("None"); rotation_choices.Add("Cubic"); rotation_choices.Add("Arbitrary"); rotation_text = new wxStaticText(panel, wxID_ANY, "Rotations:"); rotation_dropdown = new wxChoice(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, rotation_choices); + rotation_dropdown->Disable(); preview_voxelization_button = new wxButton(panel, wxID_ANY, "Preview voxelization"); + preview_voxelization_button->Disable(); preview_bounding_box_button = new wxButton(panel, wxID_ANY, "Preview bounding box"); + preview_bounding_box_button->Disable(); } { diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index 0c4accc..b3bad3f 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -27,7 +27,6 @@ main_window::main_window(const wxString& title) _parts_list.initialize(_controls.notebook_panels[0]); _results_list.initialize(_controls.notebook_panels[2]); bind_all_controls(); - enable_part_settings(false); wxGLAttributes attrs; attrs.PlatformDefaults().Defaults().EndList(); From e678569e8ff514950f7762255d0f15bb7860005f Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Sat, 26 Jul 2025 00:20:27 -0500 Subject: [PATCH 3/6] In main_window, unify _current_part and _current_part_index into a vector _current_parts --- src/pstack/gui/main_window.cpp | 41 +++++++++++++++++----------------- src/pstack/gui/main_window.hpp | 7 ++++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index b3bad3f..c84f0b6 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -58,19 +58,18 @@ void main_window::on_select_parts(const std::vector& indices) { void main_window::set_part(const std::size_t index) { enable_part_settings(true); - _current_part = _parts_list.at(index); - _current_part_index.emplace(index); - _controls.quantity_spinner->SetValue(_current_part->quantity); - _controls.min_hole_spinner->SetValue(_current_part->min_hole); - _controls.minimize_checkbox->SetValue(_current_part->rotate_min_box); - _controls.rotation_dropdown->SetSelection(_current_part->rotation_index); - _viewport->set_mesh(_current_part->mesh, _current_part->centroid); + _current_parts.clear(); + _current_parts.emplace_back(_parts_list.at(index).get(), index); + _controls.quantity_spinner->SetValue(_current_parts[0].part->quantity); + _controls.min_hole_spinner->SetValue(_current_parts[0].part->min_hole); + _controls.minimize_checkbox->SetValue(_current_parts[0].part->rotate_min_box); + _controls.rotation_dropdown->SetSelection(_current_parts[0].part->rotation_index); + _viewport->set_mesh(_current_parts[0].part->mesh, _current_parts[0].part->centroid); } void main_window::unset_part() { enable_part_settings(false); - _current_part.reset(); - _current_part_index.reset(); + _current_parts.clear(); } void main_window::enable_part_settings(bool enable) { @@ -206,7 +205,7 @@ void main_window::on_stacking_success(calc::stack_result result, const std::chro void main_window::enable_on_stacking(const bool starting) { const bool enable = not starting; - enable_part_settings(enable and _current_part_index.has_value()); + enable_part_settings(enable and not _current_parts.empty()); _parts_list.control()->Enable(enable); for (wxMenuItem* item : _disableable_menu_items) { item->Enable(enable); @@ -353,16 +352,16 @@ void main_window::bind_all_controls() { _controls.delete_part_button->Bind(wxEVT_BUTTON, &main_window::on_delete_part, this); _controls.reload_part_button->Bind(wxEVT_BUTTON, &main_window::on_reload_part, this); _controls.copy_part_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - _parts_list.append(*_current_part); + _parts_list.append(*_current_parts[0].part); _parts_list.update_label(); event.Skip(); }); _controls.mirror_part_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - _current_part->mirrored = not _current_part->mirrored; - _current_part->mesh.mirror_x(); - _current_part->mesh.set_baseline({ 0, 0, 0 }); - _parts_list.reload_text(_current_part_index.value()); - set_part(_current_part_index.value()); + _current_parts[0].part->mirrored = not _current_parts[0].part->mirrored; + _current_parts[0].part->mesh.mirror_x(); + _current_parts[0].part->mesh.set_baseline({ 0, 0, 0 }); + _parts_list.reload_text(_current_parts[0].index); + set_part(_current_parts[0].index); event.Skip(); }); @@ -372,21 +371,21 @@ void main_window::bind_all_controls() { _controls.sinterbox_result_button->Bind(wxEVT_BUTTON, &main_window::on_sinterbox_result, this); _controls.quantity_spinner->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent& event) { - _current_part->quantity = event.GetPosition(); - _parts_list.reload_quantity(_current_part_index.value()); + _current_parts[0].part->quantity = event.GetPosition(); + _parts_list.reload_quantity(_current_parts[0].index); event.Skip(); }); _controls.min_hole_spinner->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent& event) { - _current_part->min_hole = event.GetPosition(); + _current_parts[0].part->min_hole = event.GetPosition(); event.Skip(); }); _controls.minimize_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { - _current_part->rotate_min_box = event.IsChecked(); + _current_parts[0].part->rotate_min_box = event.IsChecked(); event.Skip(); }); _controls.rotation_dropdown->Bind(wxEVT_CHOICE, [this](wxCommandEvent& event) { - _current_part->rotation_index = _controls.rotation_dropdown->GetSelection(); + _current_parts[0].part->rotation_index = _controls.rotation_dropdown->GetSelection(); event.Skip(); }); diff --git a/src/pstack/gui/main_window.hpp b/src/pstack/gui/main_window.hpp index 3bcd9ca..43d2062 100644 --- a/src/pstack/gui/main_window.hpp +++ b/src/pstack/gui/main_window.hpp @@ -33,8 +33,11 @@ class main_window : public wxFrame { void set_part(std::size_t index); void unset_part(); parts_list _parts_list{}; - std::shared_ptr _current_part = nullptr; - std::optional _current_part_index = std::nullopt; + struct _current_part_t { + calc::part* part; + std::size_t index; + }; + std::vector<_current_part_t> _current_parts{}; void enable_part_settings(bool enable); void on_select_results(const std::vector& indices); From bfe26916926d5b89275d386e3558c5e08e932aef Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Sat, 26 Jul 2025 00:34:28 -0500 Subject: [PATCH 4/6] Use a reference instead of shared_ptr for parts_list::at(), because ultimately these references are bound to the lifetime of the main_window anyway, and they don't escape --- src/pstack/gui/main_window.cpp | 4 ++-- src/pstack/gui/parts_list.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index c84f0b6..31a253f 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -59,7 +59,7 @@ void main_window::on_select_parts(const std::vector& indices) { void main_window::set_part(const std::size_t index) { enable_part_settings(true); _current_parts.clear(); - _current_parts.emplace_back(_parts_list.at(index).get(), index); + _current_parts.emplace_back(&_parts_list.at(index), index); _controls.quantity_spinner->SetValue(_current_parts[0].part->quantity); _controls.min_hole_spinner->SetValue(_current_parts[0].part->min_hole); _controls.minimize_checkbox->SetValue(_current_parts[0].part->rotate_min_box); @@ -448,7 +448,7 @@ void main_window::on_import_part(wxCommandEvent& event) { } _parts_list.update_label(); if (paths.size() == 1) { - const calc::part& part = *_parts_list.at(_parts_list.rows() - 1); + const calc::part& part = _parts_list.at(_parts_list.rows() - 1); _viewport->set_mesh(part.mesh, part.centroid); } diff --git a/src/pstack/gui/parts_list.hpp b/src/pstack/gui/parts_list.hpp index 5457dcf..2f1fb01 100644 --- a/src/pstack/gui/parts_list.hpp +++ b/src/pstack/gui/parts_list.hpp @@ -28,8 +28,8 @@ class parts_list : public list_view { void reload_quantity(std::size_t row); void delete_all(); void delete_selected(); - std::shared_ptr at(std::size_t row) { - return _parts.at(row); + calc::part& at(std::size_t row) { + return *_parts.at(row); } std::vector> get_all() const; From 36763ac07259ea4a03382cd0dfd2e7046528195a Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Sat, 26 Jul 2025 01:11:25 -0500 Subject: [PATCH 5/6] Allow batch editing of multiple selected parts --- src/pstack/gui/controls.cpp | 2 +- src/pstack/gui/main_window.cpp | 128 +++++++++++++++++++++++---------- src/pstack/gui/main_window.hpp | 2 - 3 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/pstack/gui/controls.cpp b/src/pstack/gui/controls.cpp index ac194f5..fe430f4 100644 --- a/src/pstack/gui/controls.cpp +++ b/src/pstack/gui/controls.cpp @@ -58,7 +58,7 @@ void controls::initialize(main_window* parent) { min_hole_spinner = new wxSpinCtrl(panel); min_hole_spinner->SetRange(0, 100); min_hole_spinner->Disable(); - minimize_checkbox = new wxCheckBox(panel, wxID_ANY, ""); + minimize_checkbox = new wxCheckBox(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); minimize_checkbox->Disable(); wxArrayString rotation_choices; rotation_choices.Add("None"); diff --git a/src/pstack/gui/main_window.cpp b/src/pstack/gui/main_window.cpp index 31a253f..8be02f0 100644 --- a/src/pstack/gui/main_window.cpp +++ b/src/pstack/gui/main_window.cpp @@ -44,32 +44,71 @@ main_window::main_window(const wxString& title) } void main_window::on_select_parts(const std::vector& indices) { - const auto size = indices.size(); - _controls.delete_part_button->Enable(size != 0); - _controls.reload_part_button->Enable(size != 0); - _controls.copy_part_button->Enable(size == 1); - _controls.mirror_part_button->Enable(size == 1); - if (size == 1) { - set_part(indices[0]); - } else { - unset_part(); + const bool any_selected = not indices.empty(); + _controls.delete_part_button->Enable(any_selected); + _controls.reload_part_button->Enable(any_selected); + _controls.copy_part_button->Enable(any_selected); + _controls.mirror_part_button->Enable(any_selected); + enable_part_settings(any_selected); + _current_parts.clear(); + if (not any_selected) { + return; } -} -void main_window::set_part(const std::size_t index) { - enable_part_settings(true); - _current_parts.clear(); - _current_parts.emplace_back(&_parts_list.at(index), index); - _controls.quantity_spinner->SetValue(_current_parts[0].part->quantity); - _controls.min_hole_spinner->SetValue(_current_parts[0].part->min_hole); - _controls.minimize_checkbox->SetValue(_current_parts[0].part->rotate_min_box); - _controls.rotation_dropdown->SetSelection(_current_parts[0].part->rotation_index); - _viewport->set_mesh(_current_parts[0].part->mesh, _current_parts[0].part->centroid); -} + std::optional quantity{}; + std::optional min_hole{}; + std::optional rotate_min_box{}; + std::optional rotation_index{}; + bool first_time = true; + for (const std::size_t index : indices) { + calc::part& part = _parts_list.at(index); + _current_parts.emplace_back(&part, index); + if (first_time) { + first_time = false; + quantity.emplace(part.quantity); + min_hole.emplace(part.min_hole); + rotate_min_box.emplace(part.rotate_min_box); + rotation_index.emplace(part.rotation_index); + } else { + if (quantity.has_value() and *quantity != part.quantity) { + quantity.reset(); + } + if (min_hole.has_value() and *min_hole != part.min_hole) { + min_hole.reset(); + } + if (rotate_min_box.has_value() and *rotate_min_box != part.rotate_min_box) { + rotate_min_box.reset(); + } + if (rotation_index.has_value() and *rotation_index != part.rotation_index) { + rotation_index.reset(); + } + } + } + if (quantity.has_value()) { + _controls.quantity_spinner->SetValue(*quantity); + } else { + _controls.quantity_spinner->SetValue(""); + } + if (min_hole.has_value()) { + _controls.min_hole_spinner->SetValue(*min_hole); + } else { + _controls.min_hole_spinner->SetValue(""); + } + if (rotate_min_box.has_value()) { + _controls.minimize_checkbox->SetValue(*rotate_min_box); + } else { + _controls.minimize_checkbox->Set3StateValue(wxCHK_UNDETERMINED); + } + if (rotation_index.has_value()) { + _controls.rotation_dropdown->SetSelection(*rotation_index); + } else { + _controls.rotation_dropdown->SetSelection(wxNOT_FOUND); + } -void main_window::unset_part() { - enable_part_settings(false); - _current_parts.clear(); + if (_current_parts.size() == 1) { + const calc::part& part = *_current_parts[0].part; + _viewport->set_mesh(part.mesh, part.centroid); + } } void main_window::enable_part_settings(bool enable) { @@ -111,9 +150,7 @@ void main_window::on_switch_tab(wxBookCtrlEvent& event) { switch (event.GetSelection()) { case 0: { _parts_list.get_selected(selected); - if (selected.size() == 1) { - set_part(selected[0]); - } + on_select_parts(selected); break; } case 2: { @@ -352,16 +389,23 @@ void main_window::bind_all_controls() { _controls.delete_part_button->Bind(wxEVT_BUTTON, &main_window::on_delete_part, this); _controls.reload_part_button->Bind(wxEVT_BUTTON, &main_window::on_reload_part, this); _controls.copy_part_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - _parts_list.append(*_current_parts[0].part); + for (auto& current_part : _current_parts) { + _parts_list.append(*current_part.part); + } _parts_list.update_label(); event.Skip(); }); _controls.mirror_part_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { - _current_parts[0].part->mirrored = not _current_parts[0].part->mirrored; - _current_parts[0].part->mesh.mirror_x(); - _current_parts[0].part->mesh.set_baseline({ 0, 0, 0 }); - _parts_list.reload_text(_current_parts[0].index); - set_part(_current_parts[0].index); + static thread_local std::vector indices{}; + indices.clear(); + for (auto& current_part : _current_parts) { + indices.push_back(current_part.index); + current_part.part->mirrored = not current_part.part->mirrored; + current_part.part->mesh.mirror_x(); + current_part.part->mesh.set_baseline({ 0, 0, 0 }); + _parts_list.reload_text(current_part.index); + } + on_select_parts(indices); event.Skip(); }); @@ -371,21 +415,29 @@ void main_window::bind_all_controls() { _controls.sinterbox_result_button->Bind(wxEVT_BUTTON, &main_window::on_sinterbox_result, this); _controls.quantity_spinner->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent& event) { - _current_parts[0].part->quantity = event.GetPosition(); - _parts_list.reload_quantity(_current_parts[0].index); + for (auto& current_part : _current_parts) { + current_part.part->quantity = event.GetPosition(); + _parts_list.reload_quantity(current_part.index); + } event.Skip(); }); _controls.min_hole_spinner->Bind(wxEVT_SPINCTRL, [this](wxSpinEvent& event) { - _current_parts[0].part->min_hole = event.GetPosition(); + for (auto& current_part : _current_parts) { + current_part.part->min_hole = event.GetPosition(); + } event.Skip(); }); _controls.minimize_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) { - _current_parts[0].part->rotate_min_box = event.IsChecked(); + for (auto& current_part : _current_parts) { + current_part.part->rotate_min_box = event.IsChecked(); + } event.Skip(); }); _controls.rotation_dropdown->Bind(wxEVT_CHOICE, [this](wxCommandEvent& event) { - _current_parts[0].part->rotation_index = _controls.rotation_dropdown->GetSelection(); + for (auto& current_part : _current_parts) { + current_part.part->rotation_index = _controls.rotation_dropdown->GetSelection(); + } event.Skip(); }); @@ -409,7 +461,7 @@ void main_window::on_new(wxCommandEvent& event) { { _controls.reset_values(); _parts_list.delete_all(); - unset_part(); + on_select_parts({}); _results_list.delete_all(); unset_result(); _viewport->remove_mesh(); diff --git a/src/pstack/gui/main_window.hpp b/src/pstack/gui/main_window.hpp index 43d2062..d6b7462 100644 --- a/src/pstack/gui/main_window.hpp +++ b/src/pstack/gui/main_window.hpp @@ -30,8 +30,6 @@ class main_window : public wxFrame { preferences _preferences; void on_select_parts(const std::vector& indices); - void set_part(std::size_t index); - void unset_part(); parts_list _parts_list{}; struct _current_part_t { calc::part* part; From 4826331c644bb76e74b5dfd9c3bb34429debee20 Mon Sep 17 00:00:00 2001 From: Braden Ganetsky Date: Sat, 26 Jul 2025 01:17:56 -0500 Subject: [PATCH 6/6] Run GHA test.yml for each pull request --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6a970f..8e3c0fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,7 @@ on: workflow_dispatch: workflow_call: + pull_request: jobs: test-windows: