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).
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 ) );
}
}