From 76c167ce11b7610419ad2d14382e9d568a44beb8 Mon Sep 17 00:00:00 2001 From: Stepland <10530295-Buggyroom@users.noreply.gitlab.com> Date: Sat, 31 Dec 2022 23:54:59 +0100 Subject: [PATCH] Add note transform options --- src/better_note.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++ src/better_note.hpp | 23 +++++++++ src/chart_state.cpp | 84 +++++++++++++++++++++++++++++++++ src/chart_state.hpp | 9 +++- src/clipboard.cpp | 12 ++--- src/clipboard.hpp | 10 ++-- src/history.cpp | 96 ++++++++++++++++++++++++++++++++++++++ src/history.hpp | 108 ++++--------------------------------------- src/history_item.cpp | 61 ++++++++++++++++++++++++ src/history_item.hpp | 16 +++++++ src/main.cpp | 34 ++++++++++++++ src/meson.build | 1 + 12 files changed, 449 insertions(+), 112 deletions(-) create mode 100644 src/history.cpp diff --git a/src/better_note.cpp b/src/better_note.cpp index 582e519..e773a2c 100644 --- a/src/better_note.cpp +++ b/src/better_note.cpp @@ -41,6 +41,26 @@ namespace better { return out; }; + Position Position::mirror_horizontally() const { + return {3-x, y}; + } + + Position Position::mirror_vertically() const { + return {x, 3-y}; + } + + Position Position::rotate_90_clockwise() const { + return {3-y, x}; + } + + Position Position::rotate_90_counter_clockwise() const { + return {y, 3-x}; + } + + Position Position::rotate_180() const { + return {3-x, 3-y}; + } + TapNote::TapNote(Fraction time, Position position): time(time), position(position) {}; @@ -64,6 +84,27 @@ namespace better { }; }; + TapNote TapNote::mirror_horizontally() const { + return {time, position.mirror_horizontally()}; + } + + TapNote TapNote::mirror_vertically() const { + return {time, position.mirror_vertically()}; + } + + TapNote TapNote::rotate_90_clockwise() const { + return {time, position.rotate_90_clockwise()}; + } + + TapNote TapNote::rotate_90_counter_clockwise() const { + return {time, position.rotate_90_counter_clockwise()}; + } + + TapNote TapNote::rotate_180() const { + return {time, position.rotate_180()}; + } + + LongNote::LongNote(Fraction time, Position position, Fraction duration, Position tail_tip) : time(time), position(position), @@ -168,6 +209,52 @@ namespace better { } } + LongNote LongNote::mirror_horizontally() const { + return { + time, + position.mirror_horizontally(), + duration, + tail_tip.mirror_horizontally() + }; + } + + LongNote LongNote::mirror_vertically() const { + return { + time, + position.mirror_vertically(), + duration, + tail_tip.mirror_vertically() + }; + } + + LongNote LongNote::rotate_90_clockwise() const { + return { + time, + position.rotate_90_clockwise(), + duration, + tail_tip.rotate_90_clockwise() + }; + } + + LongNote LongNote::rotate_90_counter_clockwise() const { + return { + time, + position.rotate_90_counter_clockwise(), + duration, + tail_tip.rotate_90_counter_clockwise() + }; + } + + LongNote LongNote::rotate_180() const { + return { + time, + position.rotate_180(), + duration, + tail_tip.rotate_180() + }; + } + + /* * legacy long note tail index is given relative to the note position : * @@ -287,5 +374,25 @@ namespace better { return TapNote{time, position}; } } + + Note Note::mirror_horizontally() const { + return std::visit([](const auto& n) -> Note {return n.mirror_horizontally();}, this->note); + } + + Note Note::mirror_vertically() const { + return std::visit([](const auto& n) -> Note {return n.mirror_vertically();}, this->note); + } + + Note Note::rotate_90_clockwise() const { + return std::visit([](const auto& n) -> Note {return n.rotate_90_clockwise();}, this->note); + } + + Note Note::rotate_90_counter_clockwise() const { + return std::visit([](const auto& n) -> Note {return n.rotate_90_counter_clockwise();}, this->note); + } + + Note Note::rotate_180() const { + return std::visit([](const auto& n) -> Note {return n.rotate_180();}, this->note); + } } diff --git a/src/better_note.hpp b/src/better_note.hpp index 3690dee..2cd94e9 100644 --- a/src/better_note.hpp +++ b/src/better_note.hpp @@ -35,6 +35,11 @@ namespace better { auto operator<=>(const Position&) const = default; friend std::ostream& operator<<(std::ostream& out, const Position& pos); + Position mirror_horizontally() const; + Position mirror_vertically() const; + Position rotate_90_clockwise() const; + Position rotate_90_counter_clockwise() const; + Position rotate_180() const; private: std::uint64_t x; std::uint64_t y; @@ -50,6 +55,12 @@ namespace better { friend std::ostream& operator<<(std::ostream& out, const TapNote& t); nlohmann::ordered_json dump_to_memon_1_0_0() const; + + TapNote mirror_horizontally() const; + TapNote mirror_vertically() const; + TapNote rotate_90_clockwise() const; + TapNote rotate_90_counter_clockwise() const; + TapNote rotate_180() const; private: Fraction time; Position position; @@ -72,6 +83,12 @@ namespace better { nlohmann::ordered_json dump_to_memon_1_0_0() const; int tail_as_6_notation() const; + + LongNote mirror_horizontally() const; + LongNote mirror_vertically() const; + LongNote rotate_90_clockwise() const; + LongNote rotate_90_counter_clockwise() const; + LongNote rotate_180() const; private: Fraction time; Position position; @@ -108,6 +125,12 @@ namespace better { const nlohmann::json& json, std::uint64_t resolution ); + + Note mirror_horizontally() const; + Note mirror_vertically() const; + Note rotate_90_clockwise() const; + Note rotate_90_counter_clockwise() const; + Note rotate_180() const; private: std::variant note; }; diff --git a/src/chart_state.cpp b/src/chart_state.cpp index 32af02d..8b4c706 100644 --- a/src/chart_state.cpp +++ b/src/chart_state.cpp @@ -164,6 +164,90 @@ void ChartState::delete_( } } +void ChartState::transform_selected_notes( + std::function transform +) { + if (not selected_stuff.notes.empty()) { + better::Notes removed = selected_stuff.notes; + // Erase all the original notes + for (const auto& [_, note] : selected_stuff.notes) { + chart.notes->erase(note); + } + // overwriting insert of the transformed notes + better::Notes transformed; + for (const auto& [_, note] : selected_stuff.notes) { + const auto transformed_note = transform(note); + transformed.insert(transformed_note); + auto&& erased = chart.notes->overwriting_insert(transformed_note); + removed.merge(std::move(erased)); + } + selected_stuff.notes = transformed; + history.push(std::make_shared(difficulty_name, removed, transformed)); + density_graph.should_recompute = true; + } +} + +void ChartState::mirror_selected_horizontally(NotificationsQueue& nq) { + if (not selected_stuff.notes.empty()) { + const auto message = fmt::format( + "Mirrored {} note{}", + selected_stuff.notes.size(), + selected_stuff.notes.size() > 1 ? "s" : "" + ); + nq.push(std::make_shared(message)); + transform_selected_notes([](const better::Note& n){return n.mirror_horizontally();}); + } +} + +void ChartState::mirror_selected_vertically(NotificationsQueue& nq) { + if (not selected_stuff.notes.empty()) { + const auto message = fmt::format( + "Mirrored {} note{}", + selected_stuff.notes.size(), + selected_stuff.notes.size() > 1 ? "s" : "" + ); + nq.push(std::make_shared(message)); + transform_selected_notes([](const better::Note& n){return n.mirror_vertically();}); + } +} + +void ChartState::rotate_selected_90_clockwise(NotificationsQueue& nq) { + if (not selected_stuff.notes.empty()) { + const auto message = fmt::format( + "Rotated {} note{}", + selected_stuff.notes.size(), + selected_stuff.notes.size() > 1 ? "s" : "" + ); + nq.push(std::make_shared(message)); + transform_selected_notes([](const better::Note& n){return n.rotate_90_clockwise();}); + } +} + +void ChartState::rotate_selected_90_counter_clockwise(NotificationsQueue& nq) { + if (not selected_stuff.notes.empty()) { + const auto message = fmt::format( + "Rotated {} note{}", + selected_stuff.notes.size(), + selected_stuff.notes.size() > 1 ? "s" : "" + ); + nq.push(std::make_shared(message)); + transform_selected_notes([](const better::Note& n){return n.rotate_90_counter_clockwise();}); + } +} + +void ChartState::rotate_selected_180(NotificationsQueue& nq) { + if (not selected_stuff.notes.empty()) { + const auto message = fmt::format( + "Rotated {} note{}", + selected_stuff.notes.size(), + selected_stuff.notes.size() > 1 ? "s" : "" + ); + nq.push(std::make_shared(message)); + transform_selected_notes([](const better::Note& n){return n.rotate_180();}); + } +} + + Interval ChartState::visible_beats(const sf::Time& playback_position, const better::Timing& timing) { /* Approach and burst animations last (at most) 16 frames at 30 fps on diff --git a/src/chart_state.hpp b/src/chart_state.hpp index fd8aabe..59ba8d7 100644 --- a/src/chart_state.hpp +++ b/src/chart_state.hpp @@ -47,6 +47,13 @@ struct ChartState { const TimingOrigin& timing_origin ); + void transform_selected_notes(std::function transform); + void mirror_selected_horizontally(NotificationsQueue& nq); + void mirror_selected_vertically(NotificationsQueue& nq); + void rotate_selected_90_clockwise(NotificationsQueue& nq); + void rotate_selected_90_counter_clockwise(NotificationsQueue& nq); + void rotate_selected_180(NotificationsQueue& nq); + Interval visible_beats(const sf::Time& playback_position, const better::Timing& timing); void update_visible_notes(const sf::Time& playback_position, const better::Timing& timing); better::Notes visible_notes; @@ -58,7 +65,7 @@ struct ChartState { const better::Timing& timing ); - ClipboardContents selected_stuff; + NoteAndBPMSelection selected_stuff; Clipboard clipboard; void handle_time_selection_tab(Fraction beats); diff --git a/src/clipboard.cpp b/src/clipboard.cpp index a40177b..d9a564f 100644 --- a/src/clipboard.cpp +++ b/src/clipboard.cpp @@ -10,8 +10,8 @@ #include "src/better_timing.hpp" #include "variant_visitor.hpp" -ClipboardContents ClipboardContents::shifted_by(Fraction offset) const { - ClipboardContents res; +NoteAndBPMSelection NoteAndBPMSelection::shifted_by(Fraction offset) const { + NoteAndBPMSelection res; const auto shift = VariantVisitor { [&](const better::TapNote& tap_note) { return better::Note( @@ -43,16 +43,16 @@ ClipboardContents ClipboardContents::shifted_by(Fraction offset) const { return res; } -bool ClipboardContents::empty() const { +bool NoteAndBPMSelection::empty() const { return notes.empty() and bpm_events.empty(); } -void ClipboardContents::clear() { +void NoteAndBPMSelection::clear() { notes.clear(); bpm_events.clear(); } -void Clipboard::copy(const ClipboardContents& new_contents) { +void Clipboard::copy(const NoteAndBPMSelection& new_contents) { const auto offset = [&](){ std::set offsets = {}; if (not new_contents.notes.empty()) { @@ -71,7 +71,7 @@ void Clipboard::copy(const ClipboardContents& new_contents) { contents = new_contents.shifted_by(-1 * offset); } -ClipboardContents Clipboard::paste(Fraction offset) const { +NoteAndBPMSelection Clipboard::paste(Fraction offset) const { return contents.shifted_by(offset); } diff --git a/src/clipboard.hpp b/src/clipboard.hpp index 4943522..fa31b84 100644 --- a/src/clipboard.hpp +++ b/src/clipboard.hpp @@ -9,11 +9,11 @@ #include "src/better_timing.hpp" #include "variant_visitor.hpp" -struct ClipboardContents { +struct NoteAndBPMSelection { better::Notes notes; std::set bpm_events; - ClipboardContents shifted_by(Fraction offset) const; + NoteAndBPMSelection shifted_by(Fraction offset) const; bool empty() const; void clear(); }; @@ -26,12 +26,12 @@ all the note starting times. class Clipboard { public: Clipboard() = default; - void copy(const ClipboardContents& contents); - ClipboardContents paste(Fraction beat) const; + void copy(const NoteAndBPMSelection& contents); + NoteAndBPMSelection paste(Fraction beat) const; bool empty() const; private: - ClipboardContents contents; + NoteAndBPMSelection contents; }; diff --git a/src/history.cpp b/src/history.cpp new file mode 100644 index 0000000..5d05d6f --- /dev/null +++ b/src/history.cpp @@ -0,0 +1,96 @@ +#include "history.hpp" + +#include + + +std::optional History::pop_previous() { + if (previous_actions.empty()) { + return {}; + } else { + auto elt = previous_actions.front(); + next_actions.push_front(elt); + previous_actions.pop_front(); + return elt; + } +} + +std::optional History::pop_next() { + if (next_actions.empty()) { + return {}; + } else { + auto elt = next_actions.front(); + previous_actions.push_front(elt); + next_actions.pop_front(); + return elt; + } +} + +void History::push(const History::item& elt) { + previous_actions.push_front(elt); + if (not next_actions.empty()) { + next_actions.clear(); + } +} + +void History::display(bool& show) { + if (ImGui::Begin("History", &show)) { + for (const auto& it : next_actions | std::views::reverse) { + ImGui::TextUnformatted(it->get_message().c_str()); + if (last_saved_action and std::holds_alternative(*last_saved_action)) { + if (std::get(*last_saved_action) == it) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); + } + } + } + for (const auto& it : previous_actions) { + ImGui::TextUnformatted(it->get_message().c_str()); + if (it == *previous_actions.cbegin()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4, 0.8, 1, 1), "current"); + } + if (last_saved_action and std::holds_alternative(*last_saved_action)) { + if (std::get(*last_saved_action) == it) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); + } + } + } + ImGui::TextUnformatted("(initial state)"); + if (previous_actions.empty()) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.4, 0.8, 1, 1), "current"); + } + if (last_saved_action and std::holds_alternative(*last_saved_action)) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); + } + } + ImGui::End(); +} + +void History::mark_as_saved() { + if (not previous_actions.empty()) { + last_saved_action = previous_actions.front(); + } else { + last_saved_action = InitialStateSaved{}; + } +} + +bool History::current_state_is_saved() const { + if (not last_saved_action) { + return false; + } else { + const auto is_saved_ = VariantVisitor { + [&](const InitialStateSaved& i) { return previous_actions.empty(); }, + [&](const item& i) { + if (not previous_actions.empty()) { + return i == previous_actions.front(); + } else { + return false; + } + } + }; + return std::visit(is_saved_, *last_saved_action); + } +} diff --git a/src/history.hpp b/src/history.hpp index 7464a71..820d4e7 100644 --- a/src/history.hpp +++ b/src/history.hpp @@ -1,5 +1,4 @@ -#ifndef FEIS_HISTORY_H -#define FEIS_HISTORY_H +#pragma once #include #include @@ -7,14 +6,10 @@ #include #include #include - -#include #include #include "history_item.hpp" -struct InitialStateSaved {}; - /* * History implemented this way : * @@ -35,102 +30,15 @@ struct InitialStateSaved {}; class History { using item = std::shared_ptr; public: - std::optional pop_previous() { - if (previous_actions.empty()) { - return {}; - } else { - auto elt = previous_actions.front(); - next_actions.push_front(elt); - previous_actions.pop_front(); - return elt; - } - } - - std::optional pop_next() { - if (next_actions.empty()) { - return {}; - } else { - auto elt = next_actions.front(); - previous_actions.push_front(elt); - next_actions.pop_front(); - return elt; - } - } - - void push(const item& elt) { - previous_actions.push_front(elt); - if (not next_actions.empty()) { - next_actions.clear(); - } - } - - void display(bool& show) { - if (ImGui::Begin("History", &show)) { - for (const auto& it : next_actions | std::views::reverse) { - ImGui::TextUnformatted(it->get_message().c_str()); - if (last_saved_action and std::holds_alternative(*last_saved_action)) { - if (std::get(*last_saved_action) == it) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); - } - } - } - for (const auto& it : previous_actions) { - ImGui::TextUnformatted(it->get_message().c_str()); - if (it == *previous_actions.cbegin()) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.4, 0.8, 1, 1), "current"); - } - if (last_saved_action and std::holds_alternative(*last_saved_action)) { - if (std::get(*last_saved_action) == it) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); - } - } - } - ImGui::TextUnformatted("(initial state)"); - if (previous_actions.empty()) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.4, 0.8, 1, 1), "current"); - } - if (last_saved_action and std::holds_alternative(*last_saved_action)) { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.3, 0.84,0.08,1), "saved"); - } - } - ImGui::End(); - } - - void mark_as_saved() { - if (not previous_actions.empty()) { - last_saved_action = previous_actions.front(); - } else { - last_saved_action = InitialStateSaved{}; - } - } - - bool current_state_is_saved() const { - if (not last_saved_action) { - return false; - } else { - const auto is_saved_ = VariantVisitor { - [&](const InitialStateSaved& i) { return previous_actions.empty(); }, - [&](const item& i) { - if (not previous_actions.empty()) { - return i == previous_actions.front(); - } else { - return false; - } - } - }; - return std::visit(is_saved_, *last_saved_action); - } - }; - + struct InitialStateSaved {}; + std::optional pop_previous(); + std::optional pop_next(); + void push(const item& elt); + void display(bool& show); + void mark_as_saved(); + bool current_state_is_saved() const; private: std::deque previous_actions; std::deque next_actions; std::optional> last_saved_action; }; - -#endif // FEIS_HISTORY_H diff --git a/src/history_item.cpp b/src/history_item.cpp index fd0fbdb..c8a27e8 100644 --- a/src/history_item.cpp +++ b/src/history_item.cpp @@ -45,6 +45,7 @@ void AddNotes::do_action(EditorState& ed) const { ed.chart_state->chart.notes->insert(note); } ed.chart_state->density_graph.should_recompute = true; + ed.chart_state->selected_stuff.notes = notes; } } @@ -58,6 +59,7 @@ void AddNotes::undo_action(EditorState& ed) const { ed.chart_state->chart.notes->erase(note); } ed.chart_state->density_graph.should_recompute = true; + ed.chart_state->selected_stuff.notes.clear(); } } @@ -87,6 +89,65 @@ void RemoveNotes::undo_action(EditorState& ed) const { AddNotes::do_action(ed); } +RemoveThenAddNotes::RemoveThenAddNotes( + const std::string& chart, + const better::Notes& removed, + const better::Notes& added +) : + difficulty_name(chart), + removed(removed), + added(added) +{ + if (removed.empty() or added.empty()) { + throw std::invalid_argument( + "Can't construct a RemoveThenAddNotes History Action with an empty" + "note set" + ); + } + message = fmt::format( + "Removed {} note{} and added {} note{} from chart {}", + removed.size(), + removed.size() > 1 ? "s" : "", + added.size(), + added.size() > 1 ? "s" : "", + chart + ); +} + +void RemoveThenAddNotes::do_action(EditorState& ed) const { + ed.set_playback_position(ed.time_at(added.begin()->second.get_time())); + if (ed.chart_state) { + if (not (ed.chart_state->difficulty_name == difficulty_name)) { + ed.open_chart(difficulty_name); + } + for (const auto& [_, note] : removed) { + ed.chart_state->chart.notes->erase(note); + } + for (const auto& [_, note] : added) { + ed.chart_state->chart.notes->insert(note); + } + ed.chart_state->density_graph.should_recompute = true; + ed.chart_state->selected_stuff.notes = added; + } +} + +void RemoveThenAddNotes::undo_action(EditorState& ed) const { + ed.set_playback_position(ed.time_at(added.begin()->second.get_time())); + if (ed.chart_state) { + if (not (ed.chart_state->difficulty_name == difficulty_name)) { + ed.open_chart(difficulty_name); + } + for (const auto& [_, note] : added) { + ed.chart_state->chart.notes->erase(note); + } + for (const auto& [_, note] : removed) { + ed.chart_state->chart.notes->insert(note); + } + ed.chart_state->density_graph.should_recompute = true; + ed.chart_state->selected_stuff.notes = removed; + } +} + AddChart::AddChart(const std::string& difficulty_name_, const better::Chart& chart_) : difficulty_name(difficulty_name_), chart(chart_) diff --git a/src/history_item.hpp b/src/history_item.hpp index d50a8b9..07bcd27 100644 --- a/src/history_item.hpp +++ b/src/history_item.hpp @@ -55,6 +55,22 @@ public: void undo_action(EditorState& ed) const override; }; +class RemoveThenAddNotes : public HistoryItem { +public: + RemoveThenAddNotes( + const std::string& difficulty_name, + const better::Notes& removed, + const better::Notes& added + ); + + void do_action(EditorState& ed) const override; + void undo_action(EditorState& ed) const override; +protected: + std::string difficulty_name; + better::Notes removed; + better::Notes added; +}; + class AddChart : public HistoryItem { public: AddChart( diff --git a/src/main.cpp b/src/main.cpp index aa6ba28..d2f2e2a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -642,6 +642,40 @@ int main() { } ImGui::EndMenu(); } + if (ImGui::BeginMenu("Notes", editor_state.has_value())) { + if (ImGui::BeginMenu("Mirror")) { + if (ImGui::MenuItem("Horizontally")) { + if (editor_state->chart_state.has_value()) { + editor_state->chart_state->mirror_selected_horizontally(notificationsQueue); + } + } + if (ImGui::MenuItem("Vertically")) { + if (editor_state->chart_state.has_value()) { + editor_state->chart_state->mirror_selected_vertically(notificationsQueue); + } + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Rotate")) { + if (ImGui::MenuItem("90° Clockwise")) { + if (editor_state->chart_state.has_value()) { + editor_state->chart_state->rotate_selected_90_clockwise(notificationsQueue); + } + } + if (ImGui::MenuItem("90° Counter-Clockwise")) { + if (editor_state->chart_state.has_value()) { + editor_state->chart_state->rotate_selected_90_counter_clockwise(notificationsQueue); + } + } + if (ImGui::MenuItem("180°")) { + if (editor_state->chart_state.has_value()) { + editor_state->chart_state->rotate_selected_180(notificationsQueue); + } + } + ImGui::EndMenu(); + } + ImGui::EndMenu(); + } if (ImGui::BeginMenu("View", editor_state.has_value())) { if (ImGui::MenuItem("Playfield", nullptr, editor_state->show_playfield)) { editor_state->show_playfield = not editor_state->show_playfield; diff --git a/src/meson.build b/src/meson.build index a7be0f4..0cc6db5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -14,6 +14,7 @@ sources += files( 'config.cpp', 'editor_state.cpp', 'file_dialogs.cpp', + 'history.cpp', 'history_item.cpp', 'imgui_extras.cpp', 'json_decimal_handling.cpp',