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.
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( "<name>" )</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}";
}