mirror of
https://gitlab.com/square-game-liberation-front/F.E.I.S.git
synced 2025-03-01 16:00:24 +01:00
354 lines
12 KiB
C++
354 lines
12 KiB
C++
#include "chart_state.hpp"
|
|
|
|
#include <memory>
|
|
|
|
#include <fmt/core.h>
|
|
#include <SFML/System/Time.hpp>
|
|
|
|
#include "better_note.hpp"
|
|
#include "better_notes.hpp"
|
|
#include "history_item.hpp"
|
|
#include "special_numeric_types.hpp"
|
|
|
|
ChartState::ChartState(
|
|
better::Chart& c,
|
|
const std::string& name,
|
|
History& history,
|
|
std::filesystem::path assets,
|
|
const config::Config& config_
|
|
) :
|
|
chart(c),
|
|
difficulty_name(name),
|
|
config(config_),
|
|
history(history),
|
|
density_graph(assets, config_)
|
|
{}
|
|
|
|
void ChartState::cut(
|
|
NotificationsQueue& nq,
|
|
better::Timing& timing,
|
|
const TimingOrigin& timing_origin
|
|
) {
|
|
if (not selected_stuff.empty()) {
|
|
if (not selected_stuff.notes.empty()) {
|
|
const auto message = fmt::format(
|
|
"Cut {} note{}",
|
|
selected_stuff.notes.size(),
|
|
selected_stuff.notes.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
for (const auto& [_, note] : selected_stuff.notes) {
|
|
chart.notes->erase(note);
|
|
}
|
|
history.push(std::make_shared<RemoveNotes>(difficulty_name, selected_stuff.notes));
|
|
density_graph.should_recompute = true;
|
|
}
|
|
if (not selected_stuff.bpm_events.empty()) {
|
|
const auto before = timing;
|
|
for (const auto& bpm_event : selected_stuff.bpm_events) {
|
|
timing.erase(bpm_event);
|
|
}
|
|
if (before != timing) {
|
|
const auto message = fmt::format(
|
|
"Cut {} BPM event{}",
|
|
selected_stuff.bpm_events.size(),
|
|
selected_stuff.bpm_events.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
history.push(std::make_shared<ChangeTiming>(before, timing, timing_origin));
|
|
}
|
|
}
|
|
}
|
|
|
|
clipboard.copy(selected_stuff);
|
|
selected_stuff.clear();
|
|
};
|
|
|
|
void ChartState::copy(NotificationsQueue& nq) {
|
|
if (not selected_stuff.notes.empty()) {
|
|
const auto message = fmt::format(
|
|
"Copied {} note{}",
|
|
selected_stuff.notes.size(),
|
|
selected_stuff.notes.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
}
|
|
|
|
if (not selected_stuff.bpm_events.empty()) {
|
|
const auto message = fmt::format(
|
|
"Copied {} BPM event{}",
|
|
selected_stuff.bpm_events.size(),
|
|
selected_stuff.bpm_events.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
}
|
|
|
|
clipboard.copy(selected_stuff);
|
|
selected_stuff.clear();
|
|
};
|
|
|
|
void ChartState::paste(
|
|
Fraction at_beat,
|
|
NotificationsQueue& nq,
|
|
better::Timing& timing,
|
|
const TimingOrigin& timing_origin
|
|
) {
|
|
if (clipboard.empty()) {
|
|
return;
|
|
}
|
|
|
|
const auto pasted_stuff = clipboard.paste(at_beat);
|
|
if (not pasted_stuff.notes.empty()) {
|
|
const auto message = fmt::format(
|
|
"Pasted {} note{}",
|
|
pasted_stuff.notes.size(),
|
|
pasted_stuff.notes.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
better::Notes overwritten;
|
|
for (const auto& [_, note] : pasted_stuff.notes) {
|
|
auto&& erased = chart.notes->overwriting_insert(note);
|
|
overwritten.merge(std::move(erased));
|
|
}
|
|
if (not overwritten.empty()) {
|
|
history.push(std::make_shared<RemoveNotes>(difficulty_name, overwritten));
|
|
}
|
|
selected_stuff.notes = pasted_stuff.notes;
|
|
history.push(std::make_shared<AddNotes>(difficulty_name, selected_stuff.notes));
|
|
density_graph.should_recompute = true;
|
|
}
|
|
if (not pasted_stuff.bpm_events.empty()) {
|
|
const auto before = timing;
|
|
for (const auto& bpm_event : pasted_stuff.bpm_events) {
|
|
timing.insert(bpm_event);
|
|
}
|
|
if (before != timing) {
|
|
const auto message = fmt::format(
|
|
"Pasted {} BPM event{}",
|
|
selected_stuff.bpm_events.size(),
|
|
selected_stuff.bpm_events.size() > 1 ? "s" : ""
|
|
);
|
|
nq.push(std::make_shared<TextNotification>(message));
|
|
history.push(std::make_shared<ChangeTiming>(before, timing, timing_origin));
|
|
}
|
|
}
|
|
};
|
|
|
|
void ChartState::delete_(
|
|
NotificationsQueue& nq,
|
|
better::Timing& timing,
|
|
const TimingOrigin& timing_origin
|
|
) {
|
|
if (not selected_stuff.notes.empty()) {
|
|
history.push(std::make_shared<RemoveNotes>(difficulty_name, selected_stuff.notes));
|
|
nq.push(
|
|
std::make_shared<TextNotification>("Deleted selected notes")
|
|
);
|
|
for (const auto& [_, note] : selected_stuff.notes) {
|
|
chart.notes->erase(note);
|
|
}
|
|
selected_stuff.notes.clear();
|
|
density_graph.should_recompute = true;
|
|
}
|
|
if (not selected_stuff.bpm_events.empty()) {
|
|
const auto before = timing;
|
|
for (const auto& bpm_event : selected_stuff.bpm_events) {
|
|
timing.erase(bpm_event);
|
|
}
|
|
if (before != timing) {
|
|
history.push(std::make_shared<ChangeTiming>(before, timing, timing_origin));
|
|
nq.push(
|
|
std::make_shared<TextNotification>("Deleted selected BPM events")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
// create the set of transformed notes independently first :
|
|
// if two transformed notes overwrite each other, better not keep them
|
|
// separate
|
|
better::Notes transformed;
|
|
for (const auto& [_, note] : selected_stuff.notes) {
|
|
const auto transformed_note = transform(note);
|
|
transformed.insert(transformed_note);
|
|
}
|
|
|
|
// overwriting insert of the whole set of transformed notes
|
|
for (const auto& [_, transformed_note] : transformed) {
|
|
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_selection_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_selection_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_selection_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_selection_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_selection_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();});
|
|
}
|
|
}
|
|
|
|
void ChartState::quantize_selection(unsigned int snap, NotificationsQueue& nq) {
|
|
if (selected_stuff.notes.empty()) {
|
|
return;
|
|
}
|
|
const auto message = fmt::format(
|
|
"Quantized {} 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.quantize(snap);});
|
|
}
|
|
|
|
|
|
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
|
|
original jubeat markers, so we look for notes that have ended less than
|
|
16/30 seconds ago or that will start in less than 16/30 seconds
|
|
*/
|
|
const auto earliest_visible_time = playback_position - sf::seconds(16.f/30.f);
|
|
const auto latest_visible_time = playback_position + sf::seconds(16.f/30.f);
|
|
|
|
const auto earliest_visible_beat = timing.beats_at(earliest_visible_time);
|
|
const auto latest_visible_beat = timing.beats_at(latest_visible_time);
|
|
|
|
return {earliest_visible_beat, latest_visible_beat};
|
|
}
|
|
|
|
void ChartState::update_visible_notes(const sf::Time& playback_position, const better::Timing& timing) {
|
|
const auto bounds = visible_beats(playback_position, timing);
|
|
visible_notes = chart.notes->between(bounds);
|
|
};
|
|
|
|
void ChartState::toggle_note(
|
|
const sf::Time& playback_position,
|
|
std::uint64_t snap,
|
|
const better::Position& button,
|
|
const better::Timing& timing
|
|
) {
|
|
better::Notes toggled_notes;
|
|
const auto bounds = visible_beats(playback_position, timing);
|
|
chart.notes->in(
|
|
{bounds.start, bounds.end},
|
|
[&](const better::Notes::const_iterator& it){
|
|
if (it->second.get_position() == button) {
|
|
toggled_notes.insert(it->second);
|
|
}
|
|
}
|
|
);
|
|
if (not toggled_notes.empty()) {
|
|
for (const auto& [_, note] : toggled_notes) {
|
|
chart.notes->erase(note);
|
|
}
|
|
history.push(std::make_shared<RemoveNotes>(difficulty_name, toggled_notes));
|
|
} else {
|
|
const auto rounded_beats = round_beats(
|
|
timing.beats_at(playback_position),
|
|
snap
|
|
);
|
|
const auto new_note = better::TapNote{rounded_beats, button};
|
|
toggled_notes.insert(new_note);
|
|
chart.notes->insert(new_note);
|
|
history.push(std::make_shared<AddNotes>(difficulty_name, toggled_notes));
|
|
}
|
|
density_graph.should_recompute = true;
|
|
}
|
|
|
|
void ChartState::handle_time_selection_tab(Fraction beats) {
|
|
if (not time_selection) {
|
|
time_selection.emplace(beats, beats);
|
|
} else {
|
|
if (time_selection->width() == 0) {
|
|
*time_selection += beats;
|
|
selected_stuff.notes = chart.notes->between(*time_selection);
|
|
} else {
|
|
time_selection.emplace(beats, beats);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChartState::insert_long_note_just_created(std::uint64_t snap) {
|
|
if (not long_note_being_created) {
|
|
return;
|
|
}
|
|
auto new_note = make_long_note_dummy_for_linear_view(
|
|
*long_note_being_created,
|
|
Fraction{1, snap}
|
|
);
|
|
better::Notes new_notes;
|
|
new_notes.insert(new_note);
|
|
const auto& overwritten = chart.notes->overwriting_insert(new_note);
|
|
long_note_being_created.reset();
|
|
creating_long_note = false;
|
|
if (not overwritten.empty()) {
|
|
history.push(std::make_shared<RemoveNotes>(difficulty_name, overwritten));
|
|
}
|
|
history.push(std::make_shared<AddNotes>(difficulty_name,new_notes));
|
|
} |