namespace Editor.NodeEditor; public class Connection : GraphicsLine { public static Color SelectedColor { get; } = Color.Parse( "#ff99c8" )!.Value; public PlugOut Output { get; protected set; } public PlugIn Input { get; protected set; } public float WidthScale { get; set; } = 1f; public Color ColorTint { get; set; } private bool _dragging; public HandleConfig Config => Output.IsValid() ? Output.HandleConfig : Input.HandleConfig; public ConnectionStyle ConnectionStyle => (GraphicsView as GraphView)?.ConnectionStyle ?? ConnectionStyle.Default; private readonly Dictionary _handleConfigs = new(); private readonly Dictionary _handles = new(); public Vector2 OutputPosition { get; private set; } public Vector2 InputPosition { get; private set; } private Rect _localBounds; public override Rect BoundingRect => _localBounds; /// /// Per- data. /// public object StyleData { get; set; } public Connection( PlugOut output, PlugIn input ) { ZIndex = -10; HoverEvents = true; Selectable = true; Output = output; Input = input; input.SetConnectionInternal( this ); output.AddConnectionInternal( this ); Cursor = CursorShape.Finger; } public Connection( Plug source ) { Input = source as PlugIn; Output = source as PlugOut; ZIndex = -10; } internal void UpdateSceneBounds( Rect sceneRect ) { _localBounds = new Rect( FromScene( sceneRect.Position ), sceneRect.Size ).Grow( 64f ); } protected override void OnPaint() { if ( !Output.IsValid() && !Input.IsValid() ) return; if ( _dragging ) return; var config = Config; var outNode = Output.IsValid() ? Output.Node : null; var inNode = Input.IsValid() ? Input.Node : null; var color = Color.Lerp( config.Color, ColorTint.WithAlpha( 1f ), ColorTint.a ); var width = 4.0f; if ( !Input.IsValid() || !Input.IsValid() ) color = color.WithAlpha( 0.4f ); else if ( outNode?.Node.IsReachable is false || inNode?.Node.IsReachable is false ) color = color.Desaturate( 0.5f ).Darken( 0.25f ); if ( Paint.HasSelected ) { color = SelectedColor.Darken( 0.2f ); width = 4.0f; } if ( Paint.HasMouseOver ) { color = SelectedColor; width = 6.0f; } Paint.SetPen( color, width * WidthScale ); PaintLine(); } internal void LayoutForPreview( Plug plug, Vector2 scenePosition, Plug dropTarget ) { Output = plug as PlugOut ?? dropTarget as PlugOut; Input = plug as PlugIn ?? dropTarget as PlugIn; if ( Output.IsValid() && Input.IsValid() && Output.Node != Input.Node ) { Layout(); return; } OutputPosition = Output?.ConnectionPosition ?? scenePosition; InputPosition = Input?.ConnectionPosition ?? scenePosition; PrepareGeometryChange(); Position = OutputPosition; Size = new Vector2( 0f, 0f ); ConnectionStyle.Layout( this, OutputPosition, InputPosition ); } public void Layout() { OutputPosition = Output.ConnectionPosition; InputPosition = Input.ConnectionPosition; PrepareGeometryChange(); Position = OutputPosition; Size = new Vector2( 0f, 0f ); ConnectionStyle.Layout( this, OutputPosition, InputPosition ); } internal bool IsAttachedTo( NodeUI node ) { if ( node == Output?.Node ) return true; if ( node == Input?.Node ) return true; return false; } private void SetHandlesVisible( bool visible ) { if ( !Input.IsValid() || !Output.IsValid() ) return; if ( visible ) { foreach ( var config in _handleConfigs.Values ) { if ( !_handles.TryGetValue( config.Name, out var handle ) ) { _handles[config.Name] = handle = new ConnectionHandle( this ); } handle.Config = config; } } else { foreach ( var handle in _handles.Values ) { handle.Destroy(); } _handles.Clear(); } } private void UpdateZIndex() { ZIndex = Hovered ? -8 : Selected ? -9 : -10; } protected override void OnHoverEnter( GraphicsHoverEvent e ) { if ( Input.IsValid() && Output.IsValid() ) { ToolTip = $"{Output.Inner.Type.ToRichText()}
" + $"From: {Output.Node.Node.DisplayInfo.Name} \u2192 {Output.Inner.DisplayInfo.Name.WithColor( "#9CDCFE" )}
" + $"To: {Input.Node.Node.DisplayInfo.Name} \u2192 {Input.Inner.DisplayInfo.Name.WithColor( "#9CDCFE" )}
"; } UpdateZIndex(); SetHandlesVisible( true ); } protected override void OnHoverLeave( GraphicsHoverEvent e ) { UpdateZIndex(); SetHandlesVisible( Selected ); } protected override void OnSelectionChanged() { base.OnSelectionChanged(); UpdateZIndex(); SetHandlesVisible( Selected || Hovered ); } protected override void OnMousePressed( GraphicsMouseEvent e ) { if ( e.HasShift ) { Output.Node.Graph.RerouteConnection( this, e.ScenePosition ); e.Accepted = true; } } protected override void OnMouseReleased( GraphicsMouseEvent e ) { if ( _dragging ) { Output.Node.DroppedPlug( Output, e.ScenePosition, this ); } if ( !IsValid ) return; // connection might get deleted here _dragging = false; Cursor = CursorShape.Finger; Update(); } protected override void OnMouseMove( GraphicsMouseEvent e ) { _dragging = true; Cursor = CursorShape.DragLink; Output.Node.DraggingPlug( Output, e.ScenePosition, this ); Update(); foreach ( var handle in _handles.Values ) { handle.Destroy(); } _handles.Clear(); } public void Disconnect() { if ( Input.IsValid() ) Input.SetConnectionInternal( null ); if ( Output.IsValid() ) Output.RemoveConnectionInternal( this ); Output.Node.Graph.RemoveConnection( this ); } internal void SetHandles( IReadOnlyList configs ) { _handleConfigs.Clear(); foreach ( var config in configs ) { _handleConfigs[config.Name] = config; } var anyRemoved = false; foreach ( var (name, handle) in _handles ) { if ( _handleConfigs.TryGetValue( name, out var config ) ) { handle.Config = config; } else { anyRemoved = true; } } if ( !anyRemoved ) return; foreach ( var key in _handles.Keys.Where( x => !_handleConfigs.ContainsKey( x ) ).ToArray() ) { _handles.Remove( key, out var handle ); handle!.Destroy(); } } } public enum DragDirection { Horizontal, Vertical } public enum ConnectionPlug { Output, Input } public record struct ConnectionHandleConfig( string Name, DragDirection Direction, ConnectionPlug RelativePlug, Vector2 SceneOrigin, float Default, float? Min = null, float? Max = null ) { public Vector2 GetScenePosition( Connection connection ) { var value = GetValue( connection ); var axis = Direction == DragDirection.Horizontal ? new Vector2( 1f, 0f ) : new Vector2( 0f, 1f ); return SceneOrigin + axis * value; } public float GetValue( Connection connection ) { var value = connection.Input?.Inner.GetHandleOffset( Name ) ?? Default; return Math.Clamp( value, Min ?? float.NegativeInfinity, Max ?? float.PositiveInfinity ); } } internal sealed class ConnectionHandle : GraphicsItem { public Connection Connection { get; } private ConnectionHandleConfig _config; public ConnectionHandleConfig Config { get => _config; set { _config = value; UpdateCursor(); UpdatePosition(); } } public ConnectionHandle( Connection connection ) : base( connection ) { Connection = connection; ZIndex = 1; Size = new Vector2( 12f, 12f ); HandlePosition = new Vector2( 0.5f, 0.5f ); HoverEvents = true; Selectable = true; Movable = true; } private void UpdateCursor() { Cursor = Config.Direction == DragDirection.Horizontal ? CursorShape.SplitH : CursorShape.SplitV; } private void UpdatePosition() { PrepareGeometryChange(); Position = Connection.FromScene( Config.GetScenePosition( Connection ) ); } protected override void OnPaint() { var isDefault = Config.GetValue( Connection ).AlmostEqual( Config.Default ); var baseColor = isDefault ? Connection.SelectedColor : Color.White; Paint.SetPen( Connection.SelectedColor, 2f ); Paint.SetBrush( Hovered || Selected ? baseColor : baseColor.Darken( 0.2f ) ); Paint.DrawRect( Config.Direction == DragDirection.Horizontal ? LocalRect.Shrink( 2f, 0f ) : LocalRect.Shrink( 0f, 2f ), 2f ); } private bool AreNodesSelected => Config.RelativePlug == ConnectionPlug.Input ? Connection.Input is { Node.Selected: true } : Connection.Output is { Node.Selected: true }; protected override void OnMousePressed( GraphicsMouseEvent e ) { var view = GraphicsView as GraphView; view?.MoveablePressed(); } protected override void OnMouseReleased( GraphicsMouseEvent e ) { var view = GraphicsView as GraphView; view?.MoveableReleased(); } protected override void OnMoved() { if ( AreNodesSelected ) { UpdatePosition(); return; } var newScenePos = Connection.ToScene( Position ); var oldScenePos = Config.GetScenePosition( Connection ); var view = GraphicsView as GraphView; var gridSize = view?.GridSize ?? 12f; var diff = (newScenePos - oldScenePos).SnapToGrid( gridSize ); var offset = 0f; switch ( Config.Direction ) { case DragDirection.Horizontal: offset = diff.x; diff.y = 0f; break; case DragDirection.Vertical: offset = diff.y; diff.x = 0f; break; } if ( offset.AlmostEqual( 0f ) ) { UpdatePosition(); return; } view?.MoveableMoved(); var newValue = Config.GetValue( Connection ) + offset; Connection.Input?.Inner.SetHandleOffset( Config.Name, newValue.AlmostEqual( Config.Default ) ? null : newValue ); Connection.Layout(); } }