Data model and JSON (de)serialization helpers for Prism editor documents. Defines the on-disk schema records for documents, meta, settings, preview, parameters, nodes, edges, groups, notes and view, plus JsonSerializer options and custom converters for several id structs to read/write them as bare JSON strings.
using Editor.Prism.Compiler.Backends;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
namespace Editor.Prism.Serialization;
/// <summary>
/// The on-disk shape of a <c>.prism</c> / <c>.prismfn</c> document, mirroring the schema one for one.
/// <para>
/// Every section carries an <c>x</c> bag so anything a future version writes survives a round-trip
/// through an older editor, and node properties stay as raw JSON so an unregistered node type is
/// re-emitted byte for byte instead of being destroyed on save.
/// </para>
/// </summary>
public sealed record PrismDocument
{
/// <summary>Document schema version.</summary>
public int Schema { get; init; } = PrismConstants.DocumentSchemaVersion;
/// <summary>Stable document id.</summary>
public string Id { get; init; }
/// <summary><c>shader</c> or <c>subgraph</c>.</summary>
public string Kind { get; init; } = PrismConstants.DocumentKindShader;
/// <summary>Title, description, authorship and timestamps.</summary>
public DocumentMeta Meta { get; init; }
/// <summary>Domain, shading model, blend mode, targets and dialect.</summary>
public DocumentSettings Settings { get; init; }
/// <summary>Per-graph preview state.</summary>
public PreviewStateDto Preview { get; init; }
/// <summary>Blackboard parameters.</summary>
public List<ParameterDto> Parameters { get; init; } = new();
/// <summary>Feature / static / dynamic combo declarations.</summary>
public List<KeywordDto> Keywords { get; init; } = new();
/// <summary>Node instances, including reroutes and unrecognised types.</summary>
public List<NodeDto> Nodes { get; init; } = new();
/// <summary>Connections.</summary>
public List<EdgeDto> Edges { get; init; } = new();
/// <summary>Group / comment boxes.</summary>
public List<GroupDto> Groups { get; init; } = new();
/// <summary>Sticky notes.</summary>
public List<NoteDto> Notes { get; init; } = new();
/// <summary>Last pan and zoom.</summary>
public ViewDto View { get; init; }
/// <summary>Forward-compatibility bag. Anything unrecognised at the top level lands here.</summary>
public JsonObject X { get; init; }
}
/// <summary>The <c>meta</c> object.</summary>
public sealed record DocumentMeta
{
/// <summary>Display title.</summary>
public string Title { get; init; }
/// <summary>Free-text description.</summary>
public string Description { get; init; }
/// <summary>Library category for subgraphs.</summary>
public string Category { get; init; }
/// <summary>Material Icons glyph name.</summary>
public string Icon { get; init; }
/// <summary>Author name.</summary>
public string Author { get; init; }
/// <summary>Creation timestamp, UTC.</summary>
public DateTimeOffset? Created { get; init; }
/// <summary>Last-modified timestamp, UTC.</summary>
public DateTimeOffset? Modified { get; init; }
/// <summary>Version of Prism that last wrote the document.</summary>
public string EditorVersion { get; init; }
}
/// <summary>The <c>settings</c> object.</summary>
public sealed record DocumentSettings
{
/// <summary>What the graph is for.</summary>
public ShaderDomain Domain { get; init; } = ShaderDomain.Surface;
/// <summary>How a surface graph resolves to pixels.</summary>
public ShadingModel ShadingModel { get; init; } = ShadingModel.Lit;
/// <summary>Output blending.</summary>
public SurfaceBlendMode BlendMode { get; init; } = SurfaceBlendMode.Opaque;
/// <summary>Triangle culling.</summary>
public CullMode CullMode { get; init; } = CullMode.Back;
/// <summary>Declared render passes.</summary>
public List<string> Modes { get; init; } = new();
/// <summary>Backend ids to emit on save.</summary>
public List<string> Targets { get; init; } = new();
/// <summary>Which HLSL flavour the generated <c>.shader</c> uses.</summary>
public HlslDialect HlslDialect { get; init; } = HlslDialect.SboxSlang;
/// <summary>Whether the second UV channel is declared.</summary>
public bool Uv2 { get; init; }
/// <summary>Whether back faces are rendered.</summary>
public bool RenderBackfaces { get; init; }
/// <summary>Whether generated code gets descriptive temp names and per-node comments.</summary>
public bool DebugSymbols { get; init; }
/// <summary>Forward-compatibility bag.</summary>
public JsonObject X { get; init; }
}
/// <summary>The <c>preview</c> object.</summary>
public sealed record PreviewStateDto
{
/// <summary>Built-in mesh name.</summary>
public string Mesh { get; init; } = "Sphere";
/// <summary>Custom model asset path, when one is chosen.</summary>
public string Model { get; init; }
/// <summary>Environment map asset path.</summary>
public string Envmap { get; init; }
/// <summary>Whether the ground plane is drawn.</summary>
public bool ShowGround { get; init; }
/// <summary>Whether the skybox is drawn.</summary>
public bool ShowSkybox { get; init; } = true;
/// <summary>Tint applied to the preview material, as <c>"r,g,b,a"</c>.</summary>
public string Tint { get; init; }
/// <summary>Viewport background, as <c>"r,g,b,a"</c>.</summary>
public string Background { get; init; }
/// <summary>Camera orbit state.</summary>
public PreviewCameraDto Camera { get; init; }
}
/// <summary>The <c>preview.camera</c> object.</summary>
public sealed record PreviewCameraDto
{
/// <summary>Orbit yaw in degrees.</summary>
public float Yaw { get; init; }
/// <summary>Orbit pitch in degrees.</summary>
public float Pitch { get; init; }
/// <summary>Orbit distance in world units.</summary>
public float Distance { get; init; } = 150f;
}
/// <summary>One entry of the <c>parameters</c> array.</summary>
public sealed record ParameterDto
{
/// <summary>Stable parameter id, referenced by nodes.</summary>
public string Id { get; init; }
/// <summary>Display name, also the basis of the generated uniform name.</summary>
public string Name { get; init; }
/// <summary>Type spelling, e.g. <c>float</c> or <c>Texture2D</c>.</summary>
public string Type { get; init; }
/// <summary>Default value, in the typed-literal encoding for the parameter's type.</summary>
public JsonNode Default { get; init; }
/// <summary>Render-attribute name, when the parameter is driven at runtime.</summary>
public string Attribute { get; init; }
/// <summary>Material-UI hints.</summary>
public UiDto Ui { get; init; }
/// <summary>Forward-compatibility bag.</summary>
public JsonObject X { get; init; }
}
/// <summary>The <c>ui</c> object of a parameter.</summary>
public sealed record UiDto
{
/// <summary>Which editor to show.</summary>
public UiControl Control { get; init; }
/// <summary>Slider minimum.</summary>
public float? Min { get; init; }
/// <summary>Slider maximum.</summary>
public float? Max { get; init; }
/// <summary>Slider step.</summary>
public float? Step { get; init; }
/// <summary>Group heading in the material editor.</summary>
public string Group { get; init; }
/// <summary>Sort order within the group.</summary>
public int Order { get; init; }
/// <summary>Tooltip text.</summary>
public string Tooltip { get; init; }
/// <summary>Dropdown options.</summary>
public List<string> Options { get; init; }
}
/// <summary>One entry of the <c>keywords</c> array.</summary>
public sealed record KeywordDto
{
/// <summary>Stable keyword id.</summary>
public string Id { get; init; }
/// <summary>Combo name as it appears in the shader, e.g. <c>F_PUDDLES</c>.</summary>
public string Name { get; init; }
/// <summary>Feature, static or dynamic.</summary>
public ComboKind Kind { get; init; } = ComboKind.Feature;
/// <summary>Value labels, in order.</summary>
public List<string> Values { get; init; } = new();
/// <summary>Index of the default value.</summary>
public int Default { get; init; }
/// <summary>Group heading in the material editor.</summary>
public string Group { get; init; }
}
/// <summary>One entry of the <c>nodes</c> array.</summary>
public sealed record NodeDto
{
/// <summary>Stable node id, unique within the document.</summary>
public string Id { get; init; }
/// <summary>Stable node type id.</summary>
public string Type { get; init; }
/// <summary>Node-type version, driving per-node migrations.</summary>
public int V { get; init; } = 1;
/// <summary>Grid-snapped scene position as <c>[x, y]</c>.</summary>
public float[] Pos { get; init; }
/// <summary>Explicit card size as <c>[w, h]</c>. Only written when the user resized the node.</summary>
public float[] Size { get; init; }
/// <summary>Node-specific serialized properties, kept as raw JSON so unknown types round-trip.</summary>
public JsonObject Props { get; init; }
/// <summary>Port id to typed literal, for unconnected inputs.</summary>
public JsonObject Inline { get; init; }
/// <summary>Optional per-node state.</summary>
public NodeFlagsDto Flags { get; init; }
/// <summary>Forward-compatibility bag.</summary>
public JsonObject X { get; init; }
}
/// <summary>The <c>flags</c> object of a node.</summary>
public sealed record NodeFlagsDto
{
/// <summary>Render a thumbnail on the card.</summary>
public bool Preview { get; init; }
/// <summary>Draw the card as a header-only strip.</summary>
public bool Collapsed { get; init; }
/// <summary>Exclude from compilation.</summary>
public bool Disabled { get; init; }
/// <summary>Keep in view and exclude from auto-layout.</summary>
public bool Pinned { get; init; }
/// <summary>True when nothing is set, so the whole object can be omitted.</summary>
public bool IsEmpty => !Preview && !Collapsed && !Disabled && !Pinned;
}
/// <summary>One end of an edge.</summary>
public sealed record PortRefDto
{
/// <summary>Node id.</summary>
public string Node { get; init; }
/// <summary>Port id.</summary>
public string Port { get; init; }
}
/// <summary>One entry of the <c>edges</c> array.</summary>
public sealed record EdgeDto
{
/// <summary>Stable edge id.</summary>
public string Id { get; init; }
/// <summary>The producing end.</summary>
public PortRefDto From { get; init; }
/// <summary>The consuming end.</summary>
public PortRefDto To { get; init; }
/// <summary>Optional routing waypoints as <c>[[x, y], ...]</c>.</summary>
public float[][] Via { get; init; }
/// <summary>Per-edge override for the fill value of a padding conversion.</summary>
public float? Fill { get; init; }
}
/// <summary>One entry of the <c>groups</c> array.</summary>
public sealed record GroupDto
{
/// <summary>Stable group id.</summary>
public string Id { get; init; }
/// <summary>Header text.</summary>
public string Title { get; init; }
/// <summary>Body text.</summary>
public string Description { get; init; }
/// <summary>Scene rectangle as <c>[x, y, w, h]</c>.</summary>
public float[] Rect { get; init; }
/// <summary>Named colour.</summary>
public string Color { get; init; }
/// <summary>Stacking order among overlapping groups.</summary>
public int Layer { get; init; }
}
/// <summary>One entry of the <c>notes</c> array.</summary>
public sealed record NoteDto
{
/// <summary>Stable note id.</summary>
public string Id { get; init; }
/// <summary>Scene position as <c>[x, y]</c>.</summary>
public float[] Pos { get; init; }
/// <summary>Size as <c>[w, h]</c>.</summary>
public float[] Size { get; init; }
/// <summary>Named colour.</summary>
public string Color { get; init; }
/// <summary>Note text.</summary>
public string Text { get; init; }
}
/// <summary>The <c>view</c> object.</summary>
public sealed record ViewDto
{
/// <summary>Scene-space centre of the viewport as <c>[x, y]</c>.</summary>
public float[] Center { get; init; }
/// <summary>Zoom factor.</summary>
public float Zoom { get; init; } = 1f;
}
/// <summary>
/// The serializer options every Prism document is read and written with.
/// <para>
/// Deterministic output is a hard requirement: stable key order (declaration order), two-space
/// indent, camelCase keys and enums written as their declared names — the same convention the
/// engine's own resources use.
/// </para>
/// </summary>
public static class PrismJson
{
/// <summary>
/// How deep a document may nest before it is refused.
/// <para>
/// This has to be the <em>same</em> number the reader uses, and it has to be generous, because a
/// forward-compatibility <c>x</c> bag holds arbitrary JSON that a future schema — or another
/// addon — invented. System.Text.Json defaults to 64 on both sides; with a deeper reader than
/// writer, a document would open fine and then fail to save, which is the one failure mode that
/// costs the user their work rather than merely their time.
/// </para>
/// </summary>
public const int MaxDepth = 256;
/// <summary>Options for reading and writing documents.</summary>
public static readonly JsonSerializerOptions Options = Build();
static JsonSerializerOptions Build()
{
var options = new JsonSerializerOptions
{
WriteIndented = true,
IndentCharacter = ' ',
IndentSize = PrismConstants.JsonIndentSize,
MaxDepth = MaxDepth,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
options.Converters.Add( new JsonStringEnumConverter( null, true ) );
// The four id structs are one string each on disk. Without these they round-trip as
// { "value": "p1", "isValid": true }, which contradicts the schema, breaks the legacy importer's
// plain-string props, and hides a parameter reference from the clipboard's id remapper.
options.Converters.Add( new NodeIdJsonConverter() );
options.Converters.Add( new PortIdJsonConverter() );
options.Converters.Add( new EdgeIdJsonConverter() );
options.Converters.Add( new ParamIdJsonConverter() );
return options;
}
}
/// <summary>
/// Reads and writes one of Prism's id structs as a bare JSON string.
/// <para>
/// Tolerant on the way in: a plain string is the canonical form, but an object with a <c>value</c>
/// member is also accepted, which is what a document written before these converters existed contains.
/// </para>
/// </summary>
abstract class IdJsonConverter<T> : JsonConverter<T> where T : struct
{
protected abstract T Parse( string value );
protected abstract string TextOf( T value );
/// <inheritdoc/>
public override T Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
switch ( reader.TokenType )
{
case JsonTokenType.String:
return Parse( reader.GetString() );
case JsonTokenType.Null:
return default;
case JsonTokenType.Number:
return Parse( reader.TryGetInt64( out var number )
? number.ToString( System.Globalization.CultureInfo.InvariantCulture )
: null );
case JsonTokenType.StartObject:
{
var text = (string)null;
var depth = reader.CurrentDepth;
while ( reader.Read() )
{
if ( reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == depth ) break;
if ( reader.TokenType != JsonTokenType.PropertyName ) continue;
var name = reader.GetString();
reader.Read();
if ( string.Equals( name, "value", StringComparison.OrdinalIgnoreCase ) &&
reader.TokenType == JsonTokenType.String )
{
text = reader.GetString();
continue;
}
reader.TrySkip();
}
return Parse( text );
}
default:
reader.TrySkip();
return default;
}
}
/// <inheritdoc/>
public override void Write( Utf8JsonWriter writer, T value, JsonSerializerOptions options ) =>
writer.WriteStringValue( TextOf( value ) ?? string.Empty );
/// <inheritdoc/>
public override T ReadAsPropertyName( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) =>
Parse( reader.GetString() );
/// <inheritdoc/>
public override void WriteAsPropertyName( Utf8JsonWriter writer, T value, JsonSerializerOptions options ) =>
writer.WritePropertyName( TextOf( value ) ?? string.Empty );
}
sealed class NodeIdJsonConverter : IdJsonConverter<NodeId>
{
protected override NodeId Parse( string value ) => NodeId.Parse( value );
protected override string TextOf( NodeId value ) => value.Value;
}
sealed class PortIdJsonConverter : IdJsonConverter<PortId>
{
protected override PortId Parse( string value ) => PortId.Parse( value );
protected override string TextOf( PortId value ) => value.Value;
}
sealed class EdgeIdJsonConverter : IdJsonConverter<EdgeId>
{
protected override EdgeId Parse( string value ) => EdgeId.Parse( value );
protected override string TextOf( EdgeId value ) => value.Value;
}
sealed class ParamIdJsonConverter : IdJsonConverter<ParamId>
{
protected override ParamId Parse( string value ) => ParamId.Parse( value );
protected override string TextOf( ParamId value ) => value.Value;
}