Add note transform options

This commit is contained in:
Stepland 2022-12-31 23:54:59 +01:00
parent 930a7824e4
commit 76c167ce11
12 changed files with 449 additions and 112 deletions

View File

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

View File

@ -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<TapNote, LongNote> note;
};

View File

@ -164,6 +164,90 @@ void ChartState::delete_(
}
}
void ChartState::transform_selected_notes(
std::function<better::Note(const better::Note&)> 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<RemoveThenAddNotes>(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<TextNotification>(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<TextNotification>(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<TextNotification>(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<TextNotification>(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<TextNotification>(message));
transform_selected_notes([](const better::Note& n){return n.rotate_180();});
}
}
Interval<Fraction> 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

View File

@ -47,6 +47,13 @@ struct ChartState {
const TimingOrigin& timing_origin
);
void transform_selected_notes(std::function<better::Note(const better::Note&)> 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<Fraction> 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);

View File

@ -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<Fraction> 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);
}

View File

@ -9,11 +9,11 @@
#include "src/better_timing.hpp"
#include "variant_visitor.hpp"
struct ClipboardContents {
struct NoteAndBPMSelection {
better::Notes notes;
std::set<better::BPMAtBeat, better::Timing::beat_order_for_events> 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;
};

96
src/history.cpp Normal file
View File

@ -0,0 +1,96 @@
#include "history.hpp"
#include <imgui/imgui.h>
std::optional<History::item> 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::item> 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<item>(*last_saved_action)) {
if (std::get<item>(*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<item>(*last_saved_action)) {
if (std::get<item>(*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<InitialStateSaved>(*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);
}
}

View File

@ -1,5 +1,4 @@
#ifndef FEIS_HISTORY_H
#define FEIS_HISTORY_H
#pragma once
#include <deque>
#include <functional>
@ -7,14 +6,10 @@
#include <optional>
#include <ranges>
#include <stack>
#include <imgui/imgui.h>
#include <variant>
#include "history_item.hpp"
struct InitialStateSaved {};
/*
* History implemented this way :
*
@ -35,102 +30,15 @@ struct InitialStateSaved {};
class History {
using item = std::shared_ptr<HistoryItem>;
public:
std::optional<item> 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<item> 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<item>(*last_saved_action)) {
if (std::get<item>(*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<item>(*last_saved_action)) {
if (std::get<item>(*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<InitialStateSaved>(*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<item> pop_previous();
std::optional<item> pop_next();
void push(const item& elt);
void display(bool& show);
void mark_as_saved();
bool current_state_is_saved() const;
private:
std::deque<item> previous_actions;
std::deque<item> next_actions;
std::optional<std::variant<InitialStateSaved, item>> last_saved_action;
};
#endif // FEIS_HISTORY_H

View File

@ -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_)

View File

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

View File

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

View File

@ -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',