editor/AdvancedMappingTools/Framework/CableRouteGeometry.cs

Editor helper that computes cable route geometry for the Advanced Mapping Tools package. Samples a path into offset, sagged polylines per cable, handles round bundles and loose rows, and returns PathTrack geometry plus corner indices for meshes and mounts.

namespace AdvancedMappingTools;
public sealed record CableTrackGeometry( int Index, PathTrack Track, int[] Corners );
/// <summary>The same sampled, offset and sagged geometry is used by cable meshes and their mounts.</summary>
public static class CableRouteGeometry
{
	static float[] SagPattern => [0, .7f, -.5f, .35f, -.8f, .55f];

	/// <summary>Cables held together by ties or in a round bundle hang as one; only a loose row varies.</summary>
	public static bool Tied( CableRouteSettings s ) => Math.Clamp( s.Cables, 1, 12 ) > 1 && (s.BundleLayout == CableBundleLayout.Round || float.IsFinite( s.TieSpacing ) && s.TieSpacing >= 8);

	/// <summary>Where cable <paramref name="cable"/> sits in the cross section, as (left, up).</summary>
	public static Vector2 Offset( CableRouteSettings s, int cable )
	{
		var radius = Math.Clamp( s.Radius, .25f, 32 );
		var cables = Math.Clamp( s.Cables, 1, 12 );
		var pitch = radius * 2 + Math.Clamp( float.IsFinite( s.CableGap ) ? s.CableGap : 0, 0, 32 );
		if ( s.BundleLayout != CableBundleLayout.Round || cables < 3 )
			return new( (cable - (cables - 1) * .5f) * pitch, 0 );
		// Round: on a circle wide enough that neighbours keep the gap.
		var ring = pitch * .5f / MathF.Sin( MathF.PI / cables );
		var angle = MathF.PI * 2 * cable / cables + MathF.PI * .5f;
		return new( MathF.Cos( angle ) * ring, MathF.Sin( angle ) * ring );
	}

	/// <summary>The middle line of the bundle, sampled and sagged like the cables.</summary>
	public static PathTrack Center( CableRouteSettings s ) => Build( s, Vector2.Zero, 1 )?.Track;

	public static IEnumerable<CableTrackGeometry> Tracks( CableRouteSettings s )
	{
		if ( s.Path == null || s.Path.SegmentCount == 0 )
			yield break;
		var cables = Math.Clamp( s.Cables, 1, 12 );
		for ( var cable = 0; cable < cables; cable++ )
		{
			// Each cable of a loose row hangs a little deeper or shallower than its neighbour.
			var sagScale = cables > 1 && !Tied( s ) ? 1 + Math.Clamp( float.IsFinite( s.SagVariation ) ? s.SagVariation : 0, 0, 1 ) * SagPattern[cable % SagPattern.Length] : 1;
			var built = Build( s, Offset( s, cable ), sagScale );
			if ( built is not null )
				yield return built with { Index = cable };
		}
	}

	static CableTrackGeometry Build( CableRouteSettings s, Vector2 offset, float sagScale )
	{
		if ( s.Path == null || s.Path.SegmentCount == 0 )
			return null;
		var nodes = s.Path.Nodes.Select( n => n.Position ).ToArray();
		var control = PathTrack.Along( nodes, s.Path.Closed, FrameMode.Upright );
		var route = control.Frames.Take( s.Path.Closed ? control.Count - 1 : control.Count ).Select( f => f.Place( offset.x, offset.y ) ).ToArray();
		var points = new List<Vector3>();
		var corners = new List<int>();
		var segments = s.Path.Closed ? route.Length : route.Length - 1;
		for ( var i = 0; i < segments; i++ )
		{
			var a = route[i];
			var b = route[(i + 1) % route.Length];
			if ( a.Distance( b ) < .01f )
				continue;
			corners.Add( points.Count );
			var count = Math.Clamp( (int)MathF.Ceiling( a.Distance( b ) / Math.Clamp( s.SampleStep, 4, 128 ) ), 2, 256 );
			for ( var j = 0; j < count; j++ )
			{
				var t = j / (float)count;
				points.Add( Vector3.Lerp( a, b, t ) - Vector3.Up * (s.Sag ? 4 * t * (1 - t) * MathF.Min( Math.Clamp( s.SagDepth, 0, 256 ) * sagScale, a.Distance( b ) * .45f ) : 0) );
			}
		}

		if ( !s.Path.Closed )
		{
			corners.Add( points.Count );
			points.Add( route[^1] );
		}

		if ( points.Count < 2 )
			return null;
		return new( 0, PathTrack.Along( points, s.Path.Closed, FrameMode.ParallelTransport ), corners.ToArray() );
	}
}