/// <summary> /// Performs the NORMALIZE operation, as described in the comments to the general projection function. /// Clip begin and end times, normalize by beginTime, scale by speedRatio. /// </summary> /// <param name="projection">The normalized collection to create.</param> /// <param name="beginTime">Begin time of the active period for clipping.</param> /// <param name="endTime">End time of the active period for clipping.</param> /// <param name="speedRatio">The ratio by which to scale begin and end time.</param> /// <param name="includeFillPeriod">Whether a non-zero fill period exists.</param> private void ProjectionNormalize(ref TimeIntervalCollection projection, TimeSpan beginTime, Nullable<TimeSpan> endTime, bool includeFillPeriod, double speedRatio) { Debug.Assert(!IsEmptyOfRealPoints); Debug.Assert(projection.IsEmpty); projection.EnsureAllocatedCapacity(this._nodeTime.Length); this.MoveFirst(); projection.MoveFirst(); // Get to the non-clipped zone; we must overlap the active zone, so we should terminate at some point. while (!CurrentIsAtLastNode && NextNodeTime <= beginTime) { MoveNext(); } if (CurrentNodeTime < beginTime) // This means we have an interval clipped by beginTime { if (CurrentNodeIsInterval) { projection._count++; projection.CurrentNodeTime = TimeSpan.Zero; projection.CurrentNodeIsPoint = true; projection.CurrentNodeIsInterval = true; projection.MoveNext(); } this.MoveNext(); } while(_current < _count && (!endTime.HasValue || CurrentNodeTime < endTime)) // Copy the main set of segments, transforming them { double timeOffset = (double)((this.CurrentNodeTime - beginTime).Ticks); projection._count++; projection.CurrentNodeTime = TimeSpan.FromTicks((long)(speedRatio * timeOffset)); projection.CurrentNodeIsPoint = this.CurrentNodeIsPoint; projection.CurrentNodeIsInterval = this.CurrentNodeIsInterval; projection.MoveNext(); this.MoveNext(); } Debug.Assert(_current > 0); // The only way _current could stay at zero is if the collection begins at (or past) the end of active period if (_current < _count // We have an interval reaching beyond the active zone, clip that interval && (_nodeIsInterval[_current - 1] || (CurrentNodeTime == endTime.Value && CurrentNodeIsPoint && includeFillPeriod))) { Debug.Assert(endTime.HasValue && CurrentNodeTime >= endTime.Value); double timeOffset = (double)((endTime.Value - beginTime).Ticks); projection._count++; projection.CurrentNodeTime = TimeSpan.FromTicks((long)(speedRatio * timeOffset)); projection.CurrentNodeIsPoint = includeFillPeriod && (CurrentNodeTime > endTime.Value || CurrentNodeIsPoint); projection.CurrentNodeIsInterval = false; } }
/// <returns> /// Returns a collection which is the projection of this collection onto the defined periodic function. /// </returns> /// <remarks> /// The object on which this method is called is a timeline's parent's collection of intervals. /// The periodic collection passed via parameters describes the active/fill periods of the timeline. /// The output is the projection of (this) object using the parameter function of the timeline. /// /// We assume this function is ONLY called when this collection overlaps the active zone. /// /// The periodic function maps values from domain to range within its activation period of [beginTime, endTime); /// in the fill period [endTime, endTime+fillDuration) everything maps to a constant post-fill value, and outside of /// those periods every value maps to null. /// /// The projection process can be described as three major steps: /// /// (1) NORMALIZE this collection: offset the TIC's coordinates by BeginTime and scale by SpeedRatio. /// /// (2) FOLD this collection. This means we convert from parent-time coordinate space into the space of /// a single simpleDuration for the child. This is equivalent to "cutting up" the parent TIC into /// equal-length segments (of length Period) and overlapping them -- taking their union. This lets us /// know exactly which values inside the simpleDuration we have reached on the child. In the case of /// autoreversed timelines, we do the folding similiar to folding a strip of paper -- alternating direction. /// /// (3) WARP the resulting collection. We now convert from simpleDuration domain coordinates into /// coordinates in the range of the timeline function. We do this by applying the "warping" effects of /// acceleration, and deceleration. /// /// In the special case of infinite simple duration, we essentially are done after performing NORMALIZE, /// because no periodicity or acceleration is present. /// /// In the ultimate degenerate case of zero duration, we terminate early and project the zero point. /// /// </remarks> /// <param name="projection">An empty output projection, passed by reference to allow TIC reuse.</param> /// <param name="beginTime">Begin time of the periodic function.</param> /// <param name="endTime">The end (expiration) time of the periodic function. Null indicates positive infinity.</param> /// <param name="fillDuration">The fill time appended at the end of the periodic function. Zero indicates no fill period. Forever indicates infinite fill period.</param> /// <param name="period">Length of a single iteration in the periodic collection.</param> /// <param name="appliedSpeedRatio">Ratio by which to scale down the periodic collection.</param> /// <param name="accelRatio">Ratio of the length of the accelerating portion of the iteration.</param> /// <param name="decelRatio">Ratio of the length of the decelerating portion of the iteration.</param> /// <param name="isAutoReversed">Indicates whether reversed arcs should follow after forward arcs.</param> internal void ProjectOntoPeriodicFunction(ref TimeIntervalCollection projection, TimeSpan beginTime, Nullable<TimeSpan> endTime, Duration fillDuration, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { Debug.Assert(projection.IsEmpty); Debug.Assert(!_invertCollection); // Make sure we never leave inverted mode enabled Debug.Assert(!endTime.HasValue || beginTime <= endTime); // Ensure legitimate begin/end clipping parameters Debug.Assert(!IsEmptyOfRealPoints); // We assume this function is ONLY called when this collection overlaps the active zone. So we cannot be empty. Debug.Assert(!endTime.HasValue || endTime >= _nodeTime[0]); // EndTime must come at or after our first node (it can be infinite) Debug.Assert(_nodeTime[_count - 1] >= beginTime); // Our last node must come at least at begin time (since we must intersect the active period) Debug.Assert(endTime.HasValue || fillDuration == TimeSpan.Zero); // Either endTime is finite, or it's infinite hence we cannot have any fill zone Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || (endTime.HasValue && beginTime == endTime)); // Check the consistency of degenerate case where simple duration is zero; expiration time should equal beginTime Debug.Assert(!_nodeIsInterval[_count - 1]); // We should not have an infinite domain set // We initially project all intervals into a single period of the timeline, creating a union of the projected segments. // Then we warp the time coordinates of the resulting TIC from domain to range, applying the effects of speed/accel/decel bool nullPoint = _containsNullPoint // Start by projecting the null point directly, then check whether we fall anywhere outside of the active and fill period || _nodeTime[0] < beginTime // If we intersect space before beginTime, or... || (endTime.HasValue && fillDuration.HasTimeSpan // ...the active and fill periods don't stretch forever, and... && (_nodeTime[_count - 1] > endTime.Value + fillDuration.TimeSpan // ...we intersect space after endTime+fill, or... || (_nodeTime[_count - 1] == endTime.Value + fillDuration.TimeSpan // ...as we fall right onto the end of fill zone... && _nodeIsPoint[_count - 1] && (endTime > beginTime || fillDuration.TimeSpan > TimeSpan.Zero)))); // ...we may have a point intersection with the stopped zone // Now consider the main scenarios: if (endTime.HasValue && beginTime == endTime) // Degenerate case when our active period is a single point; project only the point { projection.InitializePoint(TimeSpan.Zero); } else // The case of non-zero active duration { bool includeFillPeriod = !fillDuration.HasTimeSpan || fillDuration.TimeSpan > TimeSpan.Zero; // This variable represents whether we have a non-zero fill zone if (period.HasTimeSpan) // We have a finite TimeSpan period and non-zero activation duration { TimeIntervalCollection tempCollection = new TimeIntervalCollection(); ProjectionNormalize(ref tempCollection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio); long periodInTicks = period.TimeSpan.Ticks; Nullable<TimeSpan> activeDuration; bool includeMaxPoint; if (endTime.HasValue) { activeDuration = endTime.Value - beginTime; includeMaxPoint = includeFillPeriod && (activeDuration.Value.Ticks % periodInTicks == 0); // Fill starts at a boundary } else { activeDuration = null; includeMaxPoint = false; } projection.EnsureAllocatedCapacity(_minimumCapacity); tempCollection.ProjectionFold(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint); if (accelRatio + decelRatio > 0) { projection.ProjectionWarp(periodInTicks, accelRatio, decelRatio); } } else // Infinite period degenerate case; we perform straight 1-1 linear mapping, offset by begin time and clipped { ProjectionNormalize(ref projection, beginTime, endTime, includeFillPeriod, appliedSpeedRatio); } } projection._containsNullPoint = nullPoint; // Ensure we have the null point properly set }