mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 05:48:07 -04:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
661 lines
15 KiB
C#
661 lines
15 KiB
C#
using System.Text.RegularExpressions;
|
|
using DisplayInfo = Sandbox.DisplayInfo;
|
|
|
|
namespace Editor.NodeEditor;
|
|
|
|
public class RerouteUI : NodeUI
|
|
{
|
|
public class Comment : GraphicsItem
|
|
{
|
|
private string _text;
|
|
public string Text
|
|
{
|
|
get => _text;
|
|
set
|
|
{
|
|
_text = value;
|
|
Update();
|
|
}
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( Text ) )
|
|
return;
|
|
|
|
Paint.Antialiasing = true;
|
|
Paint.TextAntialiasing = true;
|
|
|
|
Paint.SetDefaultFont( 10 );
|
|
|
|
var rect = LocalRect;
|
|
rect = Paint.MeasureText( rect, Text );
|
|
rect.Width += 20;
|
|
rect.Width = rect.Width.Clamp( 0, LocalRect.Width );
|
|
rect.Position = LocalRect.Center - new Vector2( MathF.Floor( rect.Width ) * 0.5f, 0 );
|
|
rect.Top = LocalRect.Top;
|
|
rect.Bottom = LocalRect.Bottom - 5;
|
|
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( Theme.ControlBackground.WithAlpha( 0.8f ) );
|
|
Paint.DrawRect( rect, 2 );
|
|
|
|
var pos = new Vector2( LocalRect.Center.x, LocalRect.Bottom - 5 );
|
|
Paint.DrawArrow( pos, pos + Vector2.Down * 5, 10 );
|
|
|
|
Paint.SetPen( Theme.TextControl );
|
|
Paint.DrawText( rect, Text );
|
|
}
|
|
|
|
protected override void OnMousePressed( GraphicsMouseEvent e )
|
|
{
|
|
base.OnMousePressed( e );
|
|
|
|
e.Accepted = false;
|
|
}
|
|
}
|
|
|
|
private Comment _comment;
|
|
|
|
public RerouteUI( GraphView graph, INode node ) : base( graph, node )
|
|
{
|
|
ZIndex = 0;
|
|
Position = node.Position;
|
|
Size = 16;
|
|
HandlePosition = 0.5f;
|
|
ToolTip = null;
|
|
|
|
if ( node is IRerouteNode reroute )
|
|
{
|
|
_comment = new Comment
|
|
{
|
|
Parent = this,
|
|
Size = new Vector2( 500, 30 ),
|
|
Position = new Vector2( 0, -25 ),
|
|
HandlePosition = new Vector2( 0.5f, 0.5f )
|
|
};
|
|
|
|
_comment.Bind( nameof( Comment.Text ) )
|
|
.ReadOnly()
|
|
.From( reroute, nameof( reroute.Comment ) );
|
|
}
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
var color = Outputs.First().HandleConfig.Color;
|
|
|
|
if ( !Paint.HasMouseOver )
|
|
{
|
|
color = color.Desaturate( 0.2f ).Darken( 0.3f );
|
|
}
|
|
|
|
Paint.SetPen( Theme.ControlBackground, 2 );
|
|
Paint.SetBrush( Paint.HasSelected ? SelectionOutline : color );
|
|
Paint.DrawRect( LocalRect, 10 );
|
|
}
|
|
|
|
protected override void Layout()
|
|
{
|
|
var preferSelectingOutput = Inputs.Any( x => x.Connection is not null );
|
|
|
|
foreach ( var input in Inputs )
|
|
{
|
|
input.Size = 14;
|
|
input.Position = 14 * -0.5f;
|
|
input.Visible = false;
|
|
input.ZIndex = input.DefaultZIndex = preferSelectingOutput ? 0f : 2f;
|
|
}
|
|
|
|
foreach ( var output in Outputs )
|
|
{
|
|
output.Size = 14;
|
|
output.Position = 14 * -0.5f;
|
|
output.Visible = false;
|
|
output.ZIndex = output.DefaultZIndex = preferSelectingOutput ? 2f : 0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
public partial class NodeUI : GraphicsItem
|
|
{
|
|
public INode Node { get; protected set; }
|
|
|
|
public GraphView Graph { get; protected set; }
|
|
|
|
public DisplayInfo DisplayInfo => Node.DisplayInfo;
|
|
|
|
public Color SelectionOutline = Color.Parse( "#ff99c8" ) ?? default;
|
|
public Color PrimaryColor = Color.Parse( "#ff99c8" ) ?? default;
|
|
|
|
public List<PlugIn> Inputs = new();
|
|
public List<PlugOut> Outputs = new();
|
|
|
|
protected virtual float TitleHeight => Node.HasTitleBar ? 24f : 0f;
|
|
|
|
private Rect _thumbRect;
|
|
|
|
public override Rect BoundingRect => base.BoundingRect.Grow( 8f, 4f, 8f, 4f );
|
|
|
|
private class Button : GraphicsItem
|
|
{
|
|
public Action OnPress { get; set; }
|
|
public string Icon { get; set; }
|
|
|
|
public Button( NodeUI parent ) : base( parent )
|
|
{
|
|
HoverEvents = true;
|
|
Cursor = CursorShape.Finger;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.SetPen( Theme.TextControl.WithAlpha( Paint.HasMouseOver ? 0.9f : 0.4f ) );
|
|
Paint.DrawIcon( LocalRect, Icon, 14, TextFlag.Center );
|
|
Paint.DrawRect( LocalRect );
|
|
}
|
|
|
|
protected override void OnMousePressed( GraphicsMouseEvent e )
|
|
{
|
|
base.OnMousePressed( e );
|
|
|
|
if ( e.LeftMouseButton )
|
|
{
|
|
e.Accepted = true;
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseReleased( GraphicsMouseEvent e )
|
|
{
|
|
base.OnMouseReleased( e );
|
|
|
|
if ( e.LeftMouseButton && LocalRect.IsInside( e.LocalPosition + Size * HandlePosition ) )
|
|
{
|
|
OnPress?.Invoke();
|
|
e.Accepted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public NodeUI( GraphView graph, INode node )
|
|
{
|
|
ZIndex = 1;
|
|
Node = node;
|
|
Graph = graph;
|
|
Movable = true;
|
|
Selectable = true;
|
|
HoverEvents = true;
|
|
Cursor = CursorShape.SizeAll;
|
|
|
|
Size = new Vector2( 256, 512 );
|
|
Position = node.Position;
|
|
|
|
UpdatePlugs( true );
|
|
|
|
Node.Changed += MarkNodeChanged;
|
|
}
|
|
|
|
public void Rebuild()
|
|
{
|
|
OnRebuild();
|
|
}
|
|
|
|
protected virtual void OnRebuild()
|
|
{
|
|
Position = Node.Position;
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
Node.Changed -= MarkNodeChanged;
|
|
}
|
|
|
|
public void MarkNodeChanged()
|
|
{
|
|
if ( !IsValid )
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdatePlugs( false );
|
|
Update();
|
|
|
|
Graph?.NodePositionChanged( this );
|
|
}
|
|
|
|
public static string FormatToolTip( string name, string description,
|
|
Type type = null, string error = null )
|
|
{
|
|
var tooltip = name.WithColor( "#9CDCFE" );
|
|
|
|
if ( type is not null )
|
|
{
|
|
tooltip += $": {type.ToRichText()}";
|
|
}
|
|
|
|
var desc = description ?? "No description given.";
|
|
tooltip += desc.StartsWith( "<br/>", StringComparison.OrdinalIgnoreCase )
|
|
? desc
|
|
: $"<br/>{desc}";
|
|
|
|
foreach ( var message in error?.Split( Environment.NewLine, StringSplitOptions.RemoveEmptyEntries ) ?? Array.Empty<string>() )
|
|
{
|
|
tooltip += $"<br/><span style=\"font-size: 11px; color: {Theme.Red.Hex};\">{message}</span>";
|
|
}
|
|
|
|
return tooltip;
|
|
}
|
|
|
|
protected override void OnHoverEnter( GraphicsHoverEvent e )
|
|
{
|
|
var display = DisplayInfo;
|
|
ToolTip = FormatToolTip( display.Name, display.Description, null, Node.ErrorMessage );
|
|
base.OnHoverEnter( e );
|
|
}
|
|
|
|
private void UpdatePlugs( bool firstTime )
|
|
{
|
|
if ( !IsValid )
|
|
{
|
|
return;
|
|
}
|
|
|
|
for ( var i = Inputs.Count - 1; i >= 0; --i )
|
|
{
|
|
var plugIn = Inputs[i];
|
|
var input = Node.Inputs.FirstOrDefault( x => x == plugIn.Inner );
|
|
|
|
if ( input is null )
|
|
{
|
|
Inputs.RemoveAt( i );
|
|
|
|
var connection = plugIn.Connection;
|
|
connection?.Disconnect();
|
|
connection?.Destroy();
|
|
|
|
plugIn.Destroy();
|
|
}
|
|
}
|
|
|
|
for ( var i = Outputs.Count - 1; i >= 0; --i )
|
|
{
|
|
var plugOut = Outputs[i];
|
|
var output = Node.Outputs.FirstOrDefault( x => x == plugOut.Inner );
|
|
|
|
if ( output is null )
|
|
{
|
|
Outputs.RemoveAt( i );
|
|
|
|
Graph.RemoveConnections( plugOut );
|
|
plugOut.Destroy();
|
|
}
|
|
}
|
|
|
|
var index = 0;
|
|
foreach ( var plug in Node.Inputs )
|
|
{
|
|
var match = Inputs.FirstOrDefault( x => x.Inner == plug );
|
|
|
|
if ( !match.IsValid() )
|
|
{
|
|
Inputs.Insert( index, new PlugIn( this, plug ) );
|
|
}
|
|
else if ( Inputs.IndexOf( match ) != index )
|
|
{
|
|
Inputs.Remove( match );
|
|
Inputs.Insert( index, match );
|
|
|
|
match.Update();
|
|
}
|
|
|
|
++index;
|
|
}
|
|
|
|
index = 0;
|
|
foreach ( var plug in Node.Outputs )
|
|
{
|
|
var match = Outputs.FirstOrDefault( x => x.Inner == plug );
|
|
|
|
if ( !match.IsValid() )
|
|
{
|
|
Outputs.Insert( index, new PlugOut( this, plug ) );
|
|
}
|
|
else if ( Outputs.IndexOf( match ) != index )
|
|
{
|
|
Outputs.Remove( match );
|
|
Outputs.Insert( index, match );
|
|
|
|
match.Update();
|
|
}
|
|
|
|
++index;
|
|
}
|
|
|
|
Layout();
|
|
Graph?.NodePositionChanged( this );
|
|
}
|
|
|
|
protected virtual void Layout()
|
|
{
|
|
var hasThumb = !Node.HasTitleBar && Node.DisplayInfo.Icon is not null || Node.Thumbnail is not null;
|
|
|
|
var inputHeight = Inputs.Sum( x => x.Inner.InTitleBar ? 0f : 24f );
|
|
var outputHeight = Outputs.Sum( x => x.Inner.InTitleBar ? 0f : 24f );
|
|
var thumbnailSize = hasThumb ? Node.Thumbnail is not null ? 88f : 24f : 0f;
|
|
var bodyHeight = MathF.Max( MathF.Max( inputHeight, outputHeight ), thumbnailSize );
|
|
|
|
var totalWidth = 160f;
|
|
var inputWidth = 80f;
|
|
var outputWidth = 80f;
|
|
|
|
if ( Node.AutoSize )
|
|
{
|
|
Paint.SetDefaultFont( 7, 500 );
|
|
var titleWidth = Node.HasTitleBar ? Paint.MeasureText( DisplayInfo.Name ).x + 44f : 0f;
|
|
|
|
if ( Inputs.Any( x => x.Inner.InTitleBar ) )
|
|
{
|
|
titleWidth += 24f;
|
|
}
|
|
|
|
if ( Outputs.Any( x => x.Inner.InTitleBar ) )
|
|
{
|
|
titleWidth += 24f;
|
|
}
|
|
|
|
Paint.SetDefaultFont();
|
|
inputWidth = Inputs
|
|
.Select( x => x.PreferredWidth )
|
|
.DefaultIfEmpty( 0f )
|
|
.Max();
|
|
outputWidth = Outputs
|
|
.Select( x => x.PreferredWidth )
|
|
.DefaultIfEmpty( 0f )
|
|
.Max();
|
|
|
|
totalWidth = Math.Max( 24f, Math.Max( titleWidth, inputWidth + outputWidth + thumbnailSize + (Node.HasTitleBar ? 8f : 0f) ) );
|
|
}
|
|
|
|
var verticalCenter = !Node.HasTitleBar;
|
|
|
|
totalWidth += Node.ExpandSize.x;
|
|
bodyHeight += Node.ExpandSize.y;
|
|
|
|
if ( verticalCenter )
|
|
{
|
|
bodyHeight = MathF.Ceiling( bodyHeight / Graph.GridSize ) * Graph.GridSize;
|
|
}
|
|
|
|
var totalHeight = TitleHeight + bodyHeight;
|
|
|
|
if ( !Node.HasTitleBar )
|
|
{
|
|
var size = Math.Max( totalWidth, totalHeight );
|
|
|
|
(totalWidth, totalHeight) = (Math.Max( 36f, size ), size);
|
|
}
|
|
|
|
totalWidth = totalWidth.SnapToGrid( Graph.GridSize );
|
|
totalHeight = totalHeight.SnapToGrid( Graph.GridSize );
|
|
|
|
var top = TitleHeight + (verticalCenter ? totalHeight - inputHeight : 0f) * 0.5f;
|
|
var index = 0;
|
|
|
|
top = top.SnapToGrid( Graph.GridSize );
|
|
|
|
var handleOffset = 6f;
|
|
|
|
foreach ( var input in Inputs )
|
|
{
|
|
if ( input.Inner.InTitleBar )
|
|
{
|
|
input.Position = new Vector2( -handleOffset, 0f );
|
|
input.Size = input.Size.WithX( 24f );
|
|
}
|
|
else
|
|
{
|
|
var plugWidth = inputWidth;
|
|
|
|
if ( index >= Outputs.Count && input.Inner.AllowStretch )
|
|
{
|
|
plugWidth = totalWidth;
|
|
}
|
|
|
|
plugWidth = Math.Max( 24f, plugWidth );
|
|
|
|
input.Position = new Vector2( -handleOffset, top );
|
|
input.Size = input.Size.WithX( plugWidth );
|
|
|
|
top += 24f;
|
|
++index;
|
|
}
|
|
|
|
input.Layout();
|
|
}
|
|
|
|
top = TitleHeight + (verticalCenter ? totalHeight - outputHeight : 0f) * 0.5f;
|
|
top = top.SnapToGrid( Graph.GridSize );
|
|
|
|
index = 0;
|
|
|
|
foreach ( var output in Outputs )
|
|
{
|
|
if ( output.Inner.InTitleBar )
|
|
{
|
|
output.Position = new Vector2( totalWidth - 24f + handleOffset, 0f );
|
|
output.Size = output.Size.WithX( 24f );
|
|
}
|
|
else
|
|
{
|
|
var plugWidth = Math.Max( 24f, outputWidth );
|
|
|
|
if ( index >= Inputs.Count && output.Inner.AllowStretch )
|
|
{
|
|
plugWidth = totalWidth;
|
|
}
|
|
|
|
output.Position = new Vector2( totalWidth - plugWidth + handleOffset, top );
|
|
output.Size = output.Size.WithX( plugWidth );
|
|
|
|
top += 24f;
|
|
++index;
|
|
}
|
|
|
|
output.Layout();
|
|
}
|
|
|
|
Size = new Vector2( totalWidth, totalHeight );
|
|
|
|
_thumbRect = new Rect( inputWidth, TitleHeight > 0f ? TitleHeight - 2f : 0f, Size.x - inputWidth - outputWidth, Size.y - TitleHeight );
|
|
|
|
if ( hasThumb && Node.HasTitleBar )
|
|
{
|
|
_thumbRect = _thumbRect.Shrink( 6f, 5f, 6f, 4f );
|
|
|
|
if ( inputWidth <= 0f != outputWidth <= 0f )
|
|
{
|
|
_thumbRect = _thumbRect.Align( thumbnailSize - 16f, inputWidth <= 0f ? TextFlag.LeftCenter : TextFlag.RightCenter );
|
|
}
|
|
}
|
|
|
|
PrepareGeometryChange();
|
|
}
|
|
|
|
private static Regex WordWrapPointRegex { get; } = new Regex( "[^A-Z][A-Z]|_[^_]" );
|
|
|
|
/// <summary>
|
|
/// Make <paramref name="value"/> able to word wrap at more places, like after an underscore or between words in PascalCase.
|
|
/// </summary>
|
|
private static string FixWordWrapping( string value )
|
|
{
|
|
return WordWrapPointRegex.Replace( value, x => $"{x.Value[0]}\x200B{x.Value[1]}" );
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
var rect = new Rect( 0f, Size );
|
|
var radius = 4;
|
|
|
|
PrimaryColor = Node.GetPrimaryColor( Graph );
|
|
if ( Paint.HasSelected ) PrimaryColor = SelectionOutline;
|
|
else
|
|
{
|
|
if ( !Node.IsReachable ) PrimaryColor = PrimaryColor.Desaturate( 0.5f ).Darken( 0.25f );
|
|
}
|
|
|
|
if ( Node.HasTitleBar )
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( PrimaryColor.Darken( 0.5f ) );
|
|
Paint.DrawRect( rect, radius );
|
|
}
|
|
else
|
|
{
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( PrimaryColor.Darken( 0.6f ) );
|
|
Paint.DrawRect( rect, radius );
|
|
}
|
|
|
|
Paint.ClearPen();
|
|
Paint.ClearBrush();
|
|
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( PrimaryColor.WithAlpha( 0.05f ) );
|
|
|
|
var display = DisplayInfo;
|
|
|
|
if ( Node.HasTitleBar )
|
|
{
|
|
// Normal node display, with a title bar and possible thumbnail
|
|
|
|
var titleRect = new Rect( rect.Position, new Vector2( rect.Width, TitleHeight ) ).Shrink( 4f, 0f, 4f, 0f );
|
|
|
|
if ( display.Icon != null )
|
|
{
|
|
Paint.SetPen( PrimaryColor.Lighten( 0.7f ).WithAlpha( 0.7f ) );
|
|
Paint.DrawIcon( titleRect.Shrink( 4 ), display.Icon, 17, TextFlag.LeftCenter );
|
|
titleRect.Left += 18;
|
|
}
|
|
|
|
var title = display.Name;
|
|
|
|
Paint.SetDefaultFont( 7, 500 );
|
|
Paint.SetPen( PrimaryColor.Lighten( 0.8f ) );
|
|
Paint.DrawText( titleRect.Shrink( 5, 0 ), title, TextFlag.LeftCenter );
|
|
|
|
if ( Node.Thumbnail is not null || Inputs.Any( x => !x.Inner.InTitleBar ) || Outputs.Any( x => !x.Inner.InTitleBar ) )
|
|
{
|
|
// body inner
|
|
float borderSize = 3;
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( PrimaryColor.Darken( 0.6f ) );
|
|
Paint.DrawRect( rect.Shrink( borderSize, TitleHeight, borderSize, borderSize ), radius - 2 );
|
|
}
|
|
|
|
if ( Node.Thumbnail is { } thumb )
|
|
{
|
|
var thumbRect = _thumbRect.Align( new Vector2( 72f, 72f ), TextFlag.Center );
|
|
|
|
Paint.Draw( thumbRect, thumb );
|
|
}
|
|
}
|
|
else if ( Node.Thumbnail is { } thumb )
|
|
{
|
|
// Node is a big square thumbnail with corner icon and title at the bottom, e.g. for resource / game object references
|
|
|
|
var thumbRect = _thumbRect.Align( new Vector2( 88f, 88f ), TextFlag.Center );
|
|
|
|
Paint.Draw( thumbRect, thumb );
|
|
|
|
if ( Node.DisplayInfo.Icon is { } icon )
|
|
{
|
|
var iconRect = thumbRect.Shrink( 2f ).Align( new Vector2( 12f, 12f ), TextFlag.LeftTop );
|
|
|
|
Paint.ClearPen();
|
|
Paint.SetBrush( PrimaryColor.Darken( 0.6f ) );
|
|
Paint.DrawRect( iconRect.Grow( 4f ), 2f );
|
|
|
|
Paint.ClearBrush();
|
|
Paint.SetPen( Theme.TextControl );
|
|
Paint.DrawIcon( iconRect, icon, 12f );
|
|
}
|
|
|
|
if ( Node.DisplayInfo.Name is { } name )
|
|
{
|
|
var textRect = thumbRect;
|
|
|
|
name = FixWordWrapping( name );
|
|
|
|
Paint.SetFont( null, 7f );
|
|
|
|
var backgroundRect = Paint.MeasureText( textRect, name, TextFlag.CenterBottom | TextFlag.WordWrap )
|
|
.Grow( 4f, 2f, 4f, 2f );
|
|
|
|
Paint.SetBrush( PrimaryColor.Darken( 0.6f ) );
|
|
Paint.ClearPen();
|
|
Paint.DrawRect( backgroundRect, 2f );
|
|
|
|
Paint.SetPen( Theme.TextControl );
|
|
Paint.DrawText( textRect, name, TextFlag.CenterBottom | TextFlag.WordWrap );
|
|
}
|
|
}
|
|
else if ( Node.DisplayInfo.Icon is { } icon )
|
|
{
|
|
// Node is an icon without text, e.g. for operators
|
|
|
|
var scale = icon.Length == 2 && !char.IsLetterOrDigit( icon[0] ) ? 0.5f : icon == "|" ? 0.75f : 1f;
|
|
|
|
Paint.SetPen( Theme.TextControl );
|
|
Paint.DrawIcon( _thumbRect, icon, (Math.Min( _thumbRect.Width, _thumbRect.Height ) - 8f) * scale );
|
|
}
|
|
|
|
Node.OnPaint( rect );
|
|
|
|
if ( Paint.HasSelected )
|
|
{
|
|
Paint.SetPen( SelectionOutline, 2.0f );
|
|
Paint.ClearBrush();
|
|
Paint.DrawRect( rect, radius );
|
|
}
|
|
else if ( Paint.HasMouseOver )
|
|
{
|
|
Paint.SetPen( SelectionOutline, 1.0f );
|
|
Paint.ClearBrush();
|
|
Paint.DrawRect( rect, radius );
|
|
}
|
|
}
|
|
|
|
protected override void OnMousePressed( GraphicsMouseEvent e )
|
|
{
|
|
Graph?.MoveablePressed();
|
|
}
|
|
|
|
protected override void OnMouseReleased( GraphicsMouseEvent e )
|
|
{
|
|
Graph?.MoveableReleased();
|
|
}
|
|
|
|
internal void DraggingPlug( Plug plug, Vector2 scenePosition, Connection source = null )
|
|
{
|
|
Graph?.DraggingPlug( plug, scenePosition, source );
|
|
}
|
|
|
|
internal void DroppedPlug( Plug plug, Vector2 scenePosition, Connection source = null )
|
|
{
|
|
Graph?.DroppedPlug( plug, scenePosition, source );
|
|
}
|
|
|
|
protected override void OnPositionChanged()
|
|
{
|
|
Position = Position.SnapToGrid( Graph.GridSize );
|
|
|
|
if ( Node != null )
|
|
{
|
|
Graph?.MoveableMoved();
|
|
Node.Position = Position;
|
|
}
|
|
|
|
Graph?.NodePositionChanged( this );
|
|
}
|
|
}
|