/// <returns> /// Returns whether this collection has a non-empty intersection with the inverse of the other collection /// </returns> internal bool IntersectsInverseOf(TimeIntervalCollection other) { Debug.Assert(!_invertCollection); // Make sure we never leave inverted mode enabled if (this.ContainsNullPoint && !other.ContainsNullPoint) // Intersection at null points { return true; } if (this.IsEmptyOfRealPoints) // We are empty, and have no null point; we have nothing to intersect { return false; } else if (other.IsEmptyOfRealPoints || // We are non-empty, and other is the inverse of empty (e.g. covers all real numbers, so we must intersect), OR... this._nodeTime[0] < other._nodeTime[0]) // Neither TIC is empty, and we start first; this means the inverted "other" by necessity // overlaps our first node, so it must intersect either our node or subsequent interval. { return true; } else // Neither TIC is empty, and other starts no later than we do; then use regular intersection logic with inverted boolean flags { other.SetInvertedMode(true); bool returnValue = IntersectsHelper(other); other.SetInvertedMode(false); // Make sure we don't leave other TIC in an inverted state! return returnValue; } }
// Returns true if we know at this point whether an intersection is possible between tic1 and tic2 // The fact of whether an intersection was found is stored in the ref parameter intersectionFound static private bool IntersectsHelperUnequalCase(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2, ref bool intersectionFound) { Debug.Assert(!intersectionFound); // If an intersection was already found, we should not reach this far if (tic1.CurrentNodeIsInterval) // If we are within an interval in tic1, we immediately have an intersection { // If we have gotten into this method, tic1._current comes earlier than does tic2._current; // Suppose the following assert is false; then by Rule #2A, tic2's previous interval must be included; // If this was the case, then tic2's previous interval overlapped tic1's current interval. Since it's // included, we would have encountered an intersection before even reaching this method! Then you // should not even be here now. Else suppose we are at tic2's first node, then the below Assert // follows directly from Rule #3. Debug.Assert(tic2.CurrentNodeIsPoint || tic2.CurrentNodeIsInterval); intersectionFound = true; return true; } else if (tic1.CurrentIsAtLastNode) // // If we are already at the end of tic1, we ran out of nodes that may have an intersection { intersectionFound = false; return true; } else // Else we are inside a non-included interval in tic1, no intersection is possible, but keep advancing tic2._current { while (!tic2.CurrentIsAtLastNode && (tic2.NextNodeTime <= tic1.NextNodeTime)) { tic2.MoveNext(); } // If nextNodeTime1 is null, we should never get here because the IF statement would have caught it and quit Debug.Assert(!tic1.CurrentIsAtLastNode); // Thus tic1._current can be safely advanced now // Now tic1._current can be safely advanced forward tic1.MoveNext(); // If we broke out of Case I, its conditional should no longer hold true: Debug.Assert(tic1.CurrentNodeTime >= tic2.CurrentNodeTime); // Enforce our invariant: neither index gets too far ahead of the other. Debug.Assert(tic2.CurrentIsAtLastNode || (tic1.CurrentNodeTime < tic2.NextNodeTime)); Debug.Assert(tic1.CurrentIsAtLastNode || (tic2.CurrentNodeTime < tic1.NextNodeTime)); // Tell the main algorithm to continue working return false; } }
// Returns true if we know at this point whether an intersection is possible between tic1 and tic2 // The fact of whether an intersection was found is stored in the ref parameter intersectionFound static private bool IntersectsHelperEqualCase(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2, ref bool intersectionFound) { // If the nodes match exactly, check if the points are both included, or if the intervals are both included if ((tic1.CurrentNodeIsPoint && tic2.CurrentNodeIsPoint) || (tic1.CurrentNodeIsInterval && tic2.CurrentNodeIsInterval)) { intersectionFound = true; return true; } // We did not find an intersection, but advance whichever index has a closer next node else if (!tic1.CurrentIsAtLastNode && ( tic2.CurrentIsAtLastNode || (tic1.NextNodeTime < tic2.NextNodeTime))) { tic1.MoveNext(); } else if (!tic2.CurrentIsAtLastNode && ( tic1.CurrentIsAtLastNode || (tic2.NextNodeTime < tic1.NextNodeTime))) { tic2.MoveNext(); } else if (!tic1.CurrentIsAtLastNode && !tic2.CurrentIsAtLastNode) { // If both indices have room to advance, and we haven't yet advanced either one, it must be the next nodes are also exactly equal Debug.Assert(tic1.NextNodeTime == tic2.NextNodeTime); // It is necessary to advance both indices simultaneously, otherwise we break our invariant - one will be too far ahead tic1.MoveNext(); tic2.MoveNext(); } else // The only way we could get here is if both indices are pointing to the last nodes { Debug.Assert(tic1.CurrentIsAtLastNode && tic2.CurrentIsAtLastNode); // We have exhausted all the nodes and not found an intersection; bail intersectionFound = false; return true; } // Enforce our invariant: neither index gets too far ahead of the other. Debug.Assert(tic2.CurrentIsAtLastNode || (tic1.CurrentNodeTime < tic2.NextNodeTime)); Debug.Assert(tic1.CurrentIsAtLastNode || (tic2.CurrentNodeTime < tic1.NextNodeTime)); // Tell the main algorithm to continue working return false; }
// This method was made separate to detect intersections with inverses when needed private bool IntersectsHelper(TimeIntervalCollection other) { // Make sure the indexers are starting next to each other IntersectsHelperPrepareIndexers(ref this, ref other); // The outer loop does not bail, rather we return directly from inside the loop bool intersectionFound = false; while (true) { // The inner loops iterate through the subset of a TIC // CASE I. // In this case, index1 is the dominant indexer: index2 is on its turf and we keep advancing index2 and checking for intesections // After this helper, index2 will no longer be ahead of index1 if ((this.CurrentNodeTime < other.CurrentNodeTime) && IntersectsHelperUnequalCase(ref this, ref other, ref intersectionFound)) { return intersectionFound; } // CASE II. // In this case, index2 is the dominant indexer: index1 is on its turf and we keep advancing index1 and checking for intesections // After this helper, index1 will no longer be ahead of index2 if ((this.CurrentNodeTime > other.CurrentNodeTime) && IntersectsHelperUnequalCase(ref other, ref this, ref intersectionFound)) { return intersectionFound; } // CASE III. // In this case, neither indexer is dominant: they are pointing to the same point in time // We keep doing this until the indices are no longer equal while (this.CurrentNodeTime == other.CurrentNodeTime) { if (IntersectsHelperEqualCase(ref this, ref other, ref intersectionFound)) { return intersectionFound; } } } }
// Make sure the indexers are starting next to each other static private void IntersectsHelperPrepareIndexers(ref TimeIntervalCollection tic1, ref TimeIntervalCollection tic2) { Debug.Assert(!tic1.IsEmptyOfRealPoints); // We shouldn't reach here if either TIC is empty Debug.Assert(!tic2.IsEmptyOfRealPoints); tic1.MoveFirst(); // Point _current to the first node in both TICs tic2.MoveFirst(); // First bring tic1._current and tic2._current within an interval of each other if (tic1.CurrentNodeTime < tic2.CurrentNodeTime) { // Keep advancing tic1._current as far as possible while keeping _nodeTime[tic1._current] < _nodeTime[tic2._current] while (!tic1.CurrentIsAtLastNode && (tic1.NextNodeTime <= tic2.CurrentNodeTime)) { tic1.MoveNext(); } } else if (tic2.CurrentNodeTime < tic1.CurrentNodeTime) { // Keep advancing tic2._current as far as possible while keeping _nodeTime[tic1._current] > _nodeTime[tic2._current] while (!tic2.CurrentIsAtLastNode && (tic2.NextNodeTime <= tic1.CurrentNodeTime)) { tic2.MoveNext(); } } }
// Used for optimizing slip computation in Clock // This method will discard nodes beyond the first two nodes. // The only scenario where this method is called on a larger-than-size-2 TIC is // when the parent of a Media wraps around in a Repeat. Then we only enter // the Media's active period on the wraparound part of the TIC, so it is the only important // part to leave. // Example: the parent has Duration=10 and RepeatBehavior=Forever. It went from 9ms to 2ms (wraparound). // Our default TIC is {[0, 2], (9, 10)}. Slipping this by 1 will change it to {[1, 2]}. It is apparent // that this is the only part of the parent that actually overlaps our active zone. internal TimeIntervalCollection SlipBeginningOfConnectedInterval(TimeSpan slipTime) { if (slipTime == TimeSpan.Zero) // The no-op case { return this; } TimeIntervalCollection slippedCollection; if (_count < 2 || slipTime > _nodeTime[1] - _nodeTime[0]) { // slipTime > the connected duration, which basically eliminates the parent TIC interval for us; // This would only happen when media "outruns" the parent container, producing negative slip. slippedCollection = TimeIntervalCollection.Empty; } else { // Just shift the first node by slipAmount; the constructor handles the a==b case. slippedCollection = new TimeIntervalCollection(_nodeTime[0] + slipTime, _nodeIsPoint[0], _nodeTime[1] , _nodeIsPoint[1]); } if (this.ContainsNullPoint) { slippedCollection.AddNullPoint(); } return slippedCollection; }
/// <returns> /// Returns whether this collection has a non-empty intersection with the other collection /// </returns> // RUNNING TIME: O(_count) worst-case // IMPLEMENTATION FOR INTERSECTS(OTHER) OPERATION: // // We implement intersection by "stacking" the two TICs atop each other and seeing if // there is any point or interval common to both. We do this by having two indexers, // index1 and index2, traverse the lengths of both TICs simultaneously. We maintain // the following invariant: each indexer, when "projected" onto the other TIC than the one // it actually indexes into, falls less than a node ahead of the other indexer. // To rephrase intuitively, the indexers never fall out of step by having one get // too far ahead of the other. // // Example: // // this ----[0]----[1]--------------------[2]----[3]-----------[4]---------[5]------... // other --------------------[0]----[1]------------------[2]----------------[3]------... // ^index1 // ^index2 // // Our invariant means that one of the indexed nodes either coincides exactly with // the other, as is the case for nodes this[4] and other[2] in the above example, // or "projects" into the other node's subsequent interval; in the above example, // other[index2] projects onto the interval of this[index1]. // // At each iteration, we check for an intersection at: // A) the latter of the indexed nodes, and // B) the interval right after the latter indexed node // // 3 possible scenarios: // CASE I. index1 < index2 intersects if _nodeIsInterval[index1] && (_nodeIsPoint[index2] || _nodeIsInterval[index2]) // CASE II. index1 > index2 intersects if _nodeIsInterval[index2] && (_nodeIsPoint[index1] || _nodeIsInterval[index1]) // CASE III. index1 = index2 intersects if (_nodeIsPoint[index1] && _nodeIsPoint[index2]) || (_nodeIsInterval[index1] && _nodeIsInterval[index2]) // // We say that in Case I, index1 is dominant in the sense that index2 points to a node on index1's "turf"; // We move index2 through index1's entire interval to check for intersections against it. Once index2 passes // index1's interval, we advance index1 as well. Then we again check which scenario we end up in. // // Case II is treated anti-symmetrically to Case I. // // Case III is special, because we cannot treat it the same as Case I or II. This is becasue we have to check // for a point-point intersection, and check which indexer should be advanced next. It is possible that both // indexers need to be advanced if the next 2 nodes are also equal. // // We continue advancing the pointers until we find an intersection or run out of nodes on either of the TICs. // internal bool Intersects(TimeIntervalCollection other) { Debug.Assert(!_invertCollection); // Make sure we never leave inverted mode enabled if (this.ContainsNullPoint && other.ContainsNullPoint) // Short-circuit null point intersections { return true; } else if (this.IsEmptyOfRealPoints || other.IsEmptyOfRealPoints) // Only intersection with an empty TIC is at null points, which case is already handled { return false; } else // Both TICs are non-empty and don't intersect at the null point { return IntersectsHelper(other); } }
/// <summary> /// Take a single projection point and insert into the output collection. /// NOTE: projection should have allocated arrays. /// </summary> /// <param name="projection">The output collection.</param> /// <param name="activeDuration">The duration of the active period.</param> /// <param name="periodInTicks">The length of a simple duration in ticks.</param> /// <param name="isAutoReversed">Whether autoreversing is enabled</param> /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param> private void ProjectionFoldPoint(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration, long periodInTicks, bool isAutoReversed, bool includeMaxPoint) { Debug.Assert(CurrentNodeIsPoint); // We should only call this method when we project a legitimate point Debug.Assert(!CurrentNodeIsInterval); long currentProjection; if (isAutoReversed) // Take autoreversing into account { long doublePeriod = periodInTicks << 1; currentProjection = CurrentNodeTime.Ticks % doublePeriod; if (currentProjection > periodInTicks) { currentProjection = doublePeriod - currentProjection; } } else // No autoReversing { if (includeMaxPoint && activeDuration.HasValue && CurrentNodeTime == activeDuration) { currentProjection = periodInTicks; // Exceptional end case: we are exactly at the last point } else { currentProjection = CurrentNodeTime.Ticks % periodInTicks; } } projection.MergePoint(TimeSpan.FromTicks(currentProjection)); }
/// <summary> /// Take a single projection segment [CurrentNodeTime, NextNodeTime], break it into parts and merge the /// folded parts into this collection. /// NOTE: the TIC is normalized so beginTime = TimeSpan.Zero and we are already clipped. /// NOTE: projection should have allocated arrays. /// </summary> /// <param name="projection">The output projection.</param> /// <param name="activeDuration">The duration of the active period.</param> /// <param name="periodInTicks">The length of a simple duration in ticks.</param> /// <param name="isAutoReversed">Whether autoreversing is enabled</param> /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param> private bool ProjectionFoldInterval(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration, long periodInTicks, bool isAutoReversed, bool includeMaxPoint) { // Project the begin point for the segment, then look if we are autoreversing or not. long intervalLength = (NextNodeTime - CurrentNodeTime).Ticks; long timeBeforeNextPeriod, currentProjection; // Now see how the segment falls across periodic boundaries: // Case 1: segment stretches across a full period (we can exit early, since we cover the entire range of values) // Case 2: NON-AUTEREVERSED: segment stretches across two partial periods (we need to split into two segments and insert them into the projection) // Case 2: AUTOREVERSED: we need to pick the larger half of the partial period and project only that half, since it fully overlaps the other. // Case 3: segment is fully contained within a single period (just add the segment into the projection) // These cases are handled very differently for AutoReversing and non-AutoReversing timelines. if (isAutoReversed) // In the autoreversed case, we "fold" the segment onto itself and eliminate the redundant parts { bool beginOnReversingArc; long doublePeriod = periodInTicks << 1; currentProjection = CurrentNodeTime.Ticks % doublePeriod; if (currentProjection < periodInTicks) // We are on a forward-moving segment { beginOnReversingArc = false; timeBeforeNextPeriod = periodInTicks - currentProjection; } else // We are on a reversing segment, adjust the values accordingly { beginOnReversingArc = true; currentProjection = doublePeriod - currentProjection; timeBeforeNextPeriod = currentProjection; } Debug.Assert(timeBeforeNextPeriod > 0); long timeAfterNextPeriod = intervalLength - timeBeforeNextPeriod; // How much of our interval protrudes into the next period(s); this may be negative if we don't reach it. // See which part of the segment -- before or after part -- "dominates" when we fold them unto each other. if (timeAfterNextPeriod > 0) // Case 1 or 2: we reach into the next period but don't know if we completely cover it { bool collectionIsSaturated; if (timeBeforeNextPeriod >= timeAfterNextPeriod) // Before "dominates" { bool includeTime = CurrentNodeIsPoint; if (timeBeforeNextPeriod == timeAfterNextPeriod) // Corner case where before and after overlap exactly, find the IsPoint union { includeTime = includeTime || NextNodeIsPoint; } if (beginOnReversingArc) { projection.MergeInterval(TimeSpan.Zero, true, TimeSpan.FromTicks(currentProjection), includeTime); collectionIsSaturated = includeTime && (currentProjection == periodInTicks); } else { projection.MergeInterval(TimeSpan.FromTicks(currentProjection), includeTime, TimeSpan.FromTicks(periodInTicks), true); collectionIsSaturated = includeTime && (currentProjection == 0); } } else // After "dominates" { if (beginOnReversingArc) { long clippedTime = timeAfterNextPeriod < periodInTicks ? timeAfterNextPeriod : periodInTicks; projection.MergeInterval(TimeSpan.Zero, true, TimeSpan.FromTicks(clippedTime), NextNodeIsPoint); collectionIsSaturated = NextNodeIsPoint && (clippedTime == periodInTicks); } else { long clippedTime = timeAfterNextPeriod < periodInTicks ? periodInTicks - timeAfterNextPeriod : 0; projection.MergeInterval(TimeSpan.FromTicks(clippedTime), NextNodeIsPoint, TimeSpan.FromTicks(periodInTicks), true); collectionIsSaturated = NextNodeIsPoint && (clippedTime == 0); } } return collectionIsSaturated; // See if we just saturated the collection } else // Case 3: timeAfterNextPeriod < 0, we are fully contained in the current period { // No need to split anything, insert the interval directly if (beginOnReversingArc) // Here the nodes are reversed { projection.MergeInterval(TimeSpan.FromTicks(currentProjection - intervalLength), NextNodeIsPoint, TimeSpan.FromTicks(currentProjection), CurrentNodeIsPoint); } else { projection.MergeInterval(TimeSpan.FromTicks(currentProjection), CurrentNodeIsPoint, TimeSpan.FromTicks(currentProjection + intervalLength), NextNodeIsPoint); } return false; // Keep computing the projection } } else // No AutoReversing { currentProjection = CurrentNodeTime.Ticks % periodInTicks; timeBeforeNextPeriod = periodInTicks - currentProjection; // The only way to get 0 is if we clipped by endTime which equals CurrentNodeTime, which should not have been allowed Debug.Assert(intervalLength > 0); if (intervalLength > periodInTicks) // Case 1. We may stretch across a whole arc, even if we start from the end and wrap back around { // Quickly transform the collection into a saturated collection projection._nodeTime[0] = TimeSpan.Zero; projection._nodeIsPoint[0] = true; projection._nodeIsInterval[0] = true; projection._nodeTime[1] = TimeSpan.FromTicks(periodInTicks); projection._nodeIsPoint[1] = includeMaxPoint; projection._nodeIsInterval[1] = false; _count = 2; return true; // Bail early, we have the result ready } else if (intervalLength >= timeBeforeNextPeriod) // Case 2. We stretch until the next period begins (but not long enough to cover the length of a full period) { // Split the segment into two projected segments by wrapping around the period boundary projection.MergeInterval(TimeSpan.FromTicks(currentProjection), CurrentNodeIsPoint, TimeSpan.FromTicks(periodInTicks), false); if (intervalLength > timeBeforeNextPeriod) // See if we have a legitimate interval in the second clipped part { projection.MergeInterval(TimeSpan.Zero, true, TimeSpan.FromTicks(intervalLength - timeBeforeNextPeriod), NextNodeIsPoint); } else if (NextNodeIsPoint) // We only seem to have a point, wrapped around at zero (or in the exceptional case, at the max) { if (includeMaxPoint && activeDuration.HasValue && NextNodeTime == activeDuration) // Exceptional end case: we are exactly at the last point { projection.MergePoint(TimeSpan.FromTicks(periodInTicks)); } else { projection.MergePoint(TimeSpan.Zero); } } return false; // Keep computing the projection } else // Case 3: We fall within a single period { // No need to split anything, insert the interval directly projection.MergeInterval(TimeSpan.FromTicks(currentProjection), CurrentNodeIsPoint, TimeSpan.FromTicks(currentProjection + intervalLength), NextNodeIsPoint); return false; // Keep computing the projection } } }
/// <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; } }
/// <summary> /// Performs the FOLD operation, as described in the comments to the general projection function. /// We assume this method is only called with a finite, non-zero period length. /// The TIC is normalized so beginTime = 0. /// NOTE: projection should have allocated arrays. /// </summary> /// <param name="projection">The output projection.</param> /// <param name="activeDuration">The duration of the active period.</param> /// <param name="periodInTicks">The length of a simple duration in ticks.</param> /// <param name="isAutoReversed">Whether we have auto-reversing.</param> /// <param name="includeMaxPoint">Whether the fill zone forces the max point to be included.</param> private void ProjectionFold(ref TimeIntervalCollection projection, Nullable<TimeSpan> activeDuration, long periodInTicks, bool isAutoReversed, bool includeMaxPoint) { Debug.Assert(!IsEmptyOfRealPoints); // The entire projection process assumes we are not empty (have an intersection with the active zone). Debug.Assert(periodInTicks > 0); // We do not handle the degenerate case here. // Find the smallest n such that _nodeTime[n+1] > beginTime; if n is the last index, then consider _nodeTime[n+1] to be infinity MoveFirst(); Debug.Assert(CurrentNodeTime >= TimeSpan.Zero); // Verify that we are already clipped bool quitFlag = false; // As we walk, we maintain the invarant that the interval BEFORE _current is not included. // Otherwise we handle the interval and skip the interval's last node. // Process the remaining points and segments do { if (CurrentNodeIsInterval) // Project the interval starting here { quitFlag = ProjectionFoldInterval(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint); // Project and break up the clipped segment _current += NextNodeIsInterval ? 1 : 2; // Step over the next node if it's merely the end of this interval } else // This must be a lone point; the previous interval is no included by our invariant { Debug.Assert(CurrentNodeIsPoint); ProjectionFoldPoint(ref projection, activeDuration, periodInTicks, isAutoReversed, includeMaxPoint); _current++; } } while (!quitFlag && (_current < _count)); // While we haven't run out of indices, and haven't moved past endTime }
/// <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 }
/// <summary> /// Used for projecting the end of a fill period. When calling, we already know that we intersect the fill period /// but not the active period. /// </summary> /// <returns> /// Returns a collection which is the projection of the argument point onto the defined periodic function. /// </returns> /// <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.</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 ProjectPostFillZone(ref TimeIntervalCollection projection, TimeSpan beginTime, TimeSpan endTime, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { Debug.Assert(projection.IsEmpty); // Make sure the projection was properly cleared first Debug.Assert(!_invertCollection); // Make sure we never leave inverted mode enabled Debug.Assert(beginTime <= endTime); // Ensure legitimate begin/end clipping parameters Debug.Assert(!IsEmptyOfRealPoints); // We assume this function is ONLY called when this collection overlaps the postfill zone. So we cannot be empty. Debug.Assert(!period.HasTimeSpan || period.TimeSpan > TimeSpan.Zero || 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 long outputInTicks; if (beginTime == endTime) // Degenerate case when our active period is a single point; project only that point { outputInTicks = 0; } else // The case of non-zero active duration { outputInTicks = (long)(appliedSpeedRatio * (double)(endTime - beginTime).Ticks); if (period.HasTimeSpan) // Case of finite simple duration; in the infinite case we are already done { long periodInTicks = period.TimeSpan.Ticks; // Start by folding the point into its place inside a simple duration if (isAutoReversed) { long doublePeriod = periodInTicks << 1; // Fast multiply by 2 outputInTicks = outputInTicks % doublePeriod; if (outputInTicks > periodInTicks) { outputInTicks = doublePeriod - outputInTicks; } } else { outputInTicks = outputInTicks % periodInTicks; if (outputInTicks == 0) { outputInTicks = periodInTicks; // If we are at the end, stick to the max value } } if (accelRatio + decelRatio > 0) // Now if we have acceleration, warp the point by the correct amount { double dpPeriod = (double)periodInTicks; double inversePeriod = 1 / dpPeriod; double halfMaxRate = 1 / (2 - accelRatio - decelRatio); // Constants to simplify double t; long accelEnd = (long)(dpPeriod * accelRatio); long decelStart = periodInTicks - (long)(dpPeriod * decelRatio); if (outputInTicks < accelEnd) // We are in accel zone { t = (double)outputInTicks; outputInTicks = (long)(halfMaxRate * inversePeriod * t * t / accelRatio); } else if (outputInTicks <= decelStart) // We are in the linear zone { t = (double)outputInTicks; outputInTicks = (long)(halfMaxRate * (2 * t - accelRatio)); } else // We are in decel zone { t = (double)(periodInTicks - outputInTicks); outputInTicks = periodInTicks - (long)(halfMaxRate * inversePeriod * t * t / decelRatio); } } } } projection.InitializePoint(TimeSpan.FromTicks(outputInTicks)); }
internal virtual void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan endTime, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { }
internal override void ComputeCurrentFillInterval(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan endTime, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { _currentIntervals.Clear(); parentIntervalCollection.ProjectPostFillZone(ref _currentIntervals, beginTime, endTime, period, appliedSpeedRatio, accelRatio, decelRatio, isAutoReversed); }
internal override void ComputeCurrentIntervals(TimeIntervalCollection parentIntervalCollection, TimeSpan beginTime, TimeSpan? endTime, Duration fillDuration, Duration period, double appliedSpeedRatio, double accelRatio, double decelRatio, bool isAutoReversed) { _currentIntervals.Clear(); parentIntervalCollection.ProjectOntoPeriodicFunction(ref _currentIntervals, beginTime, endTime, fillDuration, period, appliedSpeedRatio, accelRatio, decelRatio, isAutoReversed); }