From bb7c1f859311149cc7ed6dfe5084f77b317c1b69 Mon Sep 17 00:00:00 2001 From: Stepland <10530295-Buggyroom@users.noreply.gitlab.com> Date: Tue, 14 Mar 2023 01:00:20 +0100 Subject: [PATCH] created the waveform draw function --- src/editor_state.hpp | 2 +- src/linear_view_colors.cpp | 2 + src/linear_view_colors.hpp | 1 + src/ln_marker.hpp | 2 +- src/waveform.cpp | 7 + src/waveform.hpp | 4 + src/widgets/linear_view.cpp | 529 ++++++++++++++++++++++++++++++------ src/widgets/linear_view.hpp | 53 +++- 8 files changed, 499 insertions(+), 101 deletions(-) diff --git a/src/editor_state.hpp b/src/editor_state.hpp index 8dc9913..517b229 100644 --- a/src/editor_state.hpp +++ b/src/editor_state.hpp @@ -69,7 +69,7 @@ public: std::shared_ptr chord_claps; std::shared_ptr beat_ticks; std::optional> music = {}; - Toolkit::Cache> waveform_cache = {waveform::compute_waveform}; + waveform::Cache waveform_cache; bool is_playing_preview_music_from_sss = false; int get_volume() const; diff --git a/src/linear_view_colors.cpp b/src/linear_view_colors.cpp index c7904ae..a2d9246 100644 --- a/src/linear_view_colors.cpp +++ b/src/linear_view_colors.cpp @@ -19,6 +19,7 @@ namespace linear_view { load_color(colors_node["measure_line"], measure_line); load_color(colors_node["measure_number"], measure_number); load_color(colors_node["beat_line"], beat_line); + load_color(colors_node["waveform"], waveform); load_color(colors_node["bpm_button"]["text"], bpm_button.text); load_color(colors_node["bpm_button"]["button"], bpm_button.button); load_color(colors_node["bpm_button"]["hover"], bpm_button.hover); @@ -46,6 +47,7 @@ namespace linear_view { {"measure_line", dump_color(measure_line)}, {"measure_number", dump_color(measure_number)}, {"beat_line", dump_color(beat_line)}, + {"waveform", dump_color(waveform)}, {"bpm_button", toml::table{ {"text", dump_color(bpm_button.text)}, {"button", dump_color(bpm_button.button)}, diff --git a/src/linear_view_colors.hpp b/src/linear_view_colors.hpp index fa66f32..38ac51c 100644 --- a/src/linear_view_colors.hpp +++ b/src/linear_view_colors.hpp @@ -34,6 +34,7 @@ namespace linear_view { sf::Color measure_line = sf::Color::White; sf::Color measure_number = sf::Color::White; sf::Color beat_line = {255, 255, 255, 127}; + sf::Color waveform = sf::Color::White; ButtonColors bpm_button = { .text = {66, 150, 250}, .button = sf::Color::Transparent, diff --git a/src/ln_marker.hpp b/src/ln_marker.hpp index c2ff444..b301912 100644 --- a/src/ln_marker.hpp +++ b/src/ln_marker.hpp @@ -56,7 +56,7 @@ private: int frame_from_offset(const sf::Time& offset); template -std::array load_tex_with_prefix( +std::array load_tex_with_prefix( const std::filesystem::path& folder, const std::string& prefix, const unsigned int first = 0 diff --git a/src/waveform.cpp b/src/waveform.cpp index 5e1e000..47f8496 100644 --- a/src/waveform.cpp +++ b/src/waveform.cpp @@ -78,4 +78,11 @@ namespace waveform { } return waveform; } + + Cache::Cache() + : Toolkit::Cache> + (compute_waveform) + { + + } } \ No newline at end of file diff --git a/src/waveform.hpp b/src/waveform.hpp index c94e344..8df9eaf 100644 --- a/src/waveform.hpp +++ b/src/waveform.hpp @@ -28,4 +28,8 @@ namespace waveform { ); Channels downsample_to_half(const Channels& summary); std::optional compute_waveform(const std::filesystem::path& audio); + + struct Cache : Toolkit::Cache> { + Cache(); + }; } \ No newline at end of file diff --git a/src/widgets/linear_view.cpp b/src/widgets/linear_view.cpp index 7f1e876..cef0781 100644 --- a/src/widgets/linear_view.cpp +++ b/src/widgets/linear_view.cpp @@ -41,10 +41,6 @@ void SelectionRectangle::reset() { } namespace linear_view::mode { - Waveform::Waveform(const std::filesystem::path& audio) { - sound_file.open_from_path(audio); - worker = std::jthread{&Waveform::prepare_data, this}; - } void Waveform::draw_waveform( ImDrawList* draw_list, @@ -130,34 +126,69 @@ LinearView::LinearView(std::filesystem::path assets, config::Config& config_) : set_zoom(config_.linear_view.zoom); } -void LinearView::draw( - ImDrawList* draw_list, - ChartState& chart_state, - const better::Timing& timing, - const Fraction& current_beat, - const Fraction& last_editable_beat, - const Fraction& snap, - const sf::Vector2f& size, - const sf::Vector2f& origin -) { - int x = std::max(300, static_cast(size.x)); - int y = std::max(300, static_cast(size.y)); +void LinearView::draw(LinearView::draw_args_type& args) { + const auto draw_function = VariantVisitor { + [&](linear_view::mode::Beats) {this->draw_in_beats_mode(args);}, + [&](linear_view::mode::Waveform) {this->draw_in_waveform_mode(args);}, + }; + std::visit(draw_function, mode); +} - const float timeline_width = static_cast(x) - static_cast(sizes.timeline_margin); - const float timeline_left = static_cast(sizes.timeline_margin) / 2; - const float timeline_right = timeline_left + timeline_width; +linear_view::ComputedSizes linear_view::compute_sizes( + const sf::Vector2f& window_size, + linear_view::Sizes& size_settings +) { + ComputedSizes result; + result.x = std::max(300, static_cast(window_size.x)); + result.y = std::max(300, static_cast(window_size.y)); + + result.timeline_width = static_cast(result.x) - static_cast(size_settings.timeline_margin); + result.timeline_left = static_cast(size_settings.timeline_margin) / 2; + result.timeline_right = result.timeline_left + result.timeline_width; // Just in case, clamp the beat cursor inside the window, with some margin - const float cursor_y = std::clamp(static_cast(sizes.cursor_height), 25.f, static_cast(y) - 25.f); + result.cursor_y = std::clamp(static_cast(size_settings.cursor_height), 25.f, static_cast(result.y) - 25.f); + + result.bpm_events_left = result.timeline_right + 10; + + result.note_width = result.timeline_width / 16.f; + result.collizion_zone_width = result.note_width - 2.f; + result.long_note_rect_width = result.note_width * 0.75f; + + // Pre-size & center the shapes that can be // ?? + result.note_size = {result.note_width, 6.f}; + result.selected_note_size = {result.note_width + 2.f, 8.f}; + + result.cursor_width = result.timeline_width + 4.f; + result.cursor_left = result.timeline_left - 2; + result.cursor_size = {result.cursor_width, 4.f}; + result.cursor_pos = {result.cursor_left, result.cursor_y}; + + return result; +} + +void LinearView::draw_in_beats_mode(LinearView::draw_args_type& args) { + auto [ + draw_list, + chart_state, + waveform_cache, + timing, + current_beat, + last_editable_beat, + snap, + window_size, + origin + ] = args; + const auto computed_sizes = linear_view::compute_sizes(window_size, sizes); // Here we compute the range of visible beats from the size of the window // in pixels, we know by definition that the current beat is exactly at // cursor_y pixels and we use this fact to compute the rest - const auto beats_before_cursor = beats_to_pixels_proportional.backwards_transform(cursor_y); - const auto beats_after_cursor = beats_to_pixels_proportional.backwards_transform(static_cast(y) - cursor_y); + const auto beats_before_cursor = beats_to_pixels_proportional.backwards_transform(computed_sizes.cursor_y); + const auto beats_after_cursor = beats_to_pixels_proportional.backwards_transform(static_cast(computed_sizes.y) - computed_sizes.cursor_y); const Fraction first_beat_in_frame = current_beat - beats_before_cursor; const Fraction last_beat_in_frame = current_beat + beats_after_cursor; - AffineTransform beats_to_pixels_absolute{first_beat_in_frame, last_beat_in_frame, 0, y}; + AffineTransform beats_to_pixels_absolute{first_beat_in_frame, last_beat_in_frame, 0, computed_sizes.y}; const Fraction first_visible_beat = std::max(Fraction{0}, first_beat_in_frame); auto next_beat = [](const auto& first_beat) -> Fraction { @@ -171,17 +202,17 @@ void LinearView::draw( // Draw the beat lines and numbers for ( Fraction next_beat_line_y = beats_to_pixels_absolute.transform(next_beat); - next_beat_line_y < y and next_beat < last_editable_beat; + next_beat_line_y < computed_sizes.y and next_beat < last_editable_beat; next_beat_line_y = beats_to_pixels_absolute.transform(next_beat += 1) ) { - const sf::Vector2f beat_line_start = {timeline_left, static_cast(static_cast(next_beat_line_y))}; - const sf::Vector2f beat_line_end = beat_line_start + sf::Vector2f{timeline_width, 0}; + const sf::Vector2f beat_line_start = {computed_sizes.timeline_left, static_cast(static_cast(next_beat_line_y))}; + const sf::Vector2f beat_line_end = beat_line_start + sf::Vector2f{computed_sizes.timeline_width, 0}; if (next_beat % 4 == 0) { draw_list->AddLine(beat_line_start + origin, beat_line_end + origin, ImColor(colors.measure_line)); const Fraction measure = next_beat / 4; const auto measure_string = fmt::format("{}", static_cast(measure)); const sf::Vector2f text_size = ImGui::CalcTextSize(measure_string.c_str(), measure_string.c_str()+measure_string.size()); - const sf::Vector2f measure_text_pos = {timeline_left - 10, static_cast(static_cast(next_beat_line_y))}; + const sf::Vector2f measure_text_pos = {computed_sizes.timeline_left - 10, static_cast(static_cast(next_beat_line_y))}; draw_list->AddText( origin + measure_text_pos - sf::Vector2f{text_size.x, text_size.y * 0.5f}, ImColor(colors.measure_number), @@ -193,38 +224,27 @@ void LinearView::draw( } } - const float bpm_events_left = timeline_right + 10; - // Draw the bpm changes timing.for_each_event_between( first_beat_in_frame, last_beat_in_frame, [&](const auto& event){ const auto bpm_change_y = beats_to_pixels_absolute.transform(event.get_beats()); - if (bpm_change_y >= 0 and bpm_change_y <= y) { - const sf::Vector2f bpm_text_raw_pos = {bpm_events_left, static_cast(static_cast(bpm_change_y))}; + if (bpm_change_y >= 0 and bpm_change_y <= computed_sizes.y) { + const sf::Vector2f bpm_text_raw_pos = {computed_sizes.bpm_events_left, static_cast(static_cast(bpm_change_y))}; const auto bpm_at_beat = better::BPMAtBeat{event.get_bpm(), event.get_beats()}; - const auto selected = chart_state.selected_stuff.bpm_events.contains(bpm_at_beat); + const auto selected = args.chart_state.selected_stuff.bpm_events.contains(bpm_at_beat); if (BPMButton(event, selected, bpm_text_raw_pos, colors.bpm_button)) { if (selected) { - chart_state.selected_stuff.bpm_events.erase(bpm_at_beat); + args.chart_state.selected_stuff.bpm_events.erase(bpm_at_beat); } else { - chart_state.selected_stuff.bpm_events.insert(bpm_at_beat); + args.chart_state.selected_stuff.bpm_events.insert(bpm_at_beat); } } } } ); - float note_width = timeline_width / 16.f; - float collizion_zone_width = note_width - 2.f; - float long_note_rect_width = note_width * 0.75f; - - // Pre-size & center the shapes that can be - const sf::Vector2f note_size = {note_width, 6.f}; - - const sf::Vector2f selected_note_size = {note_width + 2.f, 8.f}; - // Draw the notes auto draw_note = VariantVisitor { [&](const better::TapNote& tap_note){ @@ -233,23 +253,23 @@ void LinearView::draw( return; } const auto lane = *opt_lane; - const float note_x = timeline_left + note_width * (lane + 0.5f); + const float note_x = computed_sizes.timeline_left + computed_sizes.note_width * (lane + 0.5f); const float note_y = static_cast(beats_to_pixels_absolute.transform(tap_note.get_time())); - const auto note_seconds = timing.time_at(tap_note.get_time()); - const auto first_colliding_beat = timing.beats_at(note_seconds - collision_zone * 0.5f); + const auto note_seconds = args.timing.time_at(tap_note.get_time()); + const auto first_colliding_beat = args.timing.beats_at(note_seconds - collision_zone * 0.5f); const auto collision_zone_y = beats_to_pixels_absolute.transform(first_colliding_beat); - const auto last_colliding_beat = timing.beats_at(note_seconds + collision_zone * 0.5f); + const auto last_colliding_beat = args.timing.beats_at(note_seconds + collision_zone * 0.5f); const auto collision_zone_height = beats_to_pixels_proportional.transform(last_colliding_beat - first_colliding_beat); const sf::Vector2f collision_zone_pos = { note_x, static_cast(static_cast(collision_zone_y)) }; const sf::Vector2f collizion_zone_size = { - collizion_zone_width, + computed_sizes.collizion_zone_width, static_cast(static_cast(collision_zone_height)) }; const auto collision_zone_color = [&](){ - if (chart_state.chart.notes->is_colliding(tap_note, timing, collision_zone)) { + if (args.chart_state.chart.notes->is_colliding(tap_note, args.timing, collision_zone)) { return colors.conflicting_collision_zone; } else { return colors.normal_collision_zone; @@ -258,32 +278,32 @@ void LinearView::draw( const auto tap_note_color = [&](){ if (use_quantization_colors) { return quantization_colors.color_at_beat(tap_note.get_time()); - } else if (chart_state.chart.notes->is_colliding(tap_note, timing, collision_zone)) { + } else if (args.chart_state.chart.notes->is_colliding(tap_note, args.timing, collision_zone)) { return colors.conflicting_tap_note; } else { return colors.normal_tap_note; } }(); draw_rectangle( - draw_list, - origin + collision_zone_pos, + args.draw_list, + args.origin + collision_zone_pos, collizion_zone_size, {0.5f, 0.f}, collision_zone_color ); const sf::Vector2f note_pos = {note_x, note_y}; draw_rectangle( - draw_list, - origin + note_pos, - note_size, + args.draw_list, + args.origin + note_pos, + computed_sizes.note_size, {0.5f, 0.5f}, tap_note_color ); - if (chart_state.selected_stuff.notes.contains(tap_note)) { + if (args.chart_state.selected_stuff.notes.contains(tap_note)) { draw_rectangle( - draw_list, - origin + note_pos, - selected_note_size, + args.draw_list, + args.origin + note_pos, + computed_sizes.selected_note_size, {0.5f, 0.5f}, colors.selected_note_fill, colors.selected_note_outline @@ -296,20 +316,20 @@ void LinearView::draw( return; } const auto lane = *opt_lane; - float note_x = timeline_left + note_width * (lane + 0.5f); + float note_x = computed_sizes.timeline_left + computed_sizes.note_width * (lane + 0.5f); float note_y = static_cast(beats_to_pixels_absolute.transform(long_note.get_time())); - const auto note_start_seconds = timing.time_at(long_note.get_time()); - const auto first_colliding_beat = timing.beats_at(note_start_seconds - collision_zone * 0.5f); + const auto note_start_seconds = args.timing.time_at(long_note.get_time()); + const auto first_colliding_beat = args.timing.beats_at(note_start_seconds - collision_zone * 0.5f); const auto collision_zone_y = beats_to_pixels_absolute.transform(first_colliding_beat); - const auto note_end_seconds = timing.time_at(long_note.get_end()); - const auto last_colliding_beat = timing.beats_at(note_end_seconds + collision_zone * 0.5f); + const auto note_end_seconds = args.timing.time_at(long_note.get_end()); + const auto last_colliding_beat = args.timing.beats_at(note_end_seconds + collision_zone * 0.5f); const auto collision_zone_height = beats_to_pixels_proportional.transform(last_colliding_beat - first_colliding_beat); const sf::Vector2f collision_zone_pos = { note_x, static_cast(static_cast(collision_zone_y)) }; const sf::Vector2f collision_zone_size = { - collizion_zone_width, + computed_sizes.collizion_zone_width, static_cast(static_cast(collision_zone_height)) }; auto collision_zone_color = colors.normal_collision_zone; @@ -321,7 +341,7 @@ void LinearView::draw( } }(); auto long_note_color = colors.normal_long_note; - if (chart_state.chart.notes->is_colliding(long_note, timing, collision_zone)) { + if (args.chart_state.chart.notes->is_colliding(long_note, args.timing, collision_zone)) { collision_zone_color = colors.conflicting_collision_zone; if (not use_quantization_colors) { tap_note_color = colors.conflicting_tap_note; @@ -329,37 +349,37 @@ void LinearView::draw( long_note_color = colors.conflicting_long_note; } draw_rectangle( - draw_list, - origin + collision_zone_pos, + args.draw_list, + args.origin + collision_zone_pos, collision_zone_size, {0.5f, 0.f}, collision_zone_color ); const auto long_note_rect_height = beats_to_pixels_proportional.transform(long_note.get_duration()); const sf::Vector2f long_note_size = { - long_note_rect_width, + computed_sizes.long_note_rect_width, static_cast(static_cast(long_note_rect_height)) }; const sf::Vector2f note_pos = {note_x, note_y}; draw_rectangle( - draw_list, - origin + note_pos, + args.draw_list, + args.origin + note_pos, long_note_size, {0.5f, 0.f}, long_note_color ); draw_rectangle( - draw_list, - origin + note_pos, - note_size, + args.draw_list, + args.origin + note_pos, + computed_sizes.note_size, {0.5f, 0.5f}, tap_note_color ); - if (chart_state.selected_stuff.notes.contains(long_note)) { + if (args.chart_state.selected_stuff.notes.contains(long_note)) { draw_rectangle( - draw_list, - origin + note_pos, - selected_note_size, + args.draw_list, + args.origin + note_pos, + computed_sizes.selected_note_size, {0.5f, 0.5f}, colors.selected_note_fill, colors.selected_note_outline @@ -390,32 +410,28 @@ void LinearView::draw( } // Draw the cursor - const float cursor_width = timeline_width + 4.f; - const float cursor_left = timeline_left - 2; - const sf::Vector2f cursor_size = {cursor_width, 4.f}; - const sf::Vector2f cursor_pos = {cursor_left, cursor_y}; draw_rectangle( draw_list, - origin + cursor_pos, - cursor_size, + origin + computed_sizes.cursor_pos, + computed_sizes.cursor_size, {0, 0.5}, colors.cursor ); // Draw the time selection - const float selection_width = timeline_width; + const float selection_width = computed_sizes.timeline_width; if (chart_state.time_selection.has_value()) { const auto pixel_interval = Interval{ beats_to_pixels_absolute.transform(chart_state.time_selection->start), beats_to_pixels_absolute.transform(chart_state.time_selection->end) }; - if (pixel_interval.intersects({0, y})) { + if (pixel_interval.intersects({0, computed_sizes.y})) { const sf::Vector2f selection_size = { selection_width, static_cast(static_cast(pixel_interval.width())) }; const sf::Vector2f selection_pos = { - timeline_left, + computed_sizes.timeline_left, static_cast(static_cast(pixel_interval.start)) }; draw_rectangle( @@ -464,7 +480,7 @@ void LinearView::draw( std::max(selection_rectangle.start.y, selection_rectangle.end.y), }; const ImRect full_selection = {upper_left, lower_right}; - ImRect bpm_zone = {origin.x + bpm_events_left, -INFINITY, INFINITY, INFINITY}; + ImRect bpm_zone = {origin.x + computed_sizes.bpm_events_left, -INFINITY, INFINITY, INFINITY}; bpm_zone.ClipWith(current_window->InnerRect); if (full_selection.Overlaps(bpm_zone)) { const auto first_selected_beat = beats_to_pixels_absolute.backwards_transform(full_selection.Min.y - origin.y); @@ -473,7 +489,342 @@ void LinearView::draw( first_selected_beat, last_selected_beat, [&](const auto& event){ - chart_state.selected_stuff.bpm_events.insert( + args.chart_state.selected_stuff.bpm_events.insert( + {event.get_bpm(), event.get_beats()} + ); + } + ); + } + selection_rectangle.reset(); + started_selection_inside_window = false; + } + } +} + + +void LinearView::draw_in_waveform_mode(LinearView::draw_args_type& args) { + auto [ + draw_list, + chart_state, + waveform_cache, + timing, + current_beat, + last_editable_beat, + snap, + window_size, + origin + ] = args; + const auto computed_sizes = linear_view::compute_sizes(window_size, sizes); + + // Here we compute the range of visible beats from the size of the window + // in pixels, we know by definition that the current beat is exactly at + // cursor_y pixels and we use this fact to compute the rest + const auto beats_before_cursor = beats_to_pixels_proportional.backwards_transform(computed_sizes.cursor_y); + const auto beats_after_cursor = beats_to_pixels_proportional.backwards_transform(static_cast(computed_sizes.y) - computed_sizes.cursor_y); + const Fraction first_beat_in_frame = current_beat - beats_before_cursor; + const Fraction last_beat_in_frame = current_beat + beats_after_cursor; + AffineTransform beats_to_pixels_absolute{first_beat_in_frame, last_beat_in_frame, 0, computed_sizes.y}; + + const Fraction first_visible_beat = std::max(Fraction{0}, first_beat_in_frame); + auto next_beat = [](const auto& first_beat) -> Fraction { + if (first_beat % 1 == 0) { + return first_beat; + } else { + return floor_fraction(first_beat) + 1; + } + }(first_visible_beat); + + // Draw the beat lines and numbers + for ( + Fraction next_beat_line_y = beats_to_pixels_absolute.transform(next_beat); + next_beat_line_y < computed_sizes.y and next_beat < last_editable_beat; + next_beat_line_y = beats_to_pixels_absolute.transform(next_beat += 1) + ) { + const sf::Vector2f beat_line_start = {computed_sizes.timeline_left, static_cast(static_cast(next_beat_line_y))}; + const sf::Vector2f beat_line_end = beat_line_start + sf::Vector2f{computed_sizes.timeline_width, 0}; + if (next_beat % 4 == 0) { + draw_list->AddLine(beat_line_start + origin, beat_line_end + origin, ImColor(colors.measure_line)); + const Fraction measure = next_beat / 4; + const auto measure_string = fmt::format("{}", static_cast(measure)); + const sf::Vector2f text_size = ImGui::CalcTextSize(measure_string.c_str(), measure_string.c_str()+measure_string.size()); + const sf::Vector2f measure_text_pos = {computed_sizes.timeline_left - 10, static_cast(static_cast(next_beat_line_y))}; + draw_list->AddText( + origin + measure_text_pos - sf::Vector2f{text_size.x, text_size.y * 0.5f}, + ImColor(colors.measure_number), + measure_string.c_str(), + measure_string.c_str() + measure_string.size() + ); + } else { + draw_list->AddLine(beat_line_start + origin, beat_line_end + origin, ImColor(colors.beat_line)); + } + } + + // Draw the bpm changes + timing.for_each_event_between( + first_beat_in_frame, + last_beat_in_frame, + [&](const auto& event){ + const auto bpm_change_y = beats_to_pixels_absolute.transform(event.get_beats()); + if (bpm_change_y >= 0 and bpm_change_y <= computed_sizes.y) { + const sf::Vector2f bpm_text_raw_pos = {computed_sizes.bpm_events_left, static_cast(static_cast(bpm_change_y))}; + const auto bpm_at_beat = better::BPMAtBeat{event.get_bpm(), event.get_beats()}; + const auto selected = args.chart_state.selected_stuff.bpm_events.contains(bpm_at_beat); + if (BPMButton(event, selected, bpm_text_raw_pos, colors.bpm_button)) { + if (selected) { + args.chart_state.selected_stuff.bpm_events.erase(bpm_at_beat); + } else { + args.chart_state.selected_stuff.bpm_events.insert(bpm_at_beat); + } + } + } + } + ); + + // Draw the notes + auto draw_note = VariantVisitor { + [&](const better::TapNote& tap_note){ + const auto opt_lane = button_to_lane(tap_note.get_position()); + if (not opt_lane) { + return; + } + const auto lane = *opt_lane; + const float note_x = computed_sizes.timeline_left + computed_sizes.note_width * (lane + 0.5f); + const float note_y = static_cast(beats_to_pixels_absolute.transform(tap_note.get_time())); + const auto note_seconds = args.timing.time_at(tap_note.get_time()); + const auto first_colliding_beat = args.timing.beats_at(note_seconds - collision_zone * 0.5f); + const auto collision_zone_y = beats_to_pixels_absolute.transform(first_colliding_beat); + const auto last_colliding_beat = args.timing.beats_at(note_seconds + collision_zone * 0.5f); + const auto collision_zone_height = beats_to_pixels_proportional.transform(last_colliding_beat - first_colliding_beat); + const sf::Vector2f collision_zone_pos = { + note_x, + static_cast(static_cast(collision_zone_y)) + }; + const sf::Vector2f collizion_zone_size = { + computed_sizes.collizion_zone_width, + static_cast(static_cast(collision_zone_height)) + }; + const auto collision_zone_color = [&](){ + if (args.chart_state.chart.notes->is_colliding(tap_note, args.timing, collision_zone)) { + return colors.conflicting_collision_zone; + } else { + return colors.normal_collision_zone; + } + }(); + const auto tap_note_color = [&](){ + if (use_quantization_colors) { + return quantization_colors.color_at_beat(tap_note.get_time()); + } else if (args.chart_state.chart.notes->is_colliding(tap_note, args.timing, collision_zone)) { + return colors.conflicting_tap_note; + } else { + return colors.normal_tap_note; + } + }(); + draw_rectangle( + args.draw_list, + args.origin + collision_zone_pos, + collizion_zone_size, + {0.5f, 0.f}, + collision_zone_color + ); + const sf::Vector2f note_pos = {note_x, note_y}; + draw_rectangle( + args.draw_list, + args.origin + note_pos, + computed_sizes.note_size, + {0.5f, 0.5f}, + tap_note_color + ); + if (args.chart_state.selected_stuff.notes.contains(tap_note)) { + draw_rectangle( + args.draw_list, + args.origin + note_pos, + computed_sizes.selected_note_size, + {0.5f, 0.5f}, + colors.selected_note_fill, + colors.selected_note_outline + ); + } + }, + [&](const better::LongNote& long_note){ + const auto opt_lane = button_to_lane(long_note.get_position()); + if (not opt_lane) { + return; + } + const auto lane = *opt_lane; + float note_x = computed_sizes.timeline_left + computed_sizes.note_width * (lane + 0.5f); + float note_y = static_cast(beats_to_pixels_absolute.transform(long_note.get_time())); + const auto note_start_seconds = args.timing.time_at(long_note.get_time()); + const auto first_colliding_beat = args.timing.beats_at(note_start_seconds - collision_zone * 0.5f); + const auto collision_zone_y = beats_to_pixels_absolute.transform(first_colliding_beat); + const auto note_end_seconds = args.timing.time_at(long_note.get_end()); + const auto last_colliding_beat = args.timing.beats_at(note_end_seconds + collision_zone * 0.5f); + const auto collision_zone_height = beats_to_pixels_proportional.transform(last_colliding_beat - first_colliding_beat); + const sf::Vector2f collision_zone_pos = { + note_x, + static_cast(static_cast(collision_zone_y)) + }; + const sf::Vector2f collision_zone_size = { + computed_sizes.collizion_zone_width, + static_cast(static_cast(collision_zone_height)) + }; + auto collision_zone_color = colors.normal_collision_zone; + auto tap_note_color = [&](){ + if (use_quantization_colors) { + return quantization_colors.color_at_beat(long_note.get_time()); + } else { + return colors.normal_tap_note; + } + }(); + auto long_note_color = colors.normal_long_note; + if (args.chart_state.chart.notes->is_colliding(long_note, args.timing, collision_zone)) { + collision_zone_color = colors.conflicting_collision_zone; + if (not use_quantization_colors) { + tap_note_color = colors.conflicting_tap_note; + } + long_note_color = colors.conflicting_long_note; + } + draw_rectangle( + args.draw_list, + args.origin + collision_zone_pos, + collision_zone_size, + {0.5f, 0.f}, + collision_zone_color + ); + const auto long_note_rect_height = beats_to_pixels_proportional.transform(long_note.get_duration()); + const sf::Vector2f long_note_size = { + computed_sizes.long_note_rect_width, + static_cast(static_cast(long_note_rect_height)) + }; + const sf::Vector2f note_pos = {note_x, note_y}; + draw_rectangle( + args.draw_list, + args.origin + note_pos, + long_note_size, + {0.5f, 0.f}, + long_note_color + ); + draw_rectangle( + args.draw_list, + args.origin + note_pos, + computed_sizes.note_size, + {0.5f, 0.5f}, + tap_note_color + ); + if (args.chart_state.selected_stuff.notes.contains(long_note)) { + draw_rectangle( + args.draw_list, + args.origin + note_pos, + computed_sizes.selected_note_size, + {0.5f, 0.5f}, + colors.selected_note_fill, + colors.selected_note_outline + ); + } + }, + }; + + const auto first_visible_second = timing.time_at(first_beat_in_frame); + const auto first_visible_collision_zone = timing.beats_at(first_visible_second - sf::milliseconds(500)); + const auto last_visible_second = timing.time_at(last_beat_in_frame); + const auto last_visible_collision_zone = timing.beats_at(last_visible_second + sf::milliseconds(500)); + chart_state.chart.notes->in( + first_visible_collision_zone, + last_visible_collision_zone, + [&](const better::Notes::iterator& it){ + it->second.visit(draw_note); + } + ); + + if (chart_state.long_note_being_created.has_value()) { + draw_note( + make_long_note_dummy_for_linear_view( + *chart_state.long_note_being_created, + snap + ) + ); + } + + // Draw the cursor + draw_rectangle( + draw_list, + origin + computed_sizes.cursor_pos, + computed_sizes.cursor_size, + {0, 0.5}, + colors.cursor + ); + + // Draw the time selection + const float selection_width = computed_sizes.timeline_width; + if (chart_state.time_selection.has_value()) { + const auto pixel_interval = Interval{ + beats_to_pixels_absolute.transform(chart_state.time_selection->start), + beats_to_pixels_absolute.transform(chart_state.time_selection->end) + }; + if (pixel_interval.intersects({0, computed_sizes.y})) { + const sf::Vector2f selection_size = { + selection_width, + static_cast(static_cast(pixel_interval.width())) + }; + const sf::Vector2f selection_pos = { + computed_sizes.timeline_left, + static_cast(static_cast(pixel_interval.start)) + }; + draw_rectangle( + draw_list, + origin + selection_pos, + selection_size, + {0, 0}, + colors.tab_selection.fill, + colors.tab_selection.border + ); + } + } + + + const auto current_window = ImGui::GetCurrentWindow(); + + // Don't start the selection rect if we start : + // - outside the contents of the window + // - over anything + if ( + ImGui::IsMouseClicked(ImGuiMouseButton_Left) + and current_window->InnerClipRect.Contains(ImGui::GetMousePos()) + and not ImGui::IsAnyItemHovered() + and ImGui::IsWindowFocused() + ) { + started_selection_inside_window = true; + } + + if (started_selection_inside_window) { + if ( + draw_selection_rect( + draw_list, + selection_rectangle.start, + selection_rectangle.end, + colors.selection_rect + ) + ) { + chart_state.selected_stuff.clear(); + // Select everything inside the selection rectangle + const sf::Vector2f upper_left = { + std::min(selection_rectangle.start.x, selection_rectangle.end.x), + std::min(selection_rectangle.start.y, selection_rectangle.end.y), + }; + const sf::Vector2f lower_right = { + std::max(selection_rectangle.start.x, selection_rectangle.end.x), + std::max(selection_rectangle.start.y, selection_rectangle.end.y), + }; + const ImRect full_selection = {upper_left, lower_right}; + ImRect bpm_zone = {origin.x + computed_sizes.bpm_events_left, -INFINITY, INFINITY, INFINITY}; + bpm_zone.ClipWith(current_window->InnerRect); + if (full_selection.Overlaps(bpm_zone)) { + const auto first_selected_beat = beats_to_pixels_absolute.backwards_transform(full_selection.Min.y - origin.y); + const auto last_selected_beat = beats_to_pixels_absolute.backwards_transform(full_selection.Max.y - origin.y); + timing.for_each_event_between( + first_selected_beat, + last_selected_beat, + [&](const auto& event){ + args.chart_state.selected_stuff.bpm_events.insert( {event.get_bpm(), event.get_beats()} ); } diff --git a/src/widgets/linear_view.hpp b/src/widgets/linear_view.hpp index 80b8e6e..52c6fa0 100644 --- a/src/widgets/linear_view.hpp +++ b/src/widgets/linear_view.hpp @@ -20,6 +20,7 @@ #include "../utf8_sfml.hpp" #include "quantization_colors.hpp" #include "lane_order.hpp" +#include "waveform.hpp" struct SelectionRectangle { sf::Vector2f start = {-1, -1}; @@ -54,16 +55,19 @@ class LinearView { public: LinearView(std::filesystem::path assets, config::Config& config); - void draw( - ImDrawList* draw_list, - ChartState& chart_state, - const better::Timing& timing, - const Fraction& current_beat, - const Fraction& last_editable_beat, - const Fraction& snap, - const sf::Vector2f& size, - const sf::Vector2f& origin - ); + struct draw_args_type { + ImDrawList* draw_list; + ChartState& chart_state; + const waveform::Cache& waveform_cache; + const better::Timing& timing; + const Fraction& current_beat; + const Fraction& last_editable_beat; + const Fraction& snap; + const sf::Vector2f& size; + const sf::Vector2f& origin; + }; + + void draw(draw_args_type& args); void set_zoom(int zoom); void zoom_in() { set_zoom(zoom + 1); }; @@ -75,6 +79,8 @@ public: void display_settings(); private: + void draw_in_beats_mode(draw_args_type& args); + void draw_in_waveform_mode(draw_args_type& args); linear_view::Mode mode; std::string mode_name(); linear_view::Colors& colors; @@ -82,6 +88,7 @@ private: const sf::Time& collision_zone; AffineTransform beats_to_pixels_proportional; + AffineTransform seconds_to_pixels_proportional; void reload_transforms(); @@ -99,6 +106,32 @@ private: std::optional button_to_lane(const better::Position& button); }; +namespace linear_view { + struct ComputedSizes { + int x; + int y; + float timeline_width; + float timeline_left; + float timeline_right; + float cursor_y; + float bpm_events_left; + float note_width; + float collizion_zone_width; + float long_note_rect_width; + sf::Vector2f note_size; + sf::Vector2f selected_note_size; + float cursor_width; + float cursor_left; + sf::Vector2f cursor_size; + sf::Vector2f cursor_pos; + }; + + ComputedSizes compute_sizes( + const sf::Vector2f& window_size, + linear_view::Sizes& size_settings + ); +} + void draw_rectangle( ImDrawList* draw_list, const sf::Vector2f& pos,