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: diff --git a/src/pstack/gui/controls.cpp b/src/pstack/gui/controls.cpp index 1f74128..fe430f4 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); - minimize_checkbox = new wxCheckBox(panel, wxID_ANY, ""); + min_hole_spinner->Disable(); + minimize_checkbox = new wxCheckBox(panel, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxCHK_3STATE); + 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 fcb01b7..8be02f0 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(); @@ -45,33 +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_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); -} + 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_part.reset(); - _current_part_index.reset(); + 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) { @@ -113,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: { @@ -207,7 +242,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); @@ -328,7 +363,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(); @@ -354,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_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_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()); + 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(); }); @@ -373,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_part->quantity = event.GetPosition(); - _parts_list.reload_quantity(_current_part_index.value()); + 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_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_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_part->rotation_index = _controls.rotation_dropdown->GetSelection(); + for (auto& current_part : _current_parts) { + current_part.part->rotation_index = _controls.rotation_dropdown->GetSelection(); + } event.Skip(); }); @@ -411,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(); @@ -450,7 +500,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/main_window.hpp b/src/pstack/gui/main_window.hpp index 3bcd9ca..d6b7462 100644 --- a/src/pstack/gui/main_window.hpp +++ b/src/pstack/gui/main_window.hpp @@ -30,11 +30,12 @@ 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{}; - 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); 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 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;