Model types for the Prism editor graph. Declares NodeFlags, NodeChangeKind, IPrismGraph, ValidationContext and the base PrismNode class with port management, change notification, validation hooks and painting/context menu stubs.
using Editor.Prism.Compiler;
using Editor.Prism.Core;
using System.ComponentModel;
namespace Editor.Prism.Model;
/// <summary>Optional per-node state, mirrored to the <c>flags</c> object in the document.</summary>
[Flags]
public enum NodeFlags
{
/// <summary>Nothing special.</summary>
None = 0,
/// <summary>Render a thumbnail of this node's value on the card.</summary>
Preview = 1 << 0,
/// <summary>Draw the card as a header-only strip.</summary>
Collapsed = 1 << 1,
/// <summary>Excluded from compilation; downstream inputs fall back to their inline values.</summary>
Disabled = 1 << 2,
/// <summary>Kept in view and excluded from auto-layout.</summary>
Pinned = 1 << 3
}
/// <summary>What changed about a node. Drives how much work the editor has to redo.</summary>
public enum NodeChangeKind
{
/// <summary>A literal or property value changed. Recompiles are value-only; topology is unaffected.</summary>
Value,
/// <summary>The port set changed. Edges must be revalidated and the card relaid out.</summary>
Ports,
/// <summary>A serialized property changed in a way that affects generated code structure.</summary>
Properties,
/// <summary>The node moved. Cosmetic; never triggers a recompile.</summary>
Position,
/// <summary>The node's flags changed.</summary>
Flags,
/// <summary>The node's validity changed; diagnostics must be refreshed.</summary>
Validity
}
/// <summary>
/// The minimum a node needs to know about the document that owns it.
/// <para>
/// Declared here, in the model, so <see cref="PrismNode"/> never depends on the concrete graph class
/// or on <c>Editor.NodeEditor</c>. The graph implementation lives in a later package and implements
/// this interface alongside <c>Editor.NodeEditor.IGraph</c>.
/// </para>
/// </summary>
public interface IPrismGraph
{
/// <summary>Stable document id.</summary>
string DocumentId { get; }
/// <summary>True when this document is a subgraph (<c>.prismfn</c>) rather than a shader graph.</summary>
bool IsSubgraph { get; }
/// <summary>Every node in the document, including reroutes and unknown nodes.</summary>
IReadOnlyList<PrismNode> Nodes { get; }
/// <summary>Every resolved connection in the document.</summary>
IReadOnlyList<Edge> Edges { get; }
/// <summary>Look up a node by id. Returns null when it does not exist.</summary>
PrismNode FindNode( NodeId id );
/// <summary>The edge terminating on an input port, if any.</summary>
bool TryGetIncomingEdge( NodeId node, PortId port, out Edge edge );
/// <summary>Every edge leaving an output port.</summary>
IEnumerable<Edge> GetOutgoingEdges( NodeId node, PortId port );
/// <summary>Called by a node when something about it changed.</summary>
void OnNodeChanged( PrismNode node, NodeChangeKind kind );
}
/// <summary>Everything the card painter tells a node about how it is currently being drawn.</summary>
public readonly record struct NodePaintState( bool Selected, bool Hovered, bool Disabled, bool HasError, float Zoom );
/// <summary>
/// Passed to <see cref="PrismNode.OnValidate"/>. Cheap, pure, runs on every graph edit —
/// report problems here rather than throwing, and never mutate the graph.
/// </summary>
public sealed class ValidationContext
{
/// <summary>Build a validation context for one node.</summary>
public ValidationContext( PrismNode node, IPrismGraph graph, DiagnosticSink diagnostics )
{
Node = node;
Graph = graph;
Diagnostics = diagnostics;
}
/// <summary>The node being validated.</summary>
public PrismNode Node { get; }
/// <summary>The document that owns it.</summary>
public IPrismGraph Graph { get; }
/// <summary>Where problems go.</summary>
public DiagnosticSink Diagnostics { get; }
/// <summary>True when the named input has an incoming edge.</summary>
public bool IsConnected( string port )
{
var input = Node?.FindInput( PortId.Parse( port ) );
return input is not null && input.IsConnected;
}
/// <summary>The resolved type of a port, or void when it has not been solved yet.</summary>
public ShaderType TypeOf( string port )
{
var found = Node?.FindPort( PortId.Parse( port ) );
return found?.EffectiveType ?? ShaderType.Void;
}
/// <summary>Report an error against this node, optionally against one of its ports.</summary>
public void Error( string message, string port = null, string code = DiagnosticCode.MissingInput ) =>
Diagnostics?.Error( code, message, Ref( port ) );
/// <summary>Report a warning against this node, optionally against one of its ports.</summary>
public void Warn( string message, string port = null, string code = DiagnosticCode.LossyConversion ) =>
Diagnostics?.Warn( code, message, Ref( port ) );
/// <summary>Report an informational note against this node.</summary>
public void Info( string message, string port = null, string code = DiagnosticCode.SampleLowered ) =>
Diagnostics?.Info( code, message, Ref( port ) );
GraphRef Ref( string port ) => string.IsNullOrEmpty( port )
? GraphRef.ForNode( Node?.Id ?? NodeId.None )
: GraphRef.ForPort( Node?.Id ?? NodeId.None, PortId.Parse( port ) );
}
/// <summary>
/// The base class every Prism node derives from.
/// <para>
/// A node is pure model: it declares ports, holds serialized properties and emits IR. It knows
/// nothing about widgets, HLSL or Slang. The single method every node must implement is
/// <see cref="Emit"/>, which builds IR through <see cref="EmitContext"/>.
/// </para>
/// </summary>
public abstract class PrismNode
{
readonly PortCollection _ports = new();
/// <summary>Build a node and its reflected port set.</summary>
protected PrismNode()
{
Id = NodeId.New();
BuildPorts();
}
// Everything in this block is model plumbing, and every one of these members is hidden from
// reflection-driven UI on purpose.
//
// The Inspector builds a ControlSheet over node.GetSerialized(), and s&box's SerializedObject
// walks public properties. `Graph` reaches the whole document and `Inputs`/`Outputs` reach ports
// that hold a `Node` back-reference, so either one closes a cycle:
// node -> Graph -> Nodes -> node -> ...
// node -> Inputs -> Port.Node -> Inputs -> ...
// The walk is unbounded, and because it happens inside engine code it is a StackOverflowException
// that .NET cannot catch: the whole editor dies with no log line and no autosave. Facepunch's own
// BaseNode hides the equivalent members for exactly this reason.
//
// None of this affects saving: PrismSerializer writes id/pos/flags explicitly from the schema and
// reads node properties through NodeProperties, neither of which consults these attributes.
/// <summary>Stable id, unique within the document. Minted once and never rewritten.</summary>
[Hide, Browsable( false ), JsonIgnore]
public NodeId Id { get; internal set; }
/// <summary>The document that owns this node. Null while the node is detached.</summary>
[Hide, Browsable( false ), JsonIgnore]
public IPrismGraph Graph { get; internal set; }
/// <summary>Scene position of the card, snapped to the grid by the editor.</summary>
[Hide, Browsable( false ), JsonIgnore]
public Vector2 Position { get; set; }
/// <summary>Optional per-node state.</summary>
[Hide, Browsable( false ), JsonIgnore]
public NodeFlags Flags { get; set; }
/// <summary>Cached reflection metadata for this node's concrete type.</summary>
[Hide, Browsable( false ), JsonIgnore]
public NodeDescriptor Descriptor => NodeDescriptors.Describe( GetType() );
/// <summary>Input ports, in socket order.</summary>
[Hide, Browsable( false ), JsonIgnore]
public IReadOnlyList<InputPort> Inputs => _ports.Inputs;
/// <summary>Output ports, in socket order.</summary>
[Hide, Browsable( false ), JsonIgnore]
public IReadOnlyList<OutputPort> Outputs => _ports.Outputs;
/// <summary>Ports, properties or validity changed. Drives UI invalidation and recompiles.</summary>
public event Action<PrismNode, NodeChangeKind> Changed;
/// <summary>Find an input port by id.</summary>
public InputPort FindInput( PortId id ) => _ports.FindInput( id );
/// <summary>Find an output port by id.</summary>
public OutputPort FindOutput( PortId id ) => _ports.FindOutput( id );
/// <summary>Find a port of either direction by id.</summary>
public Port FindPort( PortId id ) => _ports.Find( id );
/// <summary>
/// Add ports that cannot be expressed as [In]/[Out] properties (variadic, subgraph, mode-dependent).
/// <para>
/// Called once from the constructor and again from every <see cref="RebuildPorts"/>. Because the
/// first call happens before derived field initialisers run, read only what is safe at that point
/// and call <see cref="RebuildPorts"/> once the node's properties have been deserialized.
/// </para>
/// </summary>
protected virtual void OnDefinePorts( PortBuilder b ) { }
/// <summary>Cheap, pure, runs on every graph edit. Report problems; never throw.</summary>
public virtual void OnValidate( ValidationContext ctx ) { }
/// <summary>Emit IR for every output that was demanded. THE method every node implements.</summary>
public abstract void Emit( EmitContext ctx );
/// <summary>Optional custom body painting inside the card (curves, swatches, previews).</summary>
public virtual void OnPaintBody( Rect rect, NodePaintState state ) { }
/// <summary>Contribute entries to the node's right-click menu.</summary>
public virtual void OnContextMenu( Menu menu ) { }
[ThreadStatic] static int s_notifyDepth;
static bool s_cycleReported;
/// <summary>
/// How deep change notification may nest before it is treated as a cycle. A legitimate edit
/// notifies a handful of levels at most: model -> graph -> card -> plugs -> layout.
/// </summary>
const int MaxNotifyDepth = 32;
/// <summary>
/// Raise <see cref="Changed"/>.
/// <para>
/// Guarded against re-entrancy. Every change notification in the editor funnels through here, so a
/// handler that edits the node it is being told about — directly, or via the card, the inspector or
/// the compile service — would recurse without bound. That is a <c>StackOverflowException</c>, which
/// .NET makes uncatchable: the editor dies instantly, with no log line and no autosave. A depth cap
/// turns that into one loud diagnostic and a dropped notification.
/// </para>
/// </summary>
protected void MarkChanged( NodeChangeKind kind = NodeChangeKind.Value )
{
if ( s_notifyDepth >= MaxNotifyDepth )
{
if ( !s_cycleReported )
{
s_cycleReported = true;
Log.Error( $"Prism: change-notification cycle on '{Descriptor?.Id ?? GetType().Name}' " +
$"#{Id} ({kind}) — breaking it to keep the editor alive. Please report this stack:" +
Environment.NewLine + Environment.StackTrace );
}
return;
}
s_notifyDepth++;
try
{
Changed?.Invoke( this, kind );
Graph?.OnNodeChanged( this, kind );
}
finally
{
s_notifyDepth--;
}
}
/// <summary>
/// Rebuild the port set from reflection plus <see cref="OnDefinePorts"/>. The sanctioned runtime
/// mutation: ports whose ids survive keep their resolved type and inline value, and edges to ports
/// that disappear become broken-edge ghosts rather than being silently deleted.
/// </summary>
protected void RebuildPorts()
{
BuildPorts();
MarkChanged( NodeChangeKind.Ports );
}
/// <summary>
/// Rebuild the port set from outside the node, after its properties have been deserialized.
/// <para>
/// <see cref="RebuildPorts"/> is deliberately <c>protected</c>: a node owns its own port set and
/// nothing else may reshape it. Loading is the one exception — <see cref="OnDefinePorts"/> first runs
/// from the constructor, before any deserialized property has been assigned, so a node whose ports
/// depend on its properties has the wrong ones until someone asks again. The serializer is the only
/// caller, which is why this is <c>internal</c> rather than public.
/// </para>
/// <para>
/// It raises <see cref="Changed"/> exactly as <see cref="RebuildPorts"/> does, and must: that
/// notification is what lets the document turn edges to ports that just disappeared into broken-edge
/// ghosts. Suppressing it here would silently delete those connections instead.
/// </para>
/// </summary>
internal void RebuildPortsAfterLoad() => RebuildPorts();
void BuildPorts()
{
var builder = PortBuilder.FromReflection( GetType() );
OnDefinePorts( builder );
_ports.Apply( this, builder );
}
}