Files
2026-07-13 13:13:17 +08:00

463 lines
19 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using T3.Core.Logging;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Core.Settings;
using T3.Serialization;
// ReSharper disable AssignNullToNotNullAttribute
namespace T3.Core.Model;
public static class SymbolJson
{
#region writing
public static void WriteSymbol(Symbol symbol, JsonTextWriter writer)
{
writer.WriteStartObject();
writer.WriteValue(JsonKeys.FormatVersion, SymbolFormatVersion.Current);
writer.WriteValue(JsonKeys.Id, symbol.Id);
writer.WriteComment(symbol.Name);
WriteSymbolInputs(symbol.InputDefinitions, writer);
WriteSymbolChildren(symbol.Children.Values.OrderBy(x => x.Id), writer);
WriteConnections(symbol.Connections, writer);
symbol.CompositionSettings?.WriteToJson(writer);
symbol.Animator.Write(writer);
writer.WriteEndObject();
}
private static void WriteSymbolInputs(List<Symbol.InputDefinition> inputs, JsonTextWriter writer)
{
writer.WritePropertyName(JsonKeys.Inputs);
writer.WriteStartArray();
foreach (var input in inputs.OrderBy(x => x.Id))
{
writer.WriteStartObject();
writer.WriteObject(JsonKeys.Id, input.Id);
writer.WriteComment(input.Name);
writer.WritePropertyName(JsonKeys.DefaultValue);
input.DefaultValue.ToJson(writer);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
private static void WriteConnections(List<Symbol.Connection> connections, JsonTextWriter writer)
{
writer.WritePropertyName(JsonKeys.Connections);
writer.WriteStartArray();
foreach (var connection in connections.OrderBy(c => c.TargetParentOrChildId.ToString() + c.TargetSlotId))
{
writer.WriteStartObject();
writer.WriteValue(JsonKeys.SourceParentOrChildId, connection.SourceParentOrChildId);
writer.WriteValue(JsonKeys.SourceSlotId, connection.SourceSlotId);
writer.WriteValue(JsonKeys.TargetParentOrChildId, connection.TargetParentOrChildId);
writer.WriteValue(JsonKeys.TargetSlotId, connection.TargetSlotId);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
private static void WriteSymbolChildren(IEnumerable<Symbol.Child> children, JsonTextWriter writer)
{
writer.WritePropertyName(JsonKeys.Children);
writer.WriteStartArray();
foreach (var child in children)
{
writer.WriteStartObject();
writer.WriteValue(JsonKeys.Id, child.Id);
writer.WriteComment(child.ReadableName);
if (child.IsBypassed)
{
writer.WriteValue(JsonKeys.IsBypassed, child.IsBypassed);
}
writer.WriteValue(JsonKeys.SymbolId, child.Symbol.Id);
if (!string.IsNullOrEmpty(child.Name))
{
writer.WriteObject(JsonKeys.SymbolChildName, child.Name);
}
writer.WritePropertyName(JsonKeys.InputValues);
writer.WriteStartArray();
foreach (var (id, inputValue) in child.Inputs.OrderBy(x => x.Key))
{
if (inputValue.IsDefault)
continue;
writer.WriteStartObject();
writer.WriteValue(JsonKeys.Id, id);
writer.WriteComment(inputValue.Name);
writer.WriteObject(JsonKeys.Type, inputValue.Value.ValueType);
writer.WritePropertyName(JsonKeys.Value);
inputValue.Value.ToJson(writer);
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WritePropertyName(JsonKeys.Outputs);
writer.WriteStartArray();
foreach (var (id, output) in child.Outputs)
{
if (output.DirtyFlagTrigger != output.OutputDefinition.DirtyFlagTrigger || output.OutputData != null || output.IsDisabled)
{
writer.WriteStartObject();
writer.WriteValue(JsonKeys.Id, id);
writer.WriteComment(child.ReadableName);
if (output.OutputData != null)
{
writer.WritePropertyName(JsonKeys.OutputData);
writer.WriteStartObject();
writer.WriteObject(JsonKeys.Type, output.OutputData.DataType);
output.OutputData.ToJson(writer);
writer.WriteEndObject();
}
if (output.DirtyFlagTrigger != output.OutputDefinition.DirtyFlagTrigger)
{
writer.WriteObject(JsonKeys.DirtyFlagTrigger, output.DirtyFlagTrigger);
}
if (output.IsDisabled)
{
writer.WriteValue(JsonKeys.IsDisabled, output.IsDisabled);
}
writer.WriteEndObject();
}
}
writer.WriteEndArray();
writer.WriteEndObject(); // child
}
writer.WriteEndArray();
}
#endregion
#region reading
public static bool TryReadAndApplySymbolChildren(SymbolReadResult symbolReadResult)
{
var childrenJson = symbolReadResult.ChildrenJsonArray;
// if (childrenJson.Length == 0)
// return true;
var parent = symbolReadResult.Symbol;
if (parent == null)
return false;
var result = true;
foreach (var childJson in childrenJson)
{
result &= TryReadSymbolChild(in childJson, parent);
}
if (symbolReadResult.AnimatorJsonData != null)
parent.Animator.Read(symbolReadResult.AnimatorJsonData, parent);
return result;
}
private static bool TryReadSymbolChild(in JsonChildResult childJsonResult, Symbol parent)
{
// If the used symbol hasn't been loaded so far ensure it's loaded now
if (!SymbolRegistry.TryGetSymbol(childJsonResult.SymbolId, out var symbol))
{
Log.Warning($"Error loading symbol child {childJsonResult.SymbolId}");
// Record the miss so the editor won't later overwrite this symbol's file and truncate
// the child (and its connections) that we couldn't resolve here. Same thread per parent,
// so a plain increment is safe.
parent.UnresolvedChildCount++;
return false;
}
var symbolChildJson = childJsonResult.Json;
var name = symbolChildJson[JsonKeys.SymbolChildName]?.Value<string>();
var isBypassed = false;
if (symbolChildJson[JsonKeys.IsBypassed] is { } tt)
isBypassed = tt.Value<bool>();
var modifyAction = new Action<Symbol.Child>(child =>
{
var jInputValueArray = (JArray?)symbolChildJson[JsonKeys.InputValues];
if (jInputValueArray != null)
{
foreach (var inputValue in jInputValueArray)
{
ReadChildInputValue(child, inputValue);
}
}
var jOutputsArray = (JArray?)symbolChildJson[JsonKeys.Outputs];
if (jOutputsArray != null)
{
foreach (var outputJson in jOutputsArray)
{
JsonUtils.TryGetGuid(outputJson[JsonKeys.Id], out var outputId);
var outputDataJson = outputJson[JsonKeys.OutputData];
if (outputDataJson != null)
{
ReadChildOutputData(child, outputId, outputDataJson);
}
var dirtyFlagJson = outputJson[JsonKeys.DirtyFlagTrigger];
if (dirtyFlagJson != null)
{
if(Enum.TryParse<DirtyFlagTrigger>(dirtyFlagJson.Value<string>(), out var x))
child.Outputs[outputId].DirtyFlagTrigger = x;
//(DirtyFlagTrigger)Enum.Parse(typeof(DirtyFlagTrigger), readOnlySpan);
}
var isDisabledJson = outputJson[JsonKeys.IsDisabled];
if (isDisabledJson != null)
{
if (child.Outputs.TryGetValue(outputId, out var output))
output.IsDisabled = isDisabledJson.Value<bool>();
}
}
}
});
parent.AddChild(symbol, childJsonResult.ChildId, name, isBypassed, modifyAction);
return true;
}
private static (Guid, JToken?) ReadSymbolInputDefaults(JToken jsonInput)
{
JsonUtils.TryGetGuid(jsonInput[JsonKeys.Id], out var id);
//var id = Guid.Parse(jsonInput[JsonKeys.Id].Value<string>());
var jsonValue = jsonInput[JsonKeys.DefaultValue];
return (id, jsonValue);
}
private static bool TryReadConnection(JToken jsonConnection, [NotNullWhen(true)] out Symbol.Connection? connection)
{
connection = null;
if (!JsonUtils.TryGetGuid(jsonConnection[JsonKeys.SourceParentOrChildId], out var sourceInstanceId) ||
!JsonUtils.TryGetGuid(jsonConnection[JsonKeys.TargetParentOrChildId], out var targetInstanceId) ||
!JsonUtils.TryGetGuid(jsonConnection[JsonKeys.SourceSlotId], out var sourceSlotId) ||
!JsonUtils.TryGetGuid(jsonConnection[JsonKeys.TargetSlotId], out var targetSlotId))
{
connection = null;
return false;
}
connection = new Symbol.Connection(sourceInstanceId, sourceSlotId, targetInstanceId, targetSlotId);
return true;
}
private static void ReadChildInputValue(Symbol.Child symbolChild, JToken inputJson)
{
JsonUtils.TryGetGuid(inputJson[JsonKeys.Id], out var id);
var jsonValue = inputJson[JsonKeys.Value];
var gotInput = symbolChild.Inputs.TryGetValue(id, out var input);
if (!gotInput || input == null)
{
Log.Warning($"Skipping definition of obsolete input in [{symbolChild.Symbol.Name}]: " + id);
return;
}
try
{
input.Value.SetValueFromJson(jsonValue);
input.IsDefault = false;
}
catch
{
Log.Error($"Failed to read input value ({input.DefaultValue.ValueType}) for {symbolChild.Symbol.Id}: " + jsonValue);
}
}
private static void ReadChildOutputData(Symbol.Child symbolChild, Guid outputId, JToken json)
{
if (json[JsonKeys.Type] == null)
return;
if (!symbolChild.Outputs.TryGetValue(outputId, out var output))
{
Log.Warning("Skipping definition of obsolete output " + outputId);
return;
}
output.OutputData.ReadFromJson(json);
}
public static SymbolReadResult ReadSymbolRoot(in Guid id, JToken jToken, Type instanceType, SymbolPackage package)
{
var formatVersion = jToken[JsonKeys.FormatVersion]?.Value<int>() ?? 0;
var fileTixlVersion = jToken[JsonKeys.TixlVersion]?.Value<string>();
SymbolFormatVersion.WarnIfNewer(formatVersion, fileTixlVersion, instanceType.Name);
// Read symbol with Id -> dictionary of Guid-JToken?
var jChildrenJsonArray = (JArray?)jToken[JsonKeys.Children];
JsonChildResult[] childrenJsons = [];
if (jChildrenJsonArray != null)
{
childrenJsons = new JsonChildResult[jChildrenJsonArray.Count];
for (var i = 0; i < jChildrenJsonArray.Count; i++)
{
childrenJsons[i] = new JsonChildResult(jChildrenJsonArray[i]);
}
}
var connections = new List<Symbol.Connection>();
var connectionsJson = (JArray?)jToken[JsonKeys.Connections];
var hasConnections = false;
if (connectionsJson != null)
{
hasConnections = connectionsJson.Count > 0;
if (hasConnections)
{
ObtainConnections(connectionsJson, connections, instanceType);
}
}
var inputJsonArray = (JArray?)jToken[JsonKeys.Inputs];
var inputDefaults = inputJsonArray?
.Select(ReadSymbolInputDefaults)
.ToArray();
var inputDefaultValueTokens = new Dictionary<Guid, JToken>();
if (inputDefaults != null)
{
foreach (var (inputId,valueToken) in inputDefaults)
{
if(valueToken !=null)
inputDefaultValueTokens[inputId] = valueToken;
}
}
var symbol = package.CreateSymbol(instanceType, id);
if (hasConnections)
symbol.Connections.AddRange(connections);
foreach (var input in symbol.InputDefinitions)
{
// If no entry is present just the value default is used, happens for new inputs
if (inputDefaultValueTokens.TryGetValue(input.Id, out var jsonDefaultValue))
{
input.DefaultValue.SetValueFromJson(jsonDefaultValue);
}
}
// CompositionSettings is never null — symbols without a serialized "ProjectSettings"
// block (e.g. freshly imported custom symbols) get a default, inactive instance.
// The Enabled flag, not nullability, distinguishes "provides settings" from "doesn't".
symbol.CompositionSettings = CompositionSettings.ReadFromJson(jToken) ?? new CompositionSettings();
var animatorJsonData = (JArray?)jToken[JsonKeys.Animator];
return new SymbolReadResult(symbol, childrenJsons, animatorJsonData);
}
private static void ObtainConnections(JArray connectionsJson, List<Symbol.Connection> connections, Type type)
{
foreach (var c in connectionsJson)
{
if (!TryReadConnection(c, out var connection))
{
Log.Warning($"[{type}] Failed to read connection: " + c);
continue;
}
connections.Add(connection);
}
}
#endregion
public readonly struct JsonKeys
{
// Top-level symbol fields (in serialization order)
internal const string FormatVersion = "FormatVersion";
internal const string TixlVersion = "TixlVersion";
public const string Id = "Id";
internal const string Inputs = "Inputs";
internal const string Children = "Children";
internal const string Connections = "Connections";
// SymbolSettings is written by SymbolSettings.WriteToJson()
internal const string Animator = "Animator";
// Connection fields
internal const string SourceParentOrChildId = "SourceParentOrChildId";
internal const string SourceSlotId = "SourceSlotId";
internal const string TargetParentOrChildId = "TargetParentOrChildId";
internal const string TargetSlotId = "TargetSlotId";
// Child fields
internal const string SymbolChildName = "Name";
internal const string SymbolId = "SymbolId";
internal const string InputValues = "InputValues";
internal const string Outputs = "Outputs";
internal const string OutputData = "OutputData";
internal const string Type = "Type";
internal const string IsBypassed = "IsBypassed";
internal const string DirtyFlagTrigger = "DirtyFlagTrigger";
internal const string IsDisabled = "IsDisabled";
// Input fields
internal const string DefaultValue = "DefaultValue";
internal const string Value = "Value";
}
public readonly record struct SymbolReadResult(Symbol? Symbol, JsonChildResult[] ChildrenJsonArray, JArray? AnimatorJsonData);
public readonly struct JsonChildResult
{
internal readonly Guid SymbolId;
internal readonly Guid ChildId;
internal readonly JToken Json;
internal JsonChildResult(JToken json)
{
// todo: handle failure
var idToken = json[JsonKeys.SymbolId];
if (idToken != null)
{
var symbolIdString = idToken.Value<string>();
_ = Guid.TryParse(symbolIdString, out SymbolId);
}
var childIdToken = json[JsonKeys.Id];
if (childIdToken != null)
{
var childIdString = childIdToken.Value<string>();
_ = Guid.TryParse(childIdString, out ChildId);
}
Json = json;
}
}
public static JsonLoadSettings LoadSettings { get; } = new()
{
CommentHandling = CommentHandling.Ignore,
DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error,
LineInfoHandling = LineInfoHandling.Ignore
};
}