// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Timing; using osu.Framework.Utils; namespace osu.Framework.Graphics.Transforms { /// /// A type of object which can have s operating upon it. /// An implementer of this class must call to /// update and apply its s. /// public abstract class Transformable : ITransformable { /// /// The clock that is used to provide the timing for this object's s. /// public abstract IFrameBasedClock Clock { get; set; } /// /// The current frame's time as observed by this class's . /// public FrameTimeInfo Time => Clock.TimeInfo; /// /// The starting time to use for new s. /// public double TransformStartTime => (Clock?.CurrentTime ?? 0) + TransformDelay; /// /// Delay from the current time until new s are started, in milliseconds. /// protected double TransformDelay { get; private set; } /// /// A lazily-initialized list of s applied to this object. /// public IEnumerable Transforms => targetGroupingTrackers.SelectMany(t => t.Transforms); /// /// Retrieves the s for a given target member. /// /// The target member to find the s for. /// An enumeration over the transforms for the target member. public IEnumerable TransformsForTargetMember(string targetMember) => getTrackerFor(targetMember)?.Transforms ?? Enumerable.Empty(); /// /// The end time in milliseconds of the latest transform enqueued for this . /// Will return the current time value if no transforms are present. /// public double LatestTransformEndTime { get { //expiry should happen either at the end of the last transform or using the current sequence delay (whichever is highest). double max = TransformStartTime; foreach (var tracker in targetGroupingTrackers) { for (int i = 0; i < tracker.Transforms.Count; i++) { var t = tracker.Transforms[i]; if (t.EndTime > max) max = t.EndTime + 1; //adding 1ms here ensures we can expire on the current frame without issue. } } return max; } } /// /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms. /// public virtual bool RemoveCompletedTransforms { get; internal set; } = true; /// /// Resets and processes updates to this class based on loaded s. /// protected void UpdateTransforms() { TransformDelay = 0; updateTransforms(Time.Current); } private double lastUpdateTransformsTime; private readonly List targetGroupingTrackers = new List(); private TargetGroupingTransformTracker getTrackerFor(string targetMember) { foreach (var t in targetGroupingTrackers) { if (t.TargetMembers.Contains(targetMember)) return t; } return null; } private TargetGroupingTransformTracker getTrackerForGrouping(string targetGrouping, bool createIfNotExisting) { foreach (var t in targetGroupingTrackers) { if (t.TargetGrouping == targetGrouping) return t; } if (!createIfNotExisting) return null; var tracker = new TargetGroupingTransformTracker(this, targetGrouping); targetGroupingTrackers.Add(tracker); return tracker; } /// /// Process updates to this class based on loaded s. This does not reset . /// This is used for performing extra updates on s when new s are added. /// /// The point in time to update transforms to. /// Whether prior transforms should be reprocessed even if a rewind was not detected. private void updateTransforms(double time, bool forceRewindReprocess = false) { bool rewinding = lastUpdateTransformsTime > time || forceRewindReprocess; lastUpdateTransformsTime = time; // collection may grow due to abort / completion events. for (var i = 0; i < targetGroupingTrackers.Count; i++) targetGroupingTrackers[i].UpdateTransforms(time, rewinding); } /// /// Removes a . /// /// The to remove. public void RemoveTransform(Transform toRemove) { EnsureTransformMutationAllowed(); getTrackerForGrouping(toRemove.TargetGrouping, false)?.RemoveTransform(toRemove); toRemove.TriggerAbort(); } /// /// Clears s. /// /// Whether we also clear the s of children. /// /// An optional name of s to clear. /// Null for clearing all s. /// public virtual void ClearTransforms(bool propagateChildren = false, string targetMember = null) { EnsureTransformMutationAllowed(); ClearTransformsAfter(double.NegativeInfinity, propagateChildren, targetMember); } /// /// Removes s that start after . /// /// The time to clear s after. /// Whether to also clear such s of children. /// /// An optional name of s to clear. /// Null for clearing all s. /// public virtual void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { EnsureTransformMutationAllowed(); if (targetMember != null) { getTrackerFor(targetMember)?.ClearTransformsAfter(time, targetMember); } else { // collection may grow due to abort / completion events. for (var i = 0; i < targetGroupingTrackers.Count; i++) targetGroupingTrackers[i].ClearTransformsAfter(time); } } /// /// Applies s at a point in time. This may only be called if is set to false. /// /// This does not change the clock time. /// /// /// The time to apply s at. /// Whether to also apply children's s at . public virtual void ApplyTransformsAt(double time, bool propagateChildren = false) { EnsureTransformMutationAllowed(); if (RemoveCompletedTransforms) throw new InvalidOperationException($"Cannot arbitrarily apply transforms with {nameof(RemoveCompletedTransforms)} active."); updateTransforms(time); } /// /// Finishes specified s, using their . /// /// Whether we also finish the s of children. /// /// An optional name of s to finish. /// Null for finishing all s. /// public virtual void FinishTransforms(bool propagateChildren = false, string targetMember = null) { EnsureTransformMutationAllowed(); if (targetMember != null) { getTrackerFor(targetMember)?.FinishTransforms(targetMember); } else { // collection may grow due to abort / completion events. for (var i = 0; i < targetGroupingTrackers.Count; i++) targetGroupingTrackers[i].FinishTransforms(); } } /// /// Add a delay duration to , in milliseconds. /// /// The delay duration to add. /// Whether we also delay down the child tree. /// This internal virtual void AddDelay(double duration, bool propagateChildren = false) => TransformDelay += duration; /// /// Start a sequence of s with a (cumulative) relative delay applied. /// /// The offset in milliseconds from current time. Note that this stacks with other nested sequences. /// Whether this should be applied to all children. True by default. /// An to be used in a using() statement. public IDisposable BeginDelayedSequence(double delay, bool recursive = true) { EnsureTransformMutationAllowed(); if (delay == 0) return null; AddDelay(delay, recursive); double newTransformDelay = TransformDelay; return new ValueInvokeOnDisposal(new DelayedSequenceSender(this, delay, recursive, newTransformDelay), sender => { if (!Precision.AlmostEquals(sender.NewTransformDelay, sender.Transformable.TransformDelay)) { throw new InvalidOperationException( $"{nameof(sender.Transformable.TransformStartTime)} at the end of delayed sequence is not the same as at the beginning, but should be. " + $"(begin={sender.NewTransformDelay} end={sender.Transformable.TransformDelay})"); } AddDelay(-sender.Delay, sender.Recursive); }); } /// An ad-hoc struct used as a closure environment in . private readonly struct DelayedSequenceSender { public readonly Transformable Transformable; public readonly double Delay; public readonly bool Recursive; public readonly double NewTransformDelay; public DelayedSequenceSender(Transformable transformable, double delay, bool recursive, double newTransformDelay) { Transformable = transformable; Delay = delay; Recursive = recursive; NewTransformDelay = newTransformDelay; } } /// /// Start a sequence of s from an absolute time value (adjusts ). /// /// The new value for . /// Whether this should be applied to all children. True by default. /// An to be used in a using() statement. /// Absolute sequences should never be nested inside another existing sequence. public virtual IDisposable BeginAbsoluteSequence(double newTransformStartTime, bool recursive = true) { EnsureTransformMutationAllowed(); return createAbsoluteSequenceAction(newTransformStartTime); } internal virtual void CollectAbsoluteSequenceActionsFromSubTree(double newTransformStartTime, List actions) { actions.Add(createAbsoluteSequenceAction(newTransformStartTime)); } private AbsoluteSequenceSender createAbsoluteSequenceAction(double newTransformStartTime) { double oldTransformDelay = TransformDelay; double newTransformDelay = TransformDelay = newTransformStartTime - (Clock?.CurrentTime ?? 0); return new AbsoluteSequenceSender(this, oldTransformDelay, newTransformDelay); } /// An ad-hoc struct used as a closure environment in . internal readonly struct AbsoluteSequenceSender : IDisposable { public readonly Transformable Sender; public readonly double OldTransformDelay; public readonly double NewTransformDelay; public AbsoluteSequenceSender(Transformable sender, double oldTransformDelay, double newTransformDelay) { OldTransformDelay = oldTransformDelay; NewTransformDelay = newTransformDelay; Sender = sender; } public void Dispose() { if (!Precision.AlmostEquals(NewTransformDelay, Sender.TransformDelay)) { throw new InvalidOperationException( $"{nameof(Sender.TransformStartTime)} at the end of absolute sequence is not the same as at the beginning, but should be. " + $"(begin={NewTransformDelay} end={Sender.TransformDelay})"); } Sender.TransformDelay = OldTransformDelay; } } /// /// Adds to this object a which was previously populated using this object via /// . /// Added s are immediately applied, and therefore have an immediate effect on this object if the current time of this /// object falls within and . /// If is null, e.g. because this object has just been constructed, then the given transform will be finished instantaneously. /// /// The to be added. /// When not null, the to assign for ordering. public void AddTransform(Transform transform, ulong? customTransformID = null) { EnsureTransformMutationAllowed(); if (transform == null) throw new ArgumentNullException(nameof(transform)); if (!ReferenceEquals(transform.TargetTransformable, this)) { throw new InvalidOperationException( $"{nameof(transform)} must have been populated via {nameof(TransformableExtensions)}.{nameof(TransformableExtensions.PopulateTransform)} " + "using this object prior to being added."); } if (Clock == null) { if (!transform.HasStartValue) { transform.ReadIntoStartValue(); transform.HasStartValue = true; } transform.Apply(transform.EndTime); transform.TriggerComplete(); return; } getTrackerForGrouping(transform.TargetGrouping, true).AddTransform(transform, customTransformID); // If our newly added transform could have an immediate effect, then let's // make this effect happen immediately. // This is done globally instead of locally in the single member tracker // to keep the transformable's state consistent (e.g. with lastUpdateTransformsTime) if (transform.StartTime < Time.Current || transform.EndTime <= Time.Current) updateTransforms(Time.Current, !RemoveCompletedTransforms && transform.StartTime <= Time.Current); } /// /// Check whether the current thread is valid for operating on thread-safe properties. /// Will throw on failure. /// internal abstract void EnsureTransformMutationAllowed(); } }