Editor/ShaderGraphPlus/Nodes Core/BaseNode.cs
using Editor;
using ShaderGraphPlus.Nodes;
namespace ShaderGraphPlus;
[System.AttributeUsage( AttributeTargets.Class )]
internal class SubgraphOnlyAttribute : Attribute
{
public SubgraphOnlyAttribute()
{
}
}
public abstract class BaseNodePlus : IGraphNode
{
public event Action Changed;
[Hide, Browsable( false )]
public string Identifier { get; set; }
[JsonIgnore, Hide, Browsable( false )]
public virtual string Subtitle { get; }
[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;
[JsonIgnore, Hide, Browsable( false )]
public virtual bool CanPreview => true;
[JsonIgnore, Hide, Browsable( false )]
public virtual bool CanAddToGraph => true;
[Hide, Browsable( false )]
public Vector2 Position { get; set; }
[JsonIgnore, Hide]
public INodeGraph _graph;
[JsonIgnore, Hide, Browsable( false )]
internal int PreviewID { get; set; }
[JsonIgnore, Hide, Browsable( false )]
public bool Processed { get; set; } = false;
[JsonIgnore, Hide, Browsable( false )]
public INodeGraph Graph
{
get => _graph;
set
{
_graph = value;
FilterInputsAndOutputs();
}
}
[JsonIgnore, Hide, Browsable( false )]
public Vector2 ExpandSize { get; set; }
[JsonIgnore, Hide, Browsable( false )]
public virtual 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 { get; set; } = null;
[JsonIgnore, Hide, Browsable( false )]
public bool IsReachable => true;
[Hide, Browsable( false )]
public Dictionary<string, float> HandleOffsets { get; set; } = new();
public BaseNodePlus()
{
DisplayInfo = DisplayInfo.For( this );
NewIdentifier();
(Inputs, Outputs) = GetPlugs( this );
}
public override string ToString()
{
return $"{DisplayInfo.Fullname}.{Identifier}";
}
public void Update()
{
Changed?.Invoke();
}
public virtual void OnFrame()
{
}
public void ClearError()
{
HasError = false;
ErrorMessage = null;
}
public string NewIdentifier()
{
Identifier = Guid.NewGuid().ToString();
return Identifier;
}
public virtual NodeUI CreateUI( GraphView view )
{
return new NodeUI( view, this, false );
}
public Color GetNodeBodyTintColor( GraphView view )
{
return NodeBodyTintColor;
}
public Color GetNodeTitleColor( GraphView view )
{
return NodeTitleColor;
}
public virtual Menu CreateContextMenu( NodeUI node )
{
return null;
}
public virtual void DebugInfo( Menu menu )
{
var debugInfoHeading = menu.AddHeading( "Node Debug Info" );
menu.AddWidget( new Label( $"Node ID : {this.Identifier}" ) );
if ( this is IParameterNode blackboardSyncable )
{
menu.AddWidget( new Label( $"Blackboard ID : {blackboardSyncable.ParameterIdentifier}" ) ).AdjustSize();
}
menu.AddWidget( new Label( $"Preview ID : {this.PreviewID}" ) );
menu.AddWidget( new Label( $"IsReachable? : {this.IsReachable}" ) );
menu.AddWidget( new Label( $"CanPreview? : {this.CanPreview}" ) );
}
[JsonIgnore, Hide, Browsable( false )]
public virtual Pixmap Thumbnail { get; }
[JsonIgnore, Hide, Browsable( false )]
public virtual Color NodeBodyTintColor { get; set; } = Color.Parse( "#303030" )!.Value.Lighten( 2.0f );
[JsonIgnore, Hide, Browsable( false )]
public virtual Color NodeTitleColor { get; set; } = Color.Gray;
public virtual void OnPaint( Rect rect )
{
}
public virtual void OnDoubleClick( MouseEvent e )
{
}
[JsonIgnore, Hide, Browsable( false )]
public bool HasTitleBar => true;
[JsonIgnore, Hide, Browsable( false )]
public bool HasSubtitle => !string.IsNullOrWhiteSpace( Subtitle );
private bool _hasError;
[JsonIgnore, Hide, Browsable( false )]
public bool HasError
{
get => _hasError;
set
{
_hasError = value;
Update();
}
}
[JsonIgnore, Hide, Browsable( false )]
public bool HasWarning { get; set; } = false;
[System.AttributeUsage( AttributeTargets.Property )]
public class InputAttribute : Attribute
{
/// <summary>
/// Type of the port.
/// </summary>
public System.Type Type;
/// <summary>
/// Order of the port.
/// </summary>
public int Order;
public InputAttribute( Type type = null, int order = 0 )
{
Type = type;
Order = order;
}
}
[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
{
/// <summary>
/// Type of the port.
/// </summary>
public System.Type Type;
/// <summary>
/// Order of the port.
/// </summary>
public int Order;
public OutputAttribute( Type type = null, int order = 0 )
{
Type = type;
Order = order;
}
}
[System.AttributeUsage( AttributeTargets.Property )]
public class HideOutputAttribute : Attribute
{
public System.Type Type;
public HideOutputAttribute( Type type = null )
{
Type = type;
}
}
[System.AttributeUsage( AttributeTargets.Property )]
public class NodeValueEditorAttribute : Attribute
{
public string ValueName;
public NodeValueEditorAttribute( 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;
}
}
/// <summary>
/// Interface for nodes that want to setup anyting post node object deserializeation.
/// </summary>
public interface IInitializeNode
{
/// <summary>
/// Called after the node has been deserialized.
/// </summary>
public void InitializeNode();
}
/// <summary>
/// Connects a <see cref="NodeResult.Func"/> property from another node to the <see cref="NodeInput"/> property on this <see cref="BaseNodePlus"/> instance.
/// </summary>
/// <param name="targetInputName">The internal input name on this <see cref="BaseNodePlus"/> instance that will be connected to.</param>
/// <param name="sourceNodeOutputName">The internal output name of the source <see cref="BaseNodePlus"/> that the new connection is coming from.</param>
/// <param name="sourceNodeIdentifier">The Identifier of the souce <see cref="BaseNodePlus"/> that we are connecting from.</param>
public void ConnectNode( string targetInputName, string sourceNodeOutputName, string sourceNodeIdentifier )
{
if ( Graph == null )
{
throw new Exception( "Graph is null!!!" );
}
var graph = Graph as ShaderGraphPlus;
var targetOutputNode = graph.Nodes.Where( x => x.Identifier == sourceNodeIdentifier ).FirstOrDefault();
if ( targetOutputNode != null )
{
var plugIn = Inputs.Where( x => x.Identifier == targetInputName ).FirstOrDefault();
var targetOutputPlug = targetOutputNode.Outputs.FirstOrDefault( x => x.Identifier == sourceNodeOutputName );
if ( plugIn == null )
{
throw new Exception( $"Cannot find input with name '{targetInputName}' on node '{this}'" );
}
if ( targetOutputPlug == null )
{
throw new Exception( $"Cannot find output with name '{sourceNodeOutputName}' on node '{targetOutputNode}'" );
}
plugIn.ConnectedOutput = targetOutputPlug;
}
else
{
throw new Exception( $"Cannot find node with Identifier '{sourceNodeIdentifier}'" );
}
}
public static (IEnumerable<IPlugIn> Inputs, IEnumerable<IPlugOut> Outputs) GetPlugs( BaseNodePlus node )
{
var type = node.GetType();
var inputs = new List<BasePlugIn>();
var outputs = new List<BasePlugOut>();
var inputProperties = type.GetProperties().OrderBy( x =>
(x.GetCustomAttribute<InputAttribute>() is InputAttribute input) ? input.Order : 0 );
foreach ( var propertyInfo in inputProperties )
{
if ( propertyInfo.GetCustomAttribute<InputAttribute>() is { } inputAttrib )
{
inputs.Add( new BasePlugIn( node, new( propertyInfo ), inputAttrib.Type ?? typeof( object ) ) );
}
}
var outputProperties = type.GetProperties().OrderBy( x =>
(x.GetCustomAttribute<OutputAttribute>() is OutputAttribute output) ? output.Order : 0 );
foreach ( var propertyInfo in outputProperties )
{
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 ShaderGraphPlus sgp && !sgp.IsSubgraph && this is not BooleanFeatureSwitchNode && this is IParameterNode )
{
Inputs = new List<IPlugIn>();
}
}
}
}
public record BasePlug( BaseNodePlus Node, PlugInfo Info, Type Type ) : IPlug
{
IGraphNode IPlug.Node => Node;
Type IPlug.Type { get; set; } = Type;
public string Identifier => Info.Name;
public DisplayInfo DisplayInfo => Info.DisplayInfo;
public ValueEditor CreateEditor( NodeUI node, NodePlug 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, NodePlug plug )
{
return null;
}
public void OnDoubleClick( NodeUI node, NodePlug 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( BaseNodePlus 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 = ((ShaderGraphPlus)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( BaseNodePlus 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;
}
var descriptionAttr = property.GetCustomAttribute<DescriptionAttribute>();
if ( descriptionAttr is not null )
{
info.Description = descriptionAttr.Value;
}
DisplayInfo = info;
Property = property;
}
public NodeInput GetInput( BaseNodePlus node )
{
if ( Property is not null )
{
return (NodeInput)Property.GetValue( node )!;
}
return default;
}
public ValueEditor CreateEditor( NodeUI node, NodePlug plug, Type type )
{
var editor = Property?.GetCustomAttribute<BaseNodePlus.NodeValueEditorAttribute>();
if ( editor is not null )
{
if ( type == typeof( int ) )
{
var slider = new IntValueEditor( plug ) { Title = DisplayInfo.Name, Node = node };
slider.Bind( "Value" ).From( node.Node, editor.ValueName );
var range = Property.GetCustomAttribute<BaseNodePlus.RangeAttribute>();
if ( range != null )
{
slider.Bind( "Min" ).From( node.Node, range.Min );
slider.Bind( "Max" ).From( node.Node, range.Max );
}
else if ( Property.GetCustomAttribute<MinMaxAttribute>() is MinMaxAttribute minMax )
{
slider.Min = (int)minMax.MinValue;
slider.Max = (int)minMax.MaxValue;
}
return slider;
}
if ( type == typeof( float ) )
{
var slider = new FloatValueEditor( plug ) { Title = DisplayInfo.Name, Node = node };
slider.Bind( "Value" ).From( node.Node, editor.ValueName );
var range = Property.GetCustomAttribute<BaseNodePlus.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 ColorValueEditor( plug ) { Title = DisplayInfo.Name, Node = node };
slider.Bind( "Value" ).From( node.Node, editor.ValueName );
return slider;
}
if ( type == typeof( Gradient ) )
{
var slider = new GradientValueEditor( plug ) { Title = DisplayInfo.Name, Node = node };
slider.Bind( "Value" ).From( node.Node, editor.ValueName );
return slider;
}
}
return null;
}
}