Editor/UpgradedShaderGraph/Core/BaseNode.cs

Base classes for a shader-graph node system used in the editor. Defines BaseNode with reflection-driven input/output plug discovery, plug records (BasePlug, BasePlugIn, BasePlugOut), PlugInfo, custom attributes for marking inputs/outputs/editors, UI creation hooks, and logic to bind/connect plugs to node properties.

ReflectionFile Access
namespace Editor.ShaderGraphExtras;

public abstract class BaseNode : INode
{
	public event Action Changed;

	[Hide, Browsable( false )]
	public string Identifier { get; set; }

	[JsonIgnore, Hide, Browsable( false )]
	public virtual DisplayInfo DisplayInfo { get; }

	[JsonIgnore, Hide, Browsable( false )]
	public bool CanClone => true;

	[JsonIgnore, Hide, Browsable( false )]
	public virtual bool CanRemove => true;

	[Hide, Browsable( false )]
	public Vector2 Position { get; set; }

	[JsonIgnore, Hide]
	public IGraph _graph;
	[Browsable( false )]
	[JsonIgnore, Hide]
	public IGraph Graph
	{
		get => _graph;
		set
		{
			_graph = value;
			FilterInputsAndOutputs();
		}
	}

	[JsonIgnore, Hide, Browsable( false )]
	public Vector2 ExpandSize { get; set; }

	[JsonIgnore, Hide, Browsable( false )]
	public bool AutoSize => false;

	[JsonIgnore, Hide, Browsable( false )]
	public virtual IEnumerable<IPlugIn> Inputs { get; protected set; }

	[JsonIgnore, Hide, Browsable( false )]
	public virtual IEnumerable<IPlugOut> Outputs { get; protected set; }

	[JsonIgnore, Hide, Browsable( false )]
	public string ErrorMessage => null;

	[JsonIgnore, Hide, Browsable( false )]
	public bool IsReachable => true;

	[Hide, Browsable( false )]
	public Dictionary<string, float> HandleOffsets { get; set; } = new();

	public BaseNode()
	{
		DisplayInfo = DisplayInfo.For( this );
		NewIdentifier();

		(Inputs, Outputs) = GetPlugs( this );
	}

	public void Update()
	{
		Changed?.Invoke();
	}

	public virtual void OnFrame()
	{

	}

	public string NewIdentifier()
	{
		Identifier = Guid.NewGuid().ToString();
		return Identifier;
	}

	public virtual NodeUI CreateUI( GraphView view )
	{
		return new NodeUI( view, this );
	}

	public Color GetPrimaryColor( GraphView view )
	{
		return PrimaryColor;
	}

	public virtual Menu CreateContextMenu( NodeUI node )
	{
		return null;
	}

	[JsonIgnore, Hide, Browsable( false )]
	public virtual Pixmap Thumbnail { get; }

	[JsonIgnore, Hide, Browsable( false )]
	public virtual Color PrimaryColor { get; } = Color.Lerp( new Color( 0.7f, 0.7f, 0.7f ), Theme.Blue, 0.1f );

	public virtual void OnPaint( Rect rect )
	{

	}

	public virtual void OnDoubleClick( MouseEvent e )
	{

	}

	[JsonIgnore, Hide, Browsable( false )]
	public bool HasTitleBar => true;

	[System.AttributeUsage( AttributeTargets.Property )]
	public class InputAttribute : Attribute
	{
		public System.Type Type;

		public InputAttribute( Type type = null )
		{
			Type = type;
		}
	}

	[System.AttributeUsage( AttributeTargets.Property )]
	public class InputDefaultAttribute : Attribute
	{
		public string Input;

		public InputDefaultAttribute( string input )
		{
			Input = input;
		}
	}

	[System.AttributeUsage( AttributeTargets.Property )]
	public class OutputAttribute : Attribute
	{
		public System.Type Type;

		public OutputAttribute( Type type = null )
		{
			Type = type;
		}
	}

	[System.AttributeUsage( AttributeTargets.Property )]
	public class EditorAttribute : Attribute
	{
		public string ValueName;

		public EditorAttribute( string valueName )
		{
			ValueName = valueName;
		}
	}

	[System.AttributeUsage( AttributeTargets.Property )]
	public class RangeAttribute : Attribute
	{
		public string Min;
		public string Max;
		public string Step;

		public RangeAttribute( string min, string max, string step )
		{
			Min = min;
			Max = max;
			Step = step;
		}
	}

	private static (IEnumerable<IPlugIn> Inputs, IEnumerable<IPlugOut> Outputs) GetPlugs( BaseNode node )
	{
		var type = node.GetType();

		var inputs = new List<BasePlugIn>();
		var outputs = new List<BasePlugOut>();

		foreach ( var propertyInfo in type.GetProperties() )
		{
			if ( propertyInfo.GetCustomAttribute<InputAttribute>() is { } inputAttrib )
			{
				inputs.Add( new BasePlugIn( node, new( propertyInfo ), inputAttrib.Type ?? typeof( object ) ) );
			}

			if ( propertyInfo.GetCustomAttribute<OutputAttribute>() is { } outputAttrib )
			{
				outputs.Add( new BasePlugOut( node, new( propertyInfo ), outputAttrib.Type ?? typeof( object ) ) );
			}
		}

		return (inputs, outputs);
	}

	private void FilterInputsAndOutputs()
	{
		if ( _graph is not null )
		{
			if ( Graph is ShaderGraph sg && !sg.IsSubgraph && this is IParameterNode pn )
			{
				Inputs = new List<IPlugIn>();
			}
		}
	}
}

public record BasePlug( BaseNode Node, PlugInfo Info, Type Type ) : IPlug
{
	INode IPlug.Node => Node;

	public string Identifier => Info.Name;
	public DisplayInfo DisplayInfo => Info.DisplayInfo;

	public ValueEditor CreateEditor( NodeUI node, Plug plug )
	{
		var editor = Info.CreateEditor( node, plug, Type );
		if ( editor is not null ) return editor;

		// Default
		{
			var defaultEditor = new DefaultEditor( plug );
		}

		return null;
	}

	public Menu CreateContextMenu( NodeUI node, Plug plug )
	{
		return null;
	}

	public void OnDoubleClick( NodeUI node, Plug plug, MouseEvent e )
	{

	}

	public bool ShowLabel => true;
	public bool AllowStretch => true;
	public bool ShowConnection => IsReachable;
	public bool InTitleBar => false;
	public bool IsReachable
	{
		get
		{
			var conditional = Info.Property?.GetCustomAttribute<ConditionalVisibilityAttribute>();
			if ( conditional is not null )
			{
				if ( conditional.TestCondition( Node.GetSerialized() ) ) return false;
			}

			return true;
		}
	}

	public string ErrorMessage => null;

	public override string ToString()
	{
		return $"{Node.Identifier}.{Identifier}";
	}
}

public record BasePlugIn( BaseNode Node, PlugInfo Info, Type Type ) : BasePlug( Node, Info, Type ), IPlugIn
{
	IPlugOut IPlugIn.ConnectedOutput
	{
		get
		{
			if ( Info.Property is null )
			{
				return Info.ConnectedPlug;
			}

			if ( Info.Type != typeof( NodeInput ) )
			{
				return null;
			}

			var value = Info.GetInput( Node );

			if ( !value.IsValid )
			{
				return null;
			}

			var node = ((ShaderGraph)Node.Graph).FindNode( value.Identifier );
			var output = node?.Outputs
				.FirstOrDefault( x => x.Identifier == value.Output );

			return output;
		}
		set
		{
			var property = Info.Property;
			if ( property is null )
			{
				Info.ConnectedPlug = value;
				return;
			}

			if ( property.PropertyType != typeof( NodeInput ) )
			{
				return;
			}

			if ( value is null )
			{
				property.SetValue( Node, default( NodeInput ) );
				return;
			}

			if ( value is not BasePlug fromPlug )
			{
				return;
			}

			property.SetValue( Node, new NodeInput
			{
				Identifier = fromPlug.Node.Identifier,
				Output = fromPlug.Identifier
			} );
		}
	}

	public float? GetHandleOffset( string name )
	{
		if ( Node.HandleOffsets.TryGetValue( name, out var value ) )
		{
			return value;
		}
		return null;
	}

	public void SetHandleOffset( string name, float? value )
	{
		if ( value is null ) Node.HandleOffsets.Remove( name );
		else Node.HandleOffsets[name] = value.Value;
	}
}

public record BasePlugOut( BaseNode Node, PlugInfo Info, Type Type ) : BasePlug( Node, Info, Type ), IPlugOut;

public class PlugInfo
{
	public Guid Id { get; set; }
	public string Name { get; set; }
	public Type Type { get; set; }
	public DisplayInfo DisplayInfo { get; set; }
	public PropertyInfo Property { get; set; } = null;
	public IPlugOut ConnectedPlug { get; set; } = null;

	public PlugInfo()
	{
		DisplayInfo = new();
	}
	public PlugInfo( PropertyInfo property )
	{
		Name = property.Name;
		Type = property.PropertyType;
		var info = DisplayInfo.ForMember( Type );
		info.Name = property.Name;
		var titleAttr = property.GetCustomAttribute<TitleAttribute>();
		if ( titleAttr is not null )
		{
			info.Name = titleAttr.Value;
		}
		DisplayInfo = info;
		Property = property;
	}

	public NodeInput GetInput( BaseNode node )
	{
		if ( Property is not null )
		{
			return (NodeInput)Property.GetValue( node )!;
		}

		return default;
	}

	public ValueEditor CreateEditor( NodeUI node, Plug plug, Type type )
	{
		var editor = Property?.GetCustomAttribute<BaseNode.EditorAttribute>();

		if ( editor is not null )
		{
			if ( type == typeof( float ) )
			{
				var slider = new FloatEditor( plug ) { Title = DisplayInfo.Name, Node = node };
				slider.Bind( "Value" ).From( node.Node, editor.ValueName );

				var range = Property.GetCustomAttribute<BaseNode.RangeAttribute>();
				if ( range != null )
				{
					slider.Bind( "Min" ).From( node.Node, range.Min );
					slider.Bind( "Max" ).From( node.Node, range.Max );
					slider.Bind( "Step" ).From( node.Node, range.Step );
				}
				else if ( Property.GetCustomAttribute<MinMaxAttribute>() is MinMaxAttribute minMax )
				{
					slider.Min = minMax.MinValue;
					slider.Max = minMax.MaxValue;
				}

				return slider;
			}

			if ( type == typeof( Color ) )
			{
				var slider = new ColorEditor( plug ) { Title = DisplayInfo.Name, Node = node };
				slider.Bind( "Value" ).From( node.Node, editor.ValueName );

				return slider;
			}
		}
		return null;
	}
}