Rides/TrackRides/TrackNode.cs
using System;
using System.Text.Json.Serialization;
namespace HC3.Rides;
#nullable enable
/// <summary>
/// Describes the position and orientation of a <see cref="ITrackSection"/> at the meeting point between two elements.
/// </summary>
/// <param name="Position">Position in track grid units, which are twice the resolution of terrain grid units.</param>
/// <param name="Orientation">Heading, incline and banking of the track.</param>
public readonly record struct TrackNode( Vector3Int Position, TrackOrientation Orientation )
{
[JsonIgnore]
public TrackNode Inverse => throw new NotImplementedException();
public TrackNode? Append( TrackElement element )
{
if ( Orientation.Incline != element.StartIncline ) return null;
if ( Orientation.Banking != element.StartBanking ) return null;
if ( Orientation.Inverted != element.StartInverted ) return null;
var endHeading = Orientation.Heading.ToWorld( element.EndHeading );
var offset = element.EndOffset.Rotate( Orientation.Heading );
return new TrackNode( Position + offset, new TrackOrientation( endHeading, element.EndIncline, element.EndBanking, element.EndInverted ) );
}
public TrackSplineNode ToWorld( TrackSplineNode splineNode )
{
return new TrackSplineNode(
Position + splineNode.Position.Rotate( Orientation.Heading ),
splineNode.Tangent.Rotate( Orientation.Heading ),
splineNode.Up.Rotate( Orientation.Heading ),
splineNode.TangentScale );
}
}
public readonly record struct TrackSplineNode( Vector3 Position, Vector3 Tangent, Vector3 Up, float TangentScale )
{
public TrackSplineNode Transform( Transform transform )
{
return new TrackSplineNode(
transform.PointToWorld( Position ),
transform.NormalToWorld( Tangent * transform.Scale ),
transform.NormalToWorld( Up * transform.Scale.WithZ( 1f ) ),
TangentScale );
}
}
public static class TrackNodeExtensions
{
public static float Pitch( this TrackIncline incline ) => incline switch
{
TrackIncline.VerticalDown => 90,
TrackIncline.SteepDown => TrackOrientation.SteepInclineDegrees,
TrackIncline.ShallowDown => TrackOrientation.ShallowInclineDegrees,
TrackIncline.None => 0f,
TrackIncline.ShallowUp => -TrackOrientation.ShallowInclineDegrees,
TrackIncline.SteepUp => -TrackOrientation.SteepInclineDegrees,
TrackIncline.VerticalUp => -90,
_ => 0f
};
public static float Yaw( this TrackHeading heading ) =>
(int)heading * TrackOrientation.HeadingIncrementDegrees;
public static float Roll( this TrackBanking banking ) =>
(int)banking * TrackOrientation.BankingIncrementDegrees;
public static Vector2Int Forward( this TrackHeading heading ) => heading switch
{
TrackHeading.Forward => new Vector2Int( 1, 0 ),
TrackHeading.ForwardRight => new Vector2Int( 1, -1 ),
TrackHeading.Right => new Vector2Int( 0, -1 ),
TrackHeading.BackwardRight => new Vector2Int( -1, -1 ),
TrackHeading.Backward => new Vector2Int( -1, 0 ),
TrackHeading.BackwardLeft => new Vector2Int( -1, 1 ),
TrackHeading.Left => new Vector2Int( 0, 1 ),
TrackHeading.ForwardLeft => new Vector2Int( 1, 1 ),
_ => throw new NotImplementedException()
};
public static TrackHeading ToWorld( this TrackHeading heading, TrackHeading local )
{
return (TrackHeading)(((int)local + (int)heading) & 7);
}
public static TrackHeading ToLocal( this TrackHeading heading, TrackHeading world )
{
return (TrackHeading)(((int)world - (int)heading) & 7);
}
public static Vector2Int Rotate( this Vector2Int vector, int clockwise90Steps ) => (clockwise90Steps & 3) switch
{
0 => vector,
1 => new Vector2Int( vector.y, -vector.x ),
2 => new Vector2Int( -vector.x, -vector.y ),
3 => new Vector2Int( -vector.y, vector.x ),
_ => throw new NotImplementedException()
};
public static Vector2Int Rotate( this Vector2Int vector, TrackHeading heading ) => heading switch
{
TrackHeading.Forward => vector,
TrackHeading.Left => new Vector2Int( -vector.y, vector.x ),
TrackHeading.Backward => new Vector2Int( -vector.x, -vector.y ),
TrackHeading.Right => new Vector2Int( vector.y, -vector.x ),
_ => throw new NotImplementedException()
};
public static Vector2 Rotate( this Vector2 vector, TrackHeading heading ) => heading switch
{
TrackHeading.Forward => vector,
TrackHeading.Left => new Vector2( -vector.y, vector.x ),
TrackHeading.Backward => new Vector2( -vector.x, -vector.y ),
TrackHeading.Right => new Vector2( vector.y, -vector.x ),
_ => throw new NotImplementedException()
};
public static Vector3Int Rotate( this Vector3Int vector, TrackHeading heading )
{
var rotated2d = new Vector2Int( vector.x, vector.y ).Rotate( heading );
return new Vector3Int( rotated2d.x, rotated2d.y, vector.z );
}
public static Vector3 Rotate( this Vector3 vector, TrackHeading heading )
{
var rotated2d = new Vector2( vector.x, vector.y ).Rotate( heading );
return new Vector3( rotated2d.x, rotated2d.y, vector.z );
}
public static Rotation Rotate( this Rotation rotation, TrackHeading heading )
{
return Rotation.FromYaw( heading.Yaw() ) * rotation;
}
public static RectInt Rotate( this RectInt rect, int clockwise90Steps )
{
var min = rect.Position.Rotate( clockwise90Steps );
var max = (rect.Position + rect.Size).Rotate( clockwise90Steps );
(min, max) = (Vector2Int.Min( min, max ), Vector2Int.Max( min, max ));
return new RectInt( min, max - min );
}
public static bool IsDiagonal( this TrackHeading heading )
{
return ((int)heading & 1) == 0;
}
public static TrackHeading Mirror( this TrackHeading heading ) =>
(TrackHeading)(-(int)heading & 7);
public static TrackHeading Inverse( this TrackHeading heading ) =>
(TrackHeading)((int)(heading + 4) & 7);
public static TrackBanking Inverse( this TrackBanking banking ) =>
(TrackBanking)(-(int)banking);
public static TrackIncline Inverse( this TrackIncline incline ) =>
(TrackIncline)(-(int)incline);
public static TurnDirection Inverse( this TurnDirection direction ) =>
(TurnDirection)(-(int)direction);
public static int Ordinal( this TurnDirection direction )
{
var abs = Math.Abs( (int)direction );
return abs * 2 + ((int)direction < 0 ? 1 : 0);
}
public static int Ordinal( this TrackIncline incline )
{
var abs = Math.Abs( (int)incline );
return abs * 2 + ((int)incline < 0 ? 1 : 0);
}
public static int Ordinal( this TrackBanking banking )
{
var abs = Math.Abs( (int)banking );
return abs * 2 + ((int)banking < 0 ? 1 : 0);
}
public static bool AddTrack( this Spline spline, TrackNode start, IReadOnlyList<TrackElement> elements, List<float>? nodeDistances = null )
{
if ( elements.Count < 1 ) return false;
nodeDistances?.Add( 0f );
var prevNode = start;
Rotation prevRotation = prevNode.Orientation with { Banking = TrackBanking.None, Inverted = false };
var prev = new TrackSplineNode( prevNode.Position, prevRotation.Forward, prevRotation.Up, elements[0].StartTangentScale );
foreach ( var element in elements )
{
if ( prevNode.Append( element ) is not { } nextNode ) return false;
prev = prev with { TangentScale = element.StartTangentScale };
TrackSplineNode next;
if ( element.Definition.CustomSpline )
{
var transform = Transform.Zero;
if ( element.FlipX )
{
// TODO
}
if ( element.FlipY )
{
transform = new Transform( 0f, Rotation.Identity, new Vector3( 1f, -1f, 1f ) ).ToWorld( transform );
}
if ( element.FlipZ )
{
transform = new Transform( 0f, Rotation.Identity, new Vector3( 1f, 1f, -1f ) ).ToWorld( transform );
}
var points = element.FlipX
? element.Definition.MidPoints.AsEnumerable().Reverse()
: element.Definition.MidPoints;
foreach ( var point in points )
{
next = new TrackSplineNode( point.Position, point.Rotation.Forward, point.Rotation.Up, point.TangentScale )
.Transform( transform );
next = prevNode.ToWorld( next );
spline.AddSegment( prev, next, false );
prev = next;
}
}
Rotation nextRotation = nextNode.Orientation with { Banking = TrackBanking.None, Inverted = false };
next = new TrackSplineNode( nextNode.Position, nextRotation.Forward, nextRotation.Up, element.EndTangentScale );
spline.AddSegment( prev, next, nextNode == start );
nodeDistances?.Add( spline.Length );
if ( nextNode == start )
{
return true;
}
prevNode = nextNode;
prev = next;
}
return true;
}
private static void AddSegment( this Spline spline, TrackSplineNode prev, TrackSplineNode next, bool closeLoop )
{
var scale = new Vector3( TrackSection.GridSize, TrackSection.GridSize, TrackSection.HeightStep );
var length = ((next.Position - prev.Position) * scale).Length / 2.5f;
var tangentIn = -next.Tangent * length * next.TangentScale;
var tangentOut = Vector3.Zero;
if ( spline.PointCount > 0 )
{
var prevPoint = spline.GetPoint( spline.PointCount - 1 );
spline.UpdatePoint( spline.PointCount - 1, prevPoint with
{
Out = prev.Tangent * length * prev.TangentScale
} );
if ( closeLoop )
{
var firstPoint = spline.GetPoint( 0 );
tangentOut = firstPoint.Out;
spline.UpdatePoint( 0, firstPoint with { In = tangentIn } );
}
}
else
{
spline.AddPoint( new Spline.Point
{
Position = prev.Position * scale,
Out = prev.Tangent * length * prev.TangentScale,
Up = prev.Up,
Mode = Spline.HandleMode.Split
} );
}
spline.AddPoint( new Spline.Point
{
Position = next.Position * scale,
In = tangentIn,
Out = tangentOut,
Up = next.Up,
Mode = Spline.HandleMode.Split
} );
}
}