Editor/Prism/Ui/PrismConnectionStyle.cs

Editor UI code defining wire routing styles for a node graph. It provides an enum PrismWireStyle, resolves styles to ConnectionStyle singletons, and implements two styles: PrismBezierStyle (cubic bezier with fixed lead-out and tangent logic) and PrismOrthogonalStyle (Manhattan routing with draggable mid-column and rounded corners).

Native Interop
using Connection = Editor.NodeEditor.Connection;

namespace Editor.Prism.Ui;

/// <summary>How wires are routed across the canvas.</summary>
public enum PrismWireStyle
{
	/// <summary>Cubic curves that always leave a socket horizontally.</summary>
	Bezier,

	/// <summary>Manhattan routing with rounded corners and a draggable mid-column.</summary>
	Orthogonal
}

/// <summary>Resolves a <see cref="PrismWireStyle"/> to the connection style that implements it.</summary>
public static class PrismConnectionStyles
{
	/// <summary>The style object for a wire style. Both are singletons; the framework expects that.</summary>
	public static ConnectionStyle For( PrismWireStyle style ) => style switch
	{
		PrismWireStyle.Orthogonal => PrismOrthogonalStyle.Instance,
		_ => PrismBezierStyle.Instance
	};
}

/// <summary>
/// The default wire shape: a cubic curve with a guaranteed horizontal lead-out.
/// <para>
/// The built-in classic style eases its lead-out to zero as a wire becomes vertical, so two nodes
/// stacked directly above one another are joined by a wire that leaves the socket sideways-up and reads
/// as attached to the card rather than to the port. A fixed minimum lead fixes that, and the tangent
/// grows when the wire has to travel backwards so a feedback-shaped route bows out instead of folding
/// through the node that produced it.
/// </para>
/// </summary>
public sealed class PrismBezierStyle : ConnectionStyle
{
	/// <summary>The shared instance. Connection styles are stateless and compared by reference.</summary>
	public static PrismBezierStyle Instance { get; } = new();

	/// <summary>How far a wire travels horizontally before it is allowed to curve.</summary>
	public const float LeadOut = 14f;

	/// <inheritdoc/>
	protected override Rect OnGetBounds( Connection connection, Vector2 sceneStart, Vector2 sceneEnd ) =>
		new Rect( sceneStart ).AddPoint( sceneEnd ).Grow( 128f );

	/// <inheritdoc/>
	protected override void OnLayout( Connection connection, Vector2 sceneStart, Vector2 sceneEnd )
	{
		var start = connection.FromScene( sceneStart );
		var end = connection.FromScene( sceneEnd );

		var delta = end - start;
		var forward = MathF.Max( 0f, delta.x );
		var backward = MathF.Max( 0f, -delta.x );

		var tangent = Math.Clamp( forward * 0.5f, 32f, 220f ) + Math.Clamp( backward * 0.55f, 0f, 260f )
			+ Math.Clamp( MathF.Abs( delta.y ) * 0.12f, 0f, 90f );

		var a = start + new Vector2( LeadOut, 0f );
		var b = end - new Vector2( LeadOut, 0f );

		connection.MoveTo( start );
		connection.LineTo( a );
		connection.CubicLineTo( a + new Vector2( tangent, 0f ), b - new Vector2( tangent, 0f ), b );
		connection.LineTo( end );
	}
}

/// <summary>
/// Manhattan routing: out of the source, along a vertical column the user can drag, then into the
/// target. Corners are rounded to the grid size so a dense graph of right angles still looks drawn
/// rather than plotted.
/// </summary>
public sealed class PrismOrthogonalStyle : ConnectionStyle
{
	/// <summary>The shared instance.</summary>
	public static PrismOrthogonalStyle Instance { get; } = new();

	/// <summary>The name of the draggable mid-column handle.</summary>
	public const string ColumnHandle = "x";

	/// <summary>Minimum horizontal run before the wire is allowed to turn.</summary>
	public const float LeadOut = 16f;

	/// <inheritdoc/>
	protected override void OnConfigureHandles( Connection connection, Vector2 sceneStart, Vector2 sceneEnd )
	{
		var half = ( sceneEnd.x - sceneStart.x ) * 0.5f;

		AddHandle( new ConnectionHandleConfig( ColumnHandle, DragDirection.Horizontal,
			ConnectionPlug.Output, sceneStart, MathF.Max( half, LeadOut ), LeadOut ) );
	}

	/// <inheritdoc/>
	protected override Rect OnGetBounds( Connection connection, Vector2 sceneStart, Vector2 sceneEnd )
	{
		var column = sceneStart.x + Offset( connection, sceneStart, sceneEnd );

		return new Rect( sceneStart )
			.AddPoint( sceneEnd )
			.AddPoint( new Vector2( column, sceneStart.y ) )
			.AddPoint( new Vector2( column, sceneEnd.y ) )
			.Grow( 64f );
	}

	/// <inheritdoc/>
	protected override void OnLayout( Connection connection, Vector2 sceneStart, Vector2 sceneEnd )
	{
		var start = connection.FromScene( sceneStart );
		var end = connection.FromScene( sceneEnd );
		var column = start.x + Offset( connection, sceneStart, sceneEnd );

		connection.MoveTo( start );

		if ( MathF.Abs( end.y - start.y ) < 1f )
		{
			connection.LineTo( end );
			return;
		}

		var radius = MathF.Min( PrismTheme.GridSize * 0.75f,
			MathF.Min( MathF.Abs( end.y - start.y ), MathF.Abs( column - start.x ) ) * 0.5f );

		radius = MathF.Max( 0f, radius );

		var down = end.y > start.y ? 1f : -1f;
		var toColumn = column > start.x ? 1f : -1f;
		var fromColumn = end.x > column ? 1f : -1f;

		var beforeFirst = new Vector2( column - radius * toColumn, start.y );
		var afterFirst = new Vector2( column, start.y + radius * down );
		var beforeSecond = new Vector2( column, end.y - radius * down );
		var afterSecond = new Vector2( column + radius * fromColumn, end.y );

		connection.LineTo( beforeFirst );
		connection.CubicLineTo( new Vector2( column, start.y ), new Vector2( column, start.y ), afterFirst );
		connection.LineTo( beforeSecond );
		connection.CubicLineTo( new Vector2( column, end.y ), new Vector2( column, end.y ), afterSecond );
		connection.LineTo( end );
	}

	static float Offset( Connection connection, Vector2 sceneStart, Vector2 sceneEnd )
	{
		var half = ( sceneEnd.x - sceneStart.x ) * 0.5f;
		var stored = connection?.Input?.Inner.GetHandleOffset( ColumnHandle );

		return MathF.Max( LeadOut, stored ?? MathF.Max( half, LeadOut ) );
	}
}