using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Storage.Entities.Profile; using Stylet; namespace Artemis.Core { /// /// Represents a timeline used by profile elements /// public class Timeline : PropertyChangedBase, IStorageModel { private const int MaxExtraTimelines = 15; /// /// Creates a new instance of the class /// public Timeline() { Entity = new TimelineEntity(); _extraTimelines = new List(); MainSegmentLength = TimeSpan.FromSeconds(5); Save(); } internal Timeline(TimelineEntity entity) { Entity = entity; _extraTimelines = new List(); Load(); } private Timeline(Timeline parent) { Parent = parent; StartSegmentLength = Parent.StartSegmentLength; MainSegmentLength = Parent.MainSegmentLength; EndSegmentLength = Parent.EndSegmentLength; _extraTimelines = new List(); } #region Extra timelines /// /// Adds an extra timeline to this timeline /// public void AddExtraTimeline() { _extraTimelines.Add(new Timeline(this)); if (_extraTimelines.Count > MaxExtraTimelines) _extraTimelines.RemoveAt(0); } /// /// Removes all extra timelines from this timeline /// public void ClearExtraTimelines() { _extraTimelines.Clear(); } #endregion #region Properties private TimeSpan _position; private TimeSpan _lastDelta; private TimeLineEventOverlapMode _eventOverlapMode; private TimelinePlayMode _playMode; private TimelineStopMode _stopMode; private readonly List _extraTimelines; private TimeSpan _startSegmentLength; private TimeSpan _mainSegmentLength; private TimeSpan _endSegmentLength; /// /// Gets the parent this timeline is an extra timeline of /// public Timeline? Parent { get; } /// /// Gets the current position of the timeline /// public TimeSpan Position { get => _position; private set => SetAndNotify(ref _position, value); } /// /// Gets the cumulative delta of all calls to that took place after the last call to /// /// Note: If this is an extra timeline is always equal to /// /// public TimeSpan Delta { get => Parent == null ? _lastDelta : DeltaToParent; private set => SetAndNotify(ref _lastDelta, value); } /// /// Gets the delta to this timeline's /// public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero; /// /// Gets or sets the mode in which the render element starts its timeline when display conditions are met /// public TimelinePlayMode PlayMode { get => _playMode; set => SetAndNotify(ref _playMode, value); } /// /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met /// public TimelineStopMode StopMode { get => _stopMode; set => SetAndNotify(ref _stopMode, value); } /// /// Gets or sets the mode in which the render element responds to display condition events being fired before the /// timeline finished /// public TimeLineEventOverlapMode EventOverlapMode { get => _eventOverlapMode; set => SetAndNotify(ref _eventOverlapMode, value); } /// /// Gets a list of extra copies of the timeline applied to this timeline /// public ReadOnlyCollection ExtraTimelines => _extraTimelines.AsReadOnly(); /// /// Gets a boolean indicating whether the timeline has finished its run /// public bool IsFinished => (Position > Length || Length == TimeSpan.Zero) && !ExtraTimelines.Any(); /// /// Gets a boolean indicating whether the timeline progress has been overridden /// public bool IsOverridden { get; private set; } #region Segments /// /// Gets the total length of this timeline /// public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength; /// /// Gets or sets the length of the start segment /// public TimeSpan StartSegmentLength { get => _startSegmentLength; set { if (SetAndNotify(ref _startSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Start, false); } } /// /// Gets or sets the length of the main segment /// public TimeSpan MainSegmentLength { get => _mainSegmentLength; set { if (SetAndNotify(ref _mainSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Main, false); } } /// /// Gets or sets the length of the end segment /// public TimeSpan EndSegmentLength { get => _endSegmentLength; set { if (SetAndNotify(ref _endSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.End, false); } } /// /// Gets or sets the start position of the main segment /// public TimeSpan MainSegmentStartPosition { get => StartSegmentEndPosition; set { StartSegmentEndPosition = value; NotifySegmentShiftAt(TimelineSegment.Main, true); } } /// /// Gets or sets the end position of the end segment /// public TimeSpan EndSegmentStartPosition { get => MainSegmentEndPosition; set { MainSegmentEndPosition = value; NotifySegmentShiftAt(TimelineSegment.End, true); } } /// /// Gets or sets the end position of the start segment /// public TimeSpan StartSegmentEndPosition { get => StartSegmentLength; set { StartSegmentLength = value; NotifySegmentShiftAt(TimelineSegment.Start, false); } } /// /// Gets or sets the end position of the main segment /// public TimeSpan MainSegmentEndPosition { get => StartSegmentEndPosition + MainSegmentLength; set { MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero; NotifySegmentShiftAt(TimelineSegment.Main, false); } } /// /// Gets or sets the end position of the end segment /// public TimeSpan EndSegmentEndPosition { get => MainSegmentEndPosition + EndSegmentLength; set { EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero; NotifySegmentShiftAt(TimelineSegment.End, false); } } internal TimelineEntity Entity { get; set; } /// /// Notifies the right segments in a way that I don't have to think about it /// /// The segment that was updated /// Whether the start point of the was updated private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated) { if (segment <= TimelineSegment.End) { if (startUpdated || segment < TimelineSegment.End) NotifyOfPropertyChange(nameof(EndSegmentStartPosition)); NotifyOfPropertyChange(nameof(EndSegmentEndPosition)); } if (segment <= TimelineSegment.Main) { if (startUpdated || segment < TimelineSegment.Main) NotifyOfPropertyChange(nameof(MainSegmentStartPosition)); NotifyOfPropertyChange(nameof(MainSegmentEndPosition)); } if (segment <= TimelineSegment.Start) NotifyOfPropertyChange(nameof(StartSegmentEndPosition)); NotifyOfPropertyChange(nameof(Length)); } #endregion #endregion #region Updating /// /// Updates the timeline, applying the provided to the /// /// The amount of time to apply to the position /// Whether to stick to the main segment, wrapping around if needed public void Update(TimeSpan delta, bool stickToMainSegment) { lock (this) { Delta += delta; Position += delta; IsOverridden = false; if (stickToMainSegment && Position >= MainSegmentStartPosition) { // If the main segment has no length, simply stick to the start of the segment if (MainSegmentLength == TimeSpan.Zero) Position = MainSegmentStartPosition; // Ensure wrapping back around retains the delta time else Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); } _extraTimelines.RemoveAll(t => t.IsFinished); foreach (Timeline extraTimeline in _extraTimelines) extraTimeline.Update(delta, false); } } /// /// Moves the position of the timeline backwards to the very start of the timeline /// public void JumpToStart() { lock (this) { if (Position == TimeSpan.Zero) return; Delta = TimeSpan.Zero - Position; Position = TimeSpan.Zero; } } /// /// Moves the position of the timeline forwards to the beginning of the end segment /// public void JumpToEndSegment() { lock (this) { if (Position >= EndSegmentStartPosition) return; Delta = EndSegmentStartPosition - Position; Position = EndSegmentStartPosition; } } /// /// Moves the position of the timeline forwards to the very end of the timeline /// public void JumpToEnd() { lock (this) { if (Position >= EndSegmentEndPosition) return; Delta = EndSegmentEndPosition - Position; Position = EndSegmentEndPosition; } } /// /// Overrides the to the specified time and clears any extra time lines /// /// The position to set the timeline to /// Whether to stick to the main segment, wrapping around if needed public void Override(TimeSpan position, bool stickToMainSegment) { lock (this) { Delta += position - Position; Position = position; IsOverridden = true; if (stickToMainSegment && Position >= MainSegmentStartPosition) Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); _extraTimelines.Clear(); } } /// /// Sets the to /// public void ClearDelta() { lock (this) { Delta = TimeSpan.Zero; } } #endregion #region Storage /// public void Load() { StartSegmentLength = Entity.StartSegmentLength; MainSegmentLength = Entity.MainSegmentLength; EndSegmentLength = Entity.EndSegmentLength; PlayMode = (TimelinePlayMode) Entity.PlayMode; StopMode = (TimelineStopMode) Entity.StopMode; EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode; } /// public void Save() { Entity.StartSegmentLength = StartSegmentLength; Entity.MainSegmentLength = MainSegmentLength; Entity.EndSegmentLength = EndSegmentLength; Entity.PlayMode = (int) PlayMode; Entity.StopMode = (int) StopMode; Entity.EventOverlapMode = (int) EventOverlapMode; } #endregion public override string ToString() { return $"Progress: {Position}/{Length} - delta: {Delta}"; } } internal enum TimelineSegment { Start, Main, End } /// /// Represents a mode for render elements to start their timeline when display conditions are met /// public enum TimelinePlayMode { /// /// Continue repeating the main segment of the timeline while the condition is met /// Repeat, /// /// Only play the timeline once when the condition is met /// Once } /// /// Represents a mode for render elements to stop their timeline when display conditions are no longer met /// public enum TimelineStopMode { /// /// When conditions are no longer met, finish the the current run of the main timeline /// Finish, /// /// When conditions are no longer met, skip to the end segment of the timeline /// SkipToEnd } /// /// Represents a mode for render elements to start their timeline when display conditions events are fired /// public enum TimeLineEventOverlapMode { /// /// Stop the current run and restart the timeline /// Restart, /// /// Ignore subsequent event fires until the timeline finishes /// Ignore, /// /// Play another copy of the timeline on top of the current run /// Copy } }