<commit_before>﻿// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;

namespace osu.Game.Screens.Edit
{
    /// <summary>
    /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
    /// </summary>
    public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
    {
        public IBindable<Track> Track => track;

        private readonly Bindable<Track> track = new Bindable<Track>();

        public double TrackLength => track.Value?.Length ?? 60000;

        public ControlPointInfo ControlPointInfo;

        private readonly BindableBeatDivisor beatDivisor;

        private readonly DecoupleableInterpolatingFramedClock underlyingClock;

        private bool playbackFinished;

        public IBindable<bool> SeekingOrStopped => seekingOrStopped;

        private readonly Bindable<bool> seekingOrStopped = new Bindable<bool>(true);

        /// <summary>
        /// Whether a seek is currently in progress. True for the duration of a seek performed via <see cref="SeekSmoothlyTo"/>.
        /// </summary>
        public bool IsSeeking { get; private set; }

        public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
            : this(beatmap.ControlPointInfo, beatDivisor)
        {
        }

        public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
        {
            this.beatDivisor = beatDivisor;

            ControlPointInfo = controlPointInfo;

            underlyingClock = new DecoupleableInterpolatingFramedClock();
        }

        public EditorClock()
            : this(new ControlPointInfo(), new BindableBeatDivisor())
        {
        }

        /// <summary>
        /// Seek to the closest snappable beat from a time.
        /// </summary>
        /// <param name="position">The raw position which should be seeked around.</param>
        /// <returns>Whether the seek could be performed.</returns>
        public bool SeekSnapped(double position)
        {
            var timingPoint = ControlPointInfo.TimingPointAt(position);
            double beatSnapLength = timingPoint.BeatLength / beatDivisor.Value;

            // We will be snapping to beats within the timing point
            position -= timingPoint.Time;

            // Determine the index from the current timing point of the closest beat to position
            int closestBeat = (int)Math.Round(position / beatSnapLength);
            position = timingPoint.Time + closestBeat * beatSnapLength;

            // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
            // the next timing point's start time
            var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
            if (position > nextTimingPoint?.Time)
                position = nextTimingPoint.Time;

            return Seek(position);
        }

        /// <summary>
        /// Seeks backwards by one beat length.
        /// </summary>
        /// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
        /// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
        public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount + (IsRunning ? 1.5 : 0));

        /// <summary>
        /// Seeks forwards by one beat length.
        /// </summary>
        /// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
        /// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
        public void SeekForward(bool snapped = false, double amount = 1) => seek(1, snapped, amount);

        private void seek(int direction, bool snapped, double amount = 1)
        {
            double current = CurrentTimeAccurate;

            if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount));

            var timingPoint = ControlPointInfo.TimingPointAt(current);

            if (direction < 0 && timingPoint.Time == current)
                // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
                timingPoint = ControlPointInfo.TimingPointAt(current - 1);

            double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount;
            double seekTime = current + seekAmount * direction;

            if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
            {
                SeekSmoothlyTo(seekTime);<commit_msg>Clamping seekTime to beatmap length<commit_after>﻿// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;

namespace osu.Game.Screens.Edit
{
    /// <summary>
    /// A decoupled clock which adds editor-specific functionality, such as snapping to a user-defined beat divisor.
    /// </summary>
    public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
    {
        public IBindable<Track> Track => track;

        private readonly Bindable<Track> track = new Bindable<Track>();

        public double TrackLength => track.Value?.Length ?? 60000;

        public ControlPointInfo ControlPointInfo;

        private readonly BindableBeatDivisor beatDivisor;

        private readonly DecoupleableInterpolatingFramedClock underlyingClock;

        private bool playbackFinished;

        public IBindable<bool> SeekingOrStopped => seekingOrStopped;

        private readonly Bindable<bool> seekingOrStopped = new Bindable<bool>(true);

        /// <summary>
        /// Whether a seek is currently in progress. True for the duration of a seek performed via <see cref="SeekSmoothlyTo"/>.
        /// </summary>
        public bool IsSeeking { get; private set; }

        public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
            : this(beatmap.ControlPointInfo, beatDivisor)
        {
        }

        public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor)
        {
            this.beatDivisor = beatDivisor;

            ControlPointInfo = controlPointInfo;

            underlyingClock = new DecoupleableInterpolatingFramedClock();
        }

        public EditorClock()
            : this(new ControlPointInfo(), new BindableBeatDivisor())
        {
        }

        /// <summary>
        /// Seek to the closest snappable beat from a time.
        /// </summary>
        /// <param name="position">The raw position which should be seeked around.</param>
        /// <returns>Whether the seek could be performed.</returns>
        public bool SeekSnapped(double position)
        {
            var timingPoint = ControlPointInfo.TimingPointAt(position);
            double beatSnapLength = timingPoint.BeatLength / beatDivisor.Value;

            // We will be snapping to beats within the timing point
            position -= timingPoint.Time;

            // Determine the index from the current timing point of the closest beat to position
            int closestBeat = (int)Math.Round(position / beatSnapLength);
            position = timingPoint.Time + closestBeat * beatSnapLength;

            // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
            // the next timing point's start time
            var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
            if (position > nextTimingPoint?.Time)
                position = nextTimingPoint.Time;

            return Seek(position);
        }

        /// <summary>
        /// Seeks backwards by one beat length.
        /// </summary>
        /// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
        /// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
        public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount + (IsRunning ? 1.5 : 0));

        /// <summary>
        /// Seeks forwards by one beat length.
        /// </summary>
        /// <param name="snapped">Whether to snap to the closest beat after seeking.</param>
        /// <param name="amount">The relative amount (magnitude) which should be seeked.</param>
        public void SeekForward(bool snapped = false, double amount = 1) => seek(1, snapped, amount);

        private void seek(int direction, bool snapped, double amount = 1)
        {
            double current = CurrentTimeAccurate;

            if (amount <= 0) throw new ArgumentException("Value should be greater than zero", nameof(amount));

            var timingPoint = ControlPointInfo.TimingPointAt(current);

            if (direction < 0 && timingPoint.Time == current)
                // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
                timingPoint = ControlPointInfo.TimingPointAt(current - 1);

            double seekAmount = timingPoint.BeatLength / beatDivisor.Value * amount;
            double seekTime = current + seekAmount * direction;

            if (!snapped || ControlPointInfo.TimingPoints.Count == 0)
            {
                seekTime = Math.Clamp(seekTime, 0, TrackLength);
                SeekSmoothlyTo(seekTime);
{'fraction_non_alphanumeric': 0.08915521777258112, 'fraction_numerical': 0.0034103088765468186, 'mean_word_length': 2.5259361044314668, 'pattern_counts': {'":': 0, '<': 69, '<?xml version=': 0, '>': 81, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2747480', 'n_tokens_mistral': 2743, 'n_tokens_neox': 2571, 'n_words': 859}