2023-06-23 00:37:25 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2018-10-11 16:44:25 +08:00
using System ;
2018-04-13 17:19:50 +08:00
using System.Collections.Generic ;
2020-02-17 14:06:14 +08:00
using System.Collections.Specialized ;
2022-12-16 17:16:26 +08:00
using System.Diagnostics ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
2018-11-12 15:38:33 +08:00
using Newtonsoft.Json ;
2019-12-05 16:49:32 +08:00
using osu.Framework.Bindables ;
using osu.Framework.Caching ;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Types ;
2018-11-20 15:51:59 +08:00
using osuTK ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.Objects
{
2019-12-05 16:49:32 +08:00
public class SliderPath
2018-04-13 17:19:50 +08:00
{
2019-12-05 17:31:28 +08:00
/// <summary>
/// The current version of this <see cref="SliderPath"/>. Updated when any change to the path occurs.
/// </summary>
2019-12-09 19:18:18 +08:00
[JsonIgnore]
2019-12-05 17:31:28 +08:00
public IBindable < int > Version = > version ;
private readonly Bindable < int > version = new Bindable < int > ( ) ;
2018-11-12 13:16:21 +08:00
/// <summary>
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
/// and the path will be shortened/lengthened to match this length.
/// </summary>
2019-12-05 16:49:54 +08:00
public readonly Bindable < double? > ExpectedDistance = new Bindable < double? > ( ) ;
2018-04-13 17:19:50 +08:00
2023-12-20 04:20:21 +08:00
public bool HasValidLength = > Precision . DefinitelyBigger ( Distance , 0 ) ;
2021-04-16 14:22:32 +08:00
2018-11-12 15:38:14 +08:00
/// <summary>
2019-12-05 16:49:32 +08:00
/// The control points of the path.
2018-11-12 15:38:14 +08:00
/// </summary>
2019-12-05 16:49:32 +08:00
public readonly BindableList < PathControlPoint > ControlPoints = new BindableList < PathControlPoint > ( ) ;
2018-11-12 15:38:14 +08:00
2019-12-05 16:49:32 +08:00
private readonly List < Vector2 > calculatedPath = new List < Vector2 > ( ) ;
private readonly List < double > cumulativeLength = new List < double > ( ) ;
2019-12-09 16:45:08 +08:00
private readonly Cached pathCache = new Cached ( ) ;
2024-03-27 03:05:04 +08:00
/// <summary>
/// Any additional length of the path which was optimised out during piecewise approximation, but should still be considered as part of <see cref="calculatedLength"/>.
/// </summary>
/// <remarks>
/// This is a hack for Catmull paths.
/// </remarks>
private double optimisedLength ;
/// <summary>
/// The final calculated length of the path.
/// </summary>
2019-12-06 14:06:31 +08:00
private double calculatedLength ;
2018-11-12 13:08:36 +08:00
2023-08-18 18:20:40 +08:00
private readonly List < int > segmentEnds = new List < int > ( ) ;
private double [ ] segmentEndDistances = Array . Empty < double > ( ) ;
2018-11-12 13:16:21 +08:00
/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
2019-12-05 17:19:42 +08:00
public SliderPath ( )
2018-11-01 14:38:19 +08:00
{
2019-12-05 17:31:28 +08:00
ExpectedDistance . ValueChanged + = _ = > invalidate ( ) ;
2018-04-13 17:19:50 +08:00
2020-02-17 14:06:14 +08:00
ControlPoints . CollectionChanged + = ( _ , args ) = >
2019-12-05 16:49:32 +08:00
{
2020-02-17 14:06:14 +08:00
switch ( args . Action )
{
case NotifyCollectionChangedAction . Add :
2022-12-16 17:16:26 +08:00
Debug . Assert ( args . NewItems ! = null ) ;
2024-02-17 20:46:38 +08:00
foreach ( object? newItem in args . NewItems )
( ( PathControlPoint ) newItem ) . Changed + = invalidate ;
2020-02-17 14:06:14 +08:00
break ;
2018-10-26 13:18:48 +08:00
2020-10-09 05:31:59 +08:00
case NotifyCollectionChangedAction . Reset :
2020-02-17 14:06:14 +08:00
case NotifyCollectionChangedAction . Remove :
2022-12-16 17:16:26 +08:00
Debug . Assert ( args . OldItems ! = null ) ;
2024-02-17 20:46:38 +08:00
foreach ( object? oldItem in args . OldItems )
( ( PathControlPoint ) oldItem ) . Changed - = invalidate ;
2020-02-17 14:06:14 +08:00
break ;
}
2019-12-05 16:49:32 +08:00
2019-12-05 17:31:28 +08:00
invalidate ( ) ;
2019-12-05 16:49:32 +08:00
} ;
2019-12-05 17:19:42 +08:00
}
/// <summary>
2019-12-09 16:45:08 +08:00
/// Creates a new <see cref="SliderPath"/> initialised with a list of control points.
2019-12-05 17:19:42 +08:00
/// </summary>
/// <param name="controlPoints">An optional set of <see cref="PathControlPoint"/>s to initialise the path with.</param>
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all control points.
/// The path will be shortened/lengthened to match this length. If null, the path will use the true distance between all control points.</param>
[JsonConstructor]
public SliderPath ( PathControlPoint [ ] controlPoints , double? expectedDistance = null )
: this ( )
{
2019-12-05 16:49:32 +08:00
ControlPoints . AddRange ( controlPoints ) ;
2019-12-05 17:19:42 +08:00
ExpectedDistance . Value = expectedDistance ;
}
2019-12-05 16:49:32 +08:00
2019-12-05 17:19:42 +08:00
public SliderPath ( PathType type , Vector2 [ ] controlPoints , double? expectedDistance = null )
2022-06-24 13:48:43 +08:00
: this ( controlPoints . Select ( ( c , i ) = > new PathControlPoint ( c , i = = 0 ? type : null ) ) . ToArray ( ) , expectedDistance )
2019-12-05 17:19:42 +08:00
{
2018-11-12 16:10:37 +08:00
}
2018-11-12 13:16:21 +08:00
/// <summary>
/// The distance of the path after lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
2018-11-12 15:38:39 +08:00
[JsonIgnore]
2018-11-12 15:38:14 +08:00
public double Distance
{
get
{
2019-12-05 16:49:32 +08:00
ensureValid ( ) ;
2019-12-14 20:54:22 +08:00
return cumulativeLength . Count = = 0 ? 0 : cumulativeLength [ ^ 1 ] ;
2018-11-12 15:38:14 +08:00
}
}
2018-11-12 13:08:36 +08:00
2019-12-06 14:53:19 +08:00
/// <summary>
/// The distance of the path prior to lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
public double CalculatedDistance
{
get
{
ensureValid ( ) ;
return calculatedLength ;
}
}
2024-04-01 16:22:50 +08:00
private bool optimiseCatmull ;
/// <summary>
/// Whether to optimise Catmull path segments, usually resulting in removing bulbs around stacked knots.
/// </summary>
/// <remarks>
/// This changes the path shape and should therefore not be used.
/// </remarks>
public bool OptimiseCatmull
{
get = > optimiseCatmull ;
set
{
optimiseCatmull = value ;
invalidate ( ) ;
}
}
2018-11-12 13:08:36 +08:00
/// <summary>
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list.
/// </summary>
/// <param name="path">The list to be filled with the computed path.</param>
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress ( List < Vector2 > path , double p0 , double p1 )
{
2019-12-05 16:49:32 +08:00
ensureValid ( ) ;
2018-11-12 15:38:14 +08:00
2018-11-12 13:08:36 +08:00
double d0 = progressToDistance ( p0 ) ;
double d1 = progressToDistance ( p1 ) ;
path . Clear ( ) ;
int i = 0 ;
2019-04-01 11:16:05 +08:00
2018-11-12 13:08:36 +08:00
for ( ; i < calculatedPath . Count & & cumulativeLength [ i ] < d0 ; + + i )
{
}
path . Add ( interpolateVertices ( i , d0 ) ) ;
for ( ; i < calculatedPath . Count & & cumulativeLength [ i ] < = d1 ; + + i )
path . Add ( calculatedPath [ i ] ) ;
path . Add ( interpolateVertices ( i , d1 ) ) ;
}
/// <summary>
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
/// to 1 (end of the path).
/// </summary>
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
public Vector2 PositionAt ( double progress )
{
2019-12-05 16:49:32 +08:00
ensureValid ( ) ;
2018-11-12 15:38:14 +08:00
2018-11-12 13:08:36 +08:00
double d = progressToDistance ( progress ) ;
return interpolateVertices ( indexOfDistance ( d ) , d ) ;
}
2018-04-13 17:19:50 +08:00
2021-03-22 22:55:30 +08:00
/// <summary>
/// Returns the control points belonging to the same segment as the one given.
/// The first point has a PathType which all other points inherit.
/// </summary>
/// <param name="controlPoint">One of the control points in the segment.</param>
public List < PathControlPoint > PointsInSegment ( PathControlPoint controlPoint )
{
bool found = false ;
List < PathControlPoint > pointsInCurrentSegment = new List < PathControlPoint > ( ) ;
2021-03-23 02:03:55 +08:00
2021-03-22 22:55:30 +08:00
foreach ( PathControlPoint point in ControlPoints )
{
2021-08-26 00:42:57 +08:00
if ( point . Type ! = null )
2021-03-22 22:55:30 +08:00
{
if ( ! found )
pointsInCurrentSegment . Clear ( ) ;
else
{
pointsInCurrentSegment . Add ( point ) ;
break ;
}
}
pointsInCurrentSegment . Add ( point ) ;
if ( point = = controlPoint )
found = true ;
}
return pointsInCurrentSegment ;
}
2022-12-07 16:51:22 +08:00
/// <summary>
2023-08-20 01:39:29 +08:00
/// Returns the progress values at which (control point) segments of the path end.
/// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
2022-12-07 16:51:22 +08:00
/// </summary>
2023-08-21 14:23:58 +08:00
/// <remarks>
/// <see cref="PositionAt"/> truncates the progression values to [0,1],
/// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
/// </remarks>
/// <example>
/// <para>
/// In case <see cref="Distance"/> is less than <see cref="CalculatedDistance"/>,
2023-08-20 01:39:29 +08:00
/// the last segment ends after the end of the path, hence it returns a value greater than 1.
2023-08-21 14:23:58 +08:00
/// </para>
/// <para>
2023-08-20 01:39:29 +08:00
/// In case <see cref="Distance"/> is greater than <see cref="CalculatedDistance"/>,
2023-08-21 14:23:58 +08:00
/// the last segment ends before the end of the path, hence it returns a value less than 1.
/// </para>
/// </example>
2022-12-07 16:51:22 +08:00
public IEnumerable < double > GetSegmentEnds ( )
{
ensureValid ( ) ;
2023-08-18 18:20:40 +08:00
return segmentEndDistances . Select ( d = > d / Distance ) ;
2022-12-07 16:51:22 +08:00
}
2019-12-05 17:31:28 +08:00
private void invalidate ( )
{
pathCache . Invalidate ( ) ;
version . Value + + ;
}
2019-12-05 16:49:32 +08:00
private void ensureValid ( )
2018-11-12 15:38:14 +08:00
{
2019-12-05 16:49:32 +08:00
if ( pathCache . IsValid )
2018-11-12 15:38:14 +08:00
return ;
2019-02-28 12:31:40 +08:00
2018-11-12 15:38:14 +08:00
calculatePath ( ) ;
2019-12-06 14:06:31 +08:00
calculateLength ( ) ;
2019-12-05 16:49:32 +08:00
pathCache . Validate ( ) ;
2018-11-12 15:38:14 +08:00
}
2019-12-05 16:49:32 +08:00
private void calculatePath ( )
2018-04-13 17:19:50 +08:00
{
2019-12-05 16:49:32 +08:00
calculatedPath . Clear ( ) ;
2022-12-07 16:51:22 +08:00
segmentEnds . Clear ( ) ;
2024-03-27 03:05:04 +08:00
optimisedLength = 0 ;
2019-04-01 11:16:05 +08:00
2019-12-05 16:49:32 +08:00
if ( ControlPoints . Count = = 0 )
return ;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
Vector2 [ ] vertices = new Vector2 [ ControlPoints . Count ] ;
for ( int i = 0 ; i < ControlPoints . Count ; i + + )
2021-08-26 00:42:57 +08:00
vertices [ i ] = ControlPoints [ i ] . Position ;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
int start = 0 ;
2019-04-01 11:16:05 +08:00
2019-12-05 16:49:32 +08:00
for ( int i = 0 ; i < ControlPoints . Count ; i + + )
{
2021-08-26 00:42:57 +08:00
if ( ControlPoints [ i ] . Type = = null & & i < ControlPoints . Count - 1 )
2019-12-05 16:49:32 +08:00
continue ;
2018-04-13 17:19:50 +08:00
2019-12-05 16:49:32 +08:00
// The current vertex ends the segment
var segmentVertices = vertices . AsSpan ( ) . Slice ( start , i - start + 1 ) ;
2023-11-08 18:43:54 +08:00
var segmentType = ControlPoints [ start ] . Type ? ? PathType . LINEAR ;
2023-09-19 14:28:28 +08:00
2023-09-19 13:31:26 +08:00
// No need to calculate path when there is only 1 vertex
if ( segmentVertices . Length = = 1 )
calculatedPath . Add ( segmentVertices [ 0 ] ) ;
else if ( segmentVertices . Length > 1 )
2019-12-05 16:49:32 +08:00
{
2024-04-01 16:22:50 +08:00
List < Vector2 > subPath = calculateSubPath ( segmentVertices , segmentType ) ;
2024-03-27 03:05:04 +08:00
2023-09-19 13:31:26 +08:00
// Skip the first vertex if it is the same as the last vertex from the previous segment
2024-02-17 20:46:38 +08:00
bool skipFirst = calculatedPath . Count > 0 & & subPath . Count > 0 & & calculatedPath . Last ( ) = = subPath [ 0 ] ;
2023-09-19 14:28:28 +08:00
2024-02-17 20:46:38 +08:00
for ( int j = skipFirst ? 1 : 0 ; j < subPath . Count ; j + + )
calculatedPath . Add ( subPath [ j ] ) ;
2019-12-05 16:49:32 +08:00
}
2018-10-11 16:44:25 +08:00
2023-08-18 18:20:40 +08:00
if ( i > 0 )
2023-08-19 20:34:20 +08:00
{
2023-08-18 18:20:40 +08:00
// Remember the index of the segment end
segmentEnds . Add ( calculatedPath . Count - 1 ) ;
2023-08-19 20:34:20 +08:00
}
2022-12-07 16:51:22 +08:00
2019-12-05 16:49:32 +08:00
// Start the new segment at the current vertex
start = i ;
}
2019-12-09 17:25:13 +08:00
}
2018-10-11 16:44:25 +08:00
2024-04-01 16:22:50 +08:00
private List < Vector2 > calculateSubPath ( ReadOnlySpan < Vector2 > subControlPoints , PathType type )
2019-12-09 17:25:13 +08:00
{
2023-11-11 22:02:06 +08:00
switch ( type . Type )
2018-04-13 17:19:50 +08:00
{
2023-11-08 18:43:54 +08:00
case SplineType . Linear :
2023-11-11 17:45:22 +08:00
return PathApproximator . LinearToPiecewiseLinear ( subControlPoints ) ;
2019-12-05 16:49:32 +08:00
2023-11-08 18:43:54 +08:00
case SplineType . PerfectCurve :
2024-03-27 03:05:04 +08:00
{
2019-12-09 17:25:13 +08:00
if ( subControlPoints . Length ! = 3 )
break ;
2018-10-11 16:44:25 +08:00
2023-11-11 17:45:22 +08:00
List < Vector2 > subPath = PathApproximator . CircularArcToPiecewiseLinear ( subControlPoints ) ;
2018-10-11 16:44:25 +08:00
2021-03-22 22:54:33 +08:00
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
2021-12-10 13:15:00 +08:00
if ( subPath . Count = = 0 )
2019-12-09 17:25:13 +08:00
break ;
2019-12-05 16:49:32 +08:00
2021-12-10 13:15:00 +08:00
return subPath ;
2024-03-27 03:05:04 +08:00
}
2019-12-05 16:49:32 +08:00
2023-11-08 18:43:54 +08:00
case SplineType . Catmull :
2024-03-27 03:05:04 +08:00
{
List < Vector2 > subPath = PathApproximator . CatmullToPiecewiseLinear ( subControlPoints ) ;
2024-04-01 16:22:50 +08:00
if ( ! OptimiseCatmull )
return subPath ;
2024-03-27 03:05:04 +08:00
// At draw time, osu!stable optimises paths by only keeping piecewise segments that are 6px apart.
// For the most part we don't care about this optimisation, and its additional heuristics are hard to reproduce in every implementation.
//
// However, it matters for Catmull paths which form "bulbs" around sequential knots with identical positions,
// so we'll apply a very basic form of the optimisation here and return a length representing the optimised portion.
// The returned length is important so that the optimisation doesn't cause the path to get extended to match the value of ExpectedDistance.
List < Vector2 > optimisedPath = new List < Vector2 > ( subPath . Count ) ;
Vector2 ? lastStart = null ;
double lengthRemovedSinceStart = 0 ;
for ( int i = 0 ; i < subPath . Count ; i + + )
{
if ( lastStart = = null )
{
optimisedPath . Add ( subPath [ i ] ) ;
lastStart = subPath [ i ] ;
continue ;
}
Debug . Assert ( i > 0 ) ;
double distFromStart = Vector2 . Distance ( lastStart . Value , subPath [ i ] ) ;
lengthRemovedSinceStart + = Vector2 . Distance ( subPath [ i - 1 ] , subPath [ i ] ) ;
// See PathApproximator.catmull_detail.
const int catmull_detail = 50 ;
const int catmull_segment_length = catmull_detail * 2 ;
// Either 6px from the start, the last vertex at every knot, or the end of the path.
if ( distFromStart > 6 | | ( i + 1 ) % catmull_segment_length = = 0 | | i = = subPath . Count - 1 )
{
optimisedPath . Add ( subPath [ i ] ) ;
optimisedLength + = lengthRemovedSinceStart - distFromStart ;
lastStart = null ;
lengthRemovedSinceStart = 0 ;
}
}
return optimisedPath ;
}
2018-04-13 17:19:50 +08:00
}
2019-12-09 17:25:13 +08:00
2023-11-11 17:45:22 +08:00
return PathApproximator . BSplineToPiecewiseLinear ( subControlPoints , type . Degree ? ? subControlPoints . Length ) ;
2018-04-13 17:19:50 +08:00
}
2019-12-06 14:06:31 +08:00
private void calculateLength ( )
2018-04-13 17:19:50 +08:00
{
2024-03-27 03:05:04 +08:00
calculatedLength = optimisedLength ;
2018-04-13 17:19:50 +08:00
cumulativeLength . Clear ( ) ;
2019-12-06 14:06:31 +08:00
cumulativeLength . Add ( 0 ) ;
2019-12-05 16:49:54 +08:00
2019-12-06 14:06:31 +08:00
for ( int i = 0 ; i < calculatedPath . Count - 1 ; i + + )
2018-04-13 17:19:50 +08:00
{
Vector2 diff = calculatedPath [ i + 1 ] - calculatedPath [ i ] ;
2019-12-06 14:06:31 +08:00
calculatedLength + = diff . Length ;
cumulativeLength . Add ( calculatedLength ) ;
2018-04-13 17:19:50 +08:00
}
2023-08-18 18:20:40 +08:00
// Store the distances of the segment ends now, because after shortening the indices may be out of range
segmentEndDistances = new double [ segmentEnds . Count ] ;
for ( int i = 0 ; i < segmentEnds . Count ; i + + )
{
segmentEndDistances [ i ] = cumulativeLength [ segmentEnds [ i ] ] ;
}
2019-12-06 14:06:31 +08:00
if ( ExpectedDistance . Value is double expectedDistance & & calculatedLength ! = expectedDistance )
2018-04-13 17:19:50 +08:00
{
2023-09-18 23:40:00 +08:00
// In osu-stable, if the last two path points of a slider are equal, extension is not performed.
if ( calculatedPath . Count > = 2 & & calculatedPath [ ^ 1 ] = = calculatedPath [ ^ 2 ] & & expectedDistance > calculatedLength )
2021-10-26 14:46:15 +08:00
{
cumulativeLength . Add ( calculatedLength ) ;
return ;
}
2019-12-06 14:06:31 +08:00
// The last length is always incorrect
cumulativeLength . RemoveAt ( cumulativeLength . Count - 1 ) ;
2018-04-13 17:19:50 +08:00
2019-12-06 14:06:31 +08:00
int pathEndIndex = calculatedPath . Count - 1 ;
if ( calculatedLength > expectedDistance )
{
// The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
2019-12-14 20:54:22 +08:00
while ( cumulativeLength . Count > 0 & & cumulativeLength [ ^ 1 ] > = expectedDistance )
2019-12-06 14:06:31 +08:00
{
cumulativeLength . RemoveAt ( cumulativeLength . Count - 1 ) ;
calculatedPath . RemoveAt ( pathEndIndex - - ) ;
}
}
if ( pathEndIndex < = 0 )
{
// The expected distance is negative or zero
// TODO: Perhaps negative path lengths should be disallowed altogether
2019-12-06 14:37:00 +08:00
cumulativeLength . Add ( 0 ) ;
2018-04-13 17:19:50 +08:00
return ;
2019-12-06 14:06:31 +08:00
}
// The direction of the segment to shorten or lengthen
Vector2 dir = ( calculatedPath [ pathEndIndex ] - calculatedPath [ pathEndIndex - 1 ] ) . Normalized ( ) ;
2018-04-13 17:19:50 +08:00
2019-12-14 20:54:22 +08:00
calculatedPath [ pathEndIndex ] = calculatedPath [ pathEndIndex - 1 ] + dir * ( float ) ( expectedDistance - cumulativeLength [ ^ 1 ] ) ;
2019-12-06 14:06:31 +08:00
cumulativeLength . Add ( expectedDistance ) ;
2018-10-29 14:36:43 +08:00
}
2018-04-13 17:19:50 +08:00
}
private int indexOfDistance ( double d )
{
int i = cumulativeLength . BinarySearch ( d ) ;
if ( i < 0 ) i = ~ i ;
return i ;
}
private double progressToDistance ( double progress )
{
2019-11-20 20:19:49 +08:00
return Math . Clamp ( progress , 0 , 1 ) * Distance ;
2018-04-13 17:19:50 +08:00
}
private Vector2 interpolateVertices ( int i , double d )
{
if ( calculatedPath . Count = = 0 )
return Vector2 . Zero ;
if ( i < = 0 )
return calculatedPath . First ( ) ;
2018-11-01 14:38:19 +08:00
if ( i > = calculatedPath . Count )
2018-04-13 17:19:50 +08:00
return calculatedPath . Last ( ) ;
Vector2 p0 = calculatedPath [ i - 1 ] ;
Vector2 p1 = calculatedPath [ i ] ;
double d0 = cumulativeLength [ i - 1 ] ;
double d1 = cumulativeLength [ i ] ;
// Avoid division by and almost-zero number in case two points are extremely close to each other.
if ( Precision . AlmostEquals ( d0 , d1 ) )
return p0 ;
double w = ( d - d0 ) / ( d1 - d0 ) ;
return p0 + ( p1 - p0 ) * ( float ) w ;
}
}
}