Editor/Prism/Model/Parameter.cs

Defines Parameter and ParameterUi types for the material editor. Parameter stores id, name, type, default value, UI hints and conversion helpers used by the compiler; ParameterUi holds editable UI metadata and can be lowered to immutable UiHints.

File Access
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Serialization;
using System.Text;

namespace Editor.Prism.Model;

/// <summary>
/// A blackboard parameter: a value the graph exposes to the material editor and to render attributes.
/// <para>
/// Parameters are first-class documents entities rather than a side effect of a "named constant" node.
/// Nodes reference them by <see cref="Id"/>, so renaming a parameter never breaks a graph, and a
/// parameter survives subgraph inlining instead of collapsing to a literal.
/// </para>
/// </summary>
public sealed class Parameter : IGraphParameter
{
	/// <summary>Build an empty parameter with a fresh id.</summary>
	public Parameter()
	{
		Id = ParamId.New();
		Type = ShaderType.Float;
		Ui = new ParameterUi();
	}

	/// <summary>Build a named, typed parameter with a fresh id and the type's default value.</summary>
	public Parameter( string name, ShaderType type ) : this()
	{
		Name = name;
		Type = type;
	}

	/// <summary>Stable id, minted once and never rewritten. This is what nodes store.</summary>
	public ParamId Id { get; set; }

	/// <summary>Display name. Also the basis of the generated uniform and attribute names.</summary>
	public string Name { get; set; } = "Parameter";

	/// <summary>The parameter's type. Determines which inline editor and which uniform kind it becomes.</summary>
	public ShaderType Type { get; set; }

	/// <summary>
	/// Default value, in the boxed shapes <c>ValueCodec</c> understands: <c>float</c>, <c>int</c>,
	/// <c>bool</c>, <c>Vector2/3/4</c>, <c>Color</c>, <c>string</c> or a texture descriptor.
	/// </summary>
	public object Default { get; set; }

	/// <summary>
	/// Explicit render-attribute name. When set, the generated uniform carries
	/// <c>Attribute( "&lt;name&gt;" )</c> so the value can be driven at runtime without a recompile.
	/// </summary>
	public string AttributeName { get; set; }

	/// <summary>Material-UI hints: control, range, grouping and ordering.</summary>
	public ParameterUi Ui { get; set; }

	/// <summary>Forward-compatibility bag preserved verbatim through a round-trip.</summary>
	public JsonObject X { get; set; }

	/// <summary>Group heading in the material editor. Shorthand for <c>Ui.Group</c>.</summary>
	public string Group
	{
		get => Ui?.Group;
		set
		{
			Ui ??= new ParameterUi();
			Ui.Group = value;
		}
	}

	/// <summary>Sort order within the group. Shorthand for <c>Ui.Order</c>.</summary>
	public int Order
	{
		get => Ui?.Order ?? 0;
		set
		{
			Ui ??= new ParameterUi();
			Ui.Order = value;
		}
	}

	/// <summary>True when the parameter declares an opaque resource rather than a numeric value.</summary>
	public bool IsResource => Type.IsObject;

	/// <summary>True when the parameter is a texture of any dimensionality.</summary>
	public bool IsTexture => Type.IsTexture;

	/// <summary>
	/// The attribute name actually used when emitting: the explicit one when set, otherwise the
	/// sanitized display name. Never empty for a parameter with a name.
	/// </summary>
	public string EffectiveAttributeName =>
		string.IsNullOrWhiteSpace( AttributeName ) ? Sanitize( Name ) : Sanitize( AttributeName );

	/// <summary>
	/// The generated shader symbol for this parameter, using the engine's hungarian convention:
	/// <c>g_flRoughness</c>, <c>g_vTint</c>, <c>g_tBaseColor</c>. Deterministic for a given name and type.
	/// <para>
	/// Routed through <see cref="GraphCompiler.ParameterSymbol"/> so the model, the compiler and any node
	/// that references a parameter all derive one name from one algorithm. Do not re-derive it.
	/// </para>
	/// </summary>
	public string UniformName => GraphCompiler.ParameterSymbol( this );

	/// <summary>True when the parameter is usable: it has a name and a resolvable type.</summary>
	public bool IsValid => !string.IsNullOrWhiteSpace( Name ) && !Type.IsVoid;

	/// <summary>Lower the UI hints into the shape the IR and the backends consume.</summary>
	public UiHints ToHints() => ( Ui ?? new ParameterUi() ).ToHints();

	// ------------------------------------------------------- IGraphParameter ----
	// The compiler consumes parameters through IGraphParameter so it never has to reference this class.
	// Four members need adapting: the boxed default becomes a ConstValue, the texture default splits
	// into an asset path plus an sRGB flag, and the mutable UI object becomes immutable hints.

	/// <summary>The numeric default, lowered into the compiler's literal shape.</summary>
	ConstValue IGraphParameter.DefaultValue =>
		ValueCodec.ToConst( Default ?? ValueCodec.Default( Type ) );

	/// <summary>The asset-path default, for a texture parameter. Null for everything else.</summary>
	string IGraphParameter.DefaultAsset => Default switch
	{
		TextureValue texture => string.IsNullOrWhiteSpace( texture.Path ) ? null : texture.Path,
		string path when IsResource && !string.IsNullOrWhiteSpace( path ) => path,
		_ => null
	};

	/// <summary>True when a texture default declares sRGB-encoded contents.</summary>
	bool IGraphParameter.Srgb => Default switch
	{
		TextureValue texture => texture.IsSrgb,
		_ => Ui is not null && Ui.Control == UiControl.Color
	};

	/// <summary>The immutable material-UI hints the IR carries.</summary>
	UiHints IGraphParameter.Ui => ToHints();

	/// <summary>Deep copy, keeping the same id. Used by undo snapshots and clipboard payloads.</summary>
	public Parameter Clone() => new()
	{
		Id = Id,
		Name = Name,
		Type = Type,
		Default = Default,
		AttributeName = AttributeName,
		Ui = Ui?.Clone() ?? new ParameterUi(),
		X = X?.DeepClone() as JsonObject
	};

	/// <summary>Deep copy with a freshly minted id. Used when pasting into a document that already has this id.</summary>
	public Parameter CloneWithNewId()
	{
		var copy = Clone();
		copy.Id = ParamId.New();
		return copy;
	}

	/// <inheritdoc/>
	public override string ToString() => $"{Name} : {Type} ({Id})";

	/// <summary>
	/// The hungarian prefix the engine uses for a given type. One definition, shared with the compiler
	/// and both backends — see <see cref="GraphCompiler.SymbolPrefix"/>.
	/// </summary>
	public static string Prefix( ShaderType type ) => GraphCompiler.SymbolPrefix( type );

	/// <summary>
	/// Turn a display name into a shader-legal identifier fragment: alphanumerics only, each word
	/// capitalised, never starting with a digit.
	/// </summary>
	public static string Sanitize( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) ) return "Unnamed";

		var sb = new StringBuilder( name.Length );
		var capitalise = true;

		foreach ( var c in name )
		{
			if ( char.IsLetterOrDigit( c ) )
			{
				sb.Append( capitalise ? char.ToUpperInvariant( c ) : c );
				capitalise = false;
				continue;
			}

			capitalise = true;
		}

		if ( sb.Length == 0 ) return "Unnamed";
		if ( char.IsDigit( sb[0] ) ) sb.Insert( 0, '_' );

		return sb.ToString();
	}
}

/// <summary>
/// Material-UI metadata for a <see cref="Parameter"/>. Mutable, because the Inspector edits it in
/// place; <see cref="ToHints"/> produces the immutable shape the compiler consumes.
/// </summary>
public sealed class ParameterUi
{
	/// <summary>Which inline editor the material UI shows.</summary>
	public UiControl Control { get; set; }

	/// <summary>Slider minimum. Null leaves the backend default.</summary>
	public float? Min { get; set; }

	/// <summary>Slider maximum. Null leaves the backend default.</summary>
	public float? Max { get; set; }

	/// <summary>Slider step. Null means continuous.</summary>
	public float? Step { get; set; }

	/// <summary>Group heading in the material editor.</summary>
	public string Group { get; set; }

	/// <summary>Sort order within the group.</summary>
	public int Order { get; set; }

	/// <summary>Tooltip text.</summary>
	public string Tooltip { get; set; }

	/// <summary>Dropdown options, for <see cref="UiControl.Dropdown"/>.</summary>
	public List<string> Options { get; set; }

	/// <summary>True when nothing has been customised, so the whole object can be omitted on save.</summary>
	public bool IsEmpty =>
		Control == UiControl.Default && Min is null && Max is null && Step is null &&
		string.IsNullOrEmpty( Group ) && Order == 0 && string.IsNullOrEmpty( Tooltip ) &&
		( Options is null || Options.Count == 0 );

	/// <summary>Lower into the immutable shape the IR carries.</summary>
	public UiHints ToHints() => new()
	{
		Control = Control,
		Min = Min ?? 0f,
		Max = Max ?? 1f,
		Step = Step ?? 0f,
		Group = Group,
		Order = Order,
		Tooltip = Tooltip,
		Options = Options is { Count: > 0 } ? Options.ToArray() : null
	};

	/// <summary>Deep copy.</summary>
	public ParameterUi Clone() => new()
	{
		Control = Control,
		Min = Min,
		Max = Max,
		Step = Step,
		Group = Group,
		Order = Order,
		Tooltip = Tooltip,
		Options = Options is null ? null : new List<string>( Options )
	};

	/// <inheritdoc/>
	public override string ToString() => $"{Control} {Group}/{Order}";
}