786 lines
32 KiB
C#
786 lines
32 KiB
C#
#nullable enable
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using T3.Core.DataTypes.Vector;
|
|
using T3.Core.Model;
|
|
using T3.Core.Operator;
|
|
using T3.Editor.Gui.OutputUi;
|
|
using T3.Editor.Gui.Windows.Output;
|
|
using T3.Editor.UiModel.InputsAndTypes;
|
|
using T3.Editor.UiModel.Selection;
|
|
using T3.Serialization;
|
|
|
|
// ReSharper disable AssignNullToNotNullAttribute
|
|
|
|
namespace T3.Editor.UiModel;
|
|
|
|
internal static class SymbolUiJson
|
|
{
|
|
internal static void WriteSymbolUi(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
try
|
|
{
|
|
writer.WriteStartObject();
|
|
|
|
writer.WriteValue(JsonKeys.FormatVersion, SymbolFormatVersion.Current);
|
|
writer.WriteValue(JsonKeys.Id, symbolUi.Symbol.Id);
|
|
writer.WriteComment(symbolUi.Symbol.Name);
|
|
|
|
writer.WriteObject(JsonKeys.Description, symbolUi.Description);
|
|
if (symbolUi.Tags != SymbolUi.SymbolTags.None)
|
|
writer.WriteObject(JsonKeys.SymbolTags, (int)symbolUi.Tags); // Writing as bitmask might not be ideal...
|
|
|
|
WriteInputUis(symbolUi, writer);
|
|
WriteChildUis(symbolUi, writer);
|
|
WriteOutputUis(symbolUi, writer);
|
|
WriteSections(symbolUi, writer);
|
|
WriteLinks(symbolUi, writer);
|
|
WriteTourPoints(symbolUi, writer);
|
|
WriteSettings(symbolUi, writer);
|
|
writer.WriteEndObject();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error($"Error writing symbol ui for {symbolUi.Symbol.Name} {symbolUi.Symbol.Id} to json: {e}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private static void WriteInputUis(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
writer.WritePropertyName(JsonKeys.InputUis);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var inputEntry in symbolUi.InputUis.OrderBy(x => x.Key))
|
|
{
|
|
var symbolInput = symbolUi.Symbol.InputDefinitions.SingleOrDefault(inputDef => inputDef.Id == inputEntry.Key);
|
|
if (symbolInput == null)
|
|
{
|
|
Log.Info($"In '{symbolUi.Symbol.Name}': Didn't found input definition for InputUi, skipping this one. This can happen if an input got removed.");
|
|
continue;
|
|
}
|
|
|
|
writer.WriteStartObject(); // input entry
|
|
writer.WriteObject(JsonKeys.InputId, inputEntry.Key);
|
|
writer.WriteComment(symbolInput.Name);
|
|
var inputUi = inputEntry.Value;
|
|
inputUi.Write(writer);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteChildUis(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
writer.WritePropertyName(JsonKeys.SymbolChildUis);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var childUi in symbolUi.ChildUis.Values.OrderBy(x => x.Id))
|
|
{
|
|
// Skip orphaned or invalid child UIs
|
|
if (childUi == null ||
|
|
childUi.SymbolChild == null ||
|
|
!symbolUi.Symbol.Children.TryGetValue(childUi.Id, out var symbolChild))
|
|
{
|
|
Log.Warning($"Skipping UI child {childUi?.Id} in '{symbolUi.Symbol.Name}' because corresponding symbol child is missing or invalid");
|
|
continue;
|
|
}
|
|
|
|
writer.WriteStartObject(); // child entry
|
|
writer.WriteObject(JsonKeys.ChildId, childUi.Id);
|
|
|
|
writer.WriteComment(symbolChild.ReadableName);
|
|
|
|
if (childUi.SectionId != Guid.Empty)
|
|
writer.WriteObject(JsonKeys.SectionId, childUi.SectionId);
|
|
|
|
if (childUi.Style != SymbolUi.Child.Styles.Default)
|
|
{
|
|
writer.WriteObject(JsonKeys.Style, childUi.Style);
|
|
|
|
if (childUi.Size != SymbolUi.Child.DefaultOpSize)
|
|
{
|
|
writer.WritePropertyName(JsonKeys.Size);
|
|
_vector2ToJson(writer, childUi.Size);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(childUi.Comment))
|
|
writer.WriteObject(JsonKeys.Comment, childUi.Comment);
|
|
|
|
writer.WritePropertyName(JsonKeys.Position);
|
|
_vector2ToJson(writer, childUi.PosOnCanvas);
|
|
|
|
if (childUi.SnapshotGroupIndex != 0)
|
|
writer.WriteObject(nameof(SymbolUi.Child.SnapshotGroupIndex), childUi.SnapshotGroupIndex);
|
|
|
|
if (childUi.SnapshotEnabledInputIds != null)
|
|
{
|
|
// Sorted for stable file diffs
|
|
var sortedInputIds = childUi.SnapshotEnabledInputIds.ToList();
|
|
sortedInputIds.Sort();
|
|
|
|
writer.WritePropertyName(nameof(SymbolUi.Child.SnapshotEnabledInputIds));
|
|
writer.WriteStartArray();
|
|
foreach (var inputId in sortedInputIds)
|
|
writer.WriteValue(inputId);
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
// Safely handle connection style overrides
|
|
if (childUi.ConnectionStyleOverrides != null &&
|
|
childUi.ConnectionStyleOverrides.Count > 0)
|
|
{
|
|
writer.WritePropertyName(JsonKeys.ConnectionStyleOverrides);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var kv in childUi.ConnectionStyleOverrides)
|
|
{
|
|
// Skip any malformed entries
|
|
if (kv.Key == Guid.Empty)
|
|
continue;
|
|
|
|
writer.WriteStartObject();
|
|
writer.WriteObject(JsonKeys.Id, kv.Key);
|
|
writer.WriteObject(JsonKeys.Style, kv.Value);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
|
|
|
|
private static void WriteOutputUis(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
writer.WritePropertyName(JsonKeys.OutputUis);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var outputEntry in symbolUi.OutputUis)
|
|
{
|
|
writer.WriteStartObject(); // output entry
|
|
writer.WriteObject(JsonKeys.OutputId, outputEntry.Key);
|
|
var outputName = symbolUi.Symbol.OutputDefinitions.Single(outputDef => outputDef.Id == outputEntry.Key).Name;
|
|
writer.WriteComment(outputName);
|
|
var outputUi = outputEntry.Value;
|
|
writer.WritePropertyName(JsonKeys.Position);
|
|
_vector2ToJson(writer, outputUi.PosOnCanvas);
|
|
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteSections(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
if (symbolUi.Sections.Count == 0)
|
|
return;
|
|
writer.WritePropertyName(JsonKeys.Sections);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var section in symbolUi.Sections.Values.OrderBy(x => x.Id))
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteObject(JsonKeys.Id, section.Id);
|
|
if (!string.IsNullOrEmpty(section.Title))
|
|
{
|
|
writer.WriteObject(JsonKeys.Title, section.Title);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(section.Label))
|
|
{
|
|
writer.WriteObject(JsonKeys.Label, section.Label);
|
|
}
|
|
|
|
if (section.Collapsed)
|
|
writer.WriteObject(JsonKeys.Collapsed, section.Collapsed);
|
|
|
|
if (section.ParentSectionId != Guid.Empty)
|
|
writer.WriteObject(JsonKeys.ParentSectionId, section.ParentSectionId);
|
|
|
|
writer.WritePropertyName(JsonKeys.Color);
|
|
_vector4ToJson(writer, section.Color.Rgba);
|
|
|
|
writer.WritePropertyName(JsonKeys.Position);
|
|
_vector2ToJson(writer, section.PosOnCanvas);
|
|
|
|
writer.WritePropertyName(JsonKeys.Size);
|
|
_vector2ToJson(writer, section.Size);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteLinks(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
if (symbolUi.Links.Count == 0)
|
|
return;
|
|
|
|
writer.WritePropertyName(JsonKeys.Links);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var link in symbolUi.Links.Values.OrderBy(x => x.Id))
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteObject(JsonKeys.Id, link.Id.ToString());
|
|
writer.WriteObject(JsonKeys.Title, link.Title ?? string.Empty);
|
|
writer.WriteObject(JsonKeys.Description, link.Description ?? string.Empty);
|
|
writer.WriteObject(JsonKeys.LinkUrl, link.Url ?? string.Empty);
|
|
writer.WriteObject(JsonKeys.LinkType, link.Type);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static void WriteTourPoints(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
if (symbolUi.TourPoints.Count == 0)
|
|
return;
|
|
|
|
writer.WritePropertyName(JsonKeys.TourPoints);
|
|
writer.WriteStartArray();
|
|
|
|
foreach (var tourPoint in symbolUi.TourPoints)
|
|
{
|
|
writer.WriteStartObject();
|
|
{
|
|
writer.WriteObject(JsonKeys.Id, tourPoint.Id.ToString());
|
|
|
|
if (tourPoint.ChildId != Guid.Empty)
|
|
writer.WriteObject(JsonKeys.ChildId, tourPoint.ChildId.ToString());
|
|
|
|
if (tourPoint.InputId != Guid.Empty)
|
|
writer.WriteObject(JsonKeys.InputId, tourPoint.InputId.ToString());
|
|
|
|
writer.WriteObject(JsonKeys.Description, tourPoint.Description ?? string.Empty);
|
|
|
|
if (tourPoint.Style != TourPoint.Styles.Info)
|
|
writer.WriteObject(JsonKeys.Style, tourPoint.Style);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
internal static bool TryReadSymbolUiExternal(JToken mainObject, Symbol symbol, [NotNullWhen(true)] out SymbolUi? symbolUi)
|
|
{
|
|
symbolUi = null;
|
|
var guidToken = mainObject[JsonKeys.Id];
|
|
if (guidToken == null)
|
|
return false;
|
|
|
|
var guidString = guidToken.Value<string>();
|
|
if (!Guid.TryParse(guidString, out _))
|
|
{
|
|
Log.Warning($"Error parsing guid {guidString}");
|
|
return false;
|
|
}
|
|
|
|
var success = TryReadSymbolUi(mainObject, symbol, out symbolUi);
|
|
if (!success || symbolUi == null)
|
|
return success;
|
|
|
|
symbolUi.UpdateConsistencyWithSymbol(symbol);
|
|
return true;
|
|
}
|
|
|
|
internal static bool TryReadSymbolUi(JToken mainObject, Symbol symbol, [NotNullWhen(true)] out SymbolUi? symbolUi)
|
|
{
|
|
var formatVersion = mainObject[JsonKeys.FormatVersion]?.Value<int>() ?? 0;
|
|
var fileTixlVersion = mainObject[JsonKeys.TixlVersion]?.Value<string>();
|
|
SymbolFormatVersion.WarnIfNewer(formatVersion, fileTixlVersion, symbol.Name);
|
|
|
|
var inputDict = new OrderedDictionary<Guid, IInputUi>();
|
|
if (TryGetJArray(JsonKeys.InputUis, mainObject, symbol, out var inputUiArray))
|
|
{
|
|
foreach (JToken uiInputToken in inputUiArray)
|
|
{
|
|
//inputId = Guid.Parse(uiInputEntry[JsonKeys.InputId].Value<string>() ?? string.Empty);
|
|
|
|
if (!JsonUtils.TryGetGuid(uiInputToken[JsonKeys.InputId], out var inputId))
|
|
{
|
|
Log.Error("Skipping input with missing or invalid id");
|
|
continue;
|
|
}
|
|
|
|
var inputDefinition = symbol.InputDefinitions.SingleOrDefault(def => def.Id == inputId);
|
|
if (inputDefinition == null)
|
|
{
|
|
Log.Warning($"Found input entry in ui file for symbol '{symbol.Name}', but no corresponding input in symbol. " +
|
|
$"Assuming that the input was removed and ignoring the ui information.");
|
|
continue;
|
|
}
|
|
|
|
var type = inputDefinition.DefaultValue.ValueType;
|
|
|
|
// get the symbol input definition
|
|
var inputUi = InputUiFactory.Instance.CreateFor(type);
|
|
inputUi.InputDefinition = inputDefinition;
|
|
inputUi.Read(uiInputToken);
|
|
inputDict.Add(inputId, inputUi);
|
|
}
|
|
}
|
|
|
|
var outputDict = new OrderedDictionary<Guid, IOutputUi>();
|
|
if (TryGetJArray(JsonKeys.OutputUis, mainObject, symbol, out var outputUiArray))
|
|
{
|
|
foreach (var uiOutputToken in outputUiArray)
|
|
{
|
|
if (!JsonUtils.TryGetGuid(uiOutputToken[JsonKeys.OutputId], out var outputId))
|
|
{
|
|
Log.Error("Skipping input with missing or invalid id");
|
|
continue;
|
|
}
|
|
|
|
var outputDefinition = symbol.OutputDefinitions.SingleOrDefault(def => def.Id == outputId);
|
|
if (outputDefinition == null)
|
|
{
|
|
Log.Warning($"Found output entry in ui file for symbol '{symbol.Name}', but no corresponding output in symbol. " +
|
|
$"Assuming that the output was removed and ignoring the ui information.");
|
|
continue;
|
|
}
|
|
|
|
var outputUi = OutputUiFactory.Instance.CreateFor(outputDefinition.ValueType);
|
|
outputUi.OutputDefinition = symbol.OutputDefinitions.First(def => def.Id == outputId);
|
|
outputUi.PosOnCanvas = GetVec2OrDefault(uiOutputToken[JsonKeys.Position]);
|
|
outputDict.Add(outputId, outputUi);
|
|
}
|
|
}
|
|
|
|
var sectionDict = ReadSections(mainObject);
|
|
var linksDict = ReadLinks(mainObject);
|
|
var tourPoints = ReadTourPoints(mainObject);
|
|
|
|
IEnumerable<JToken> symbolChildUiJsonEnumerable;
|
|
if (TryGetJArray(JsonKeys.SymbolChildUis, mainObject, symbol, out var symbolChildUiJson))
|
|
{
|
|
symbolChildUiJsonEnumerable = symbolChildUiJson;
|
|
}
|
|
else
|
|
{
|
|
symbolChildUiJsonEnumerable = Array.Empty<JToken>();
|
|
}
|
|
|
|
symbolUi = new SymbolUi(symbol: symbol,
|
|
childUis: parent => CreateSymbolUiChildren(parent, symbolChildUiJsonEnumerable),
|
|
inputs: inputDict,
|
|
outputs: outputDict,
|
|
sections: sectionDict,
|
|
links: linksDict,
|
|
tourPoints: tourPoints,
|
|
updateConsistency: false);
|
|
|
|
var descriptionEntry = mainObject[JsonKeys.Description];
|
|
if (descriptionEntry?.Value<string>() != null)
|
|
symbolUi.Description = descriptionEntry.Value<string>() ?? string.Empty;
|
|
|
|
var tagsEntry = mainObject[JsonKeys.SymbolTags];
|
|
if (tagsEntry?.Value<int>() != null)
|
|
symbolUi.Tags = (SymbolUi.SymbolTags)tagsEntry.Value<int>();
|
|
|
|
// Ownership is derived state - the serialized ids only preserve membership in
|
|
// collapsed sections, whose invisible bounds are excluded from derivation
|
|
SectionTree.UpdateOwnershipFromGeometry(symbolUi);
|
|
|
|
ReadSettings(mainObject, symbolUi);
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool TryGetJArray(string key, JToken token, Symbol symbol, [NotNullWhen(true)] out JArray? array)
|
|
{
|
|
var exceptionRaised = false;
|
|
array = null;
|
|
|
|
try
|
|
{
|
|
array = (JArray?)token[key];
|
|
if (array == null)
|
|
return false;
|
|
}
|
|
catch
|
|
{
|
|
exceptionRaised = true;
|
|
}
|
|
|
|
if (!exceptionRaised && array != null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Log.Error($"Error parsing {key} array from {symbol}'s {EditorSymbolPackage.SymbolUiExtension} file - invalid format");
|
|
//BlockingWindow.Instance.ShowMessageBox($"Error parsing symbol ui ({EditorSymbolPackage.SymbolUiExtension}) file of {symbol}.\n\n" +
|
|
// $"It will be regenerated next time you save this project.\n\n" +
|
|
// token);
|
|
return false;
|
|
}
|
|
|
|
private static List<SymbolUi.Child> CreateSymbolUiChildren(Symbol parentSymbol, IEnumerable<JToken> childJsons)
|
|
{
|
|
var symbolChildUis = new List<SymbolUi.Child>();
|
|
var symbolId = parentSymbol.Id;
|
|
foreach (var childEntry in childJsons)
|
|
{
|
|
if (!JsonUtils.TryGetGuid(childEntry[JsonKeys.ChildId], out var childId))
|
|
{
|
|
Log.Warning($"Skipping UI child definition in {parentSymbol.Name} {symbolId} for invalid child id");
|
|
continue;
|
|
}
|
|
|
|
if (!parentSymbol.Children.TryGetValue(childId, out var symbolChild))
|
|
{
|
|
foreach (var child in parentSymbol.Children.Values)
|
|
{
|
|
// check for duplication / changed child ids
|
|
if (child.PreviousId.HasValue && child.PreviousId == childId)
|
|
{
|
|
symbolChild = child;
|
|
childId = child.Id;
|
|
child.ClearPreviousId();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (symbolChild == null)
|
|
{
|
|
Log.Warning($"Skipping UI child definition in {parentSymbol.Name} {symbolId} for undefined child {childId}");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var childUi = new SymbolUi.Child(symbolChild.Id, parentSymbol.Id, (EditorSymbolPackage)parentSymbol.SymbolPackage);
|
|
|
|
if (childEntry[JsonKeys.Comment] != null)
|
|
{
|
|
childUi.Comment = childEntry[JsonKeys.Comment]?.Value<string>() ?? string.Empty;
|
|
}
|
|
|
|
var positionToken = childEntry[JsonKeys.Position];
|
|
childUi.PosOnCanvas = GetVec2OrDefault(positionToken);
|
|
|
|
if (childEntry[JsonKeys.Size] != null)
|
|
{
|
|
var sizeToken = childEntry[JsonKeys.Size];
|
|
childUi.Size = GetVec2OrDefault(sizeToken);
|
|
}
|
|
|
|
if (childEntry[nameof(SymbolUi.Child.SnapshotGroupIndex)] != null)
|
|
{
|
|
childUi.SnapshotGroupIndex = (childEntry[nameof(SymbolUi.Child.SnapshotGroupIndex)] ?? -1).Value<int>();
|
|
}
|
|
|
|
// Absent in pre-per-parameter files: null keeps the legacy "all inputs enabled" semantics
|
|
if (childEntry[nameof(SymbolUi.Child.SnapshotEnabledInputIds)] is JArray enabledInputIdsArray)
|
|
{
|
|
var enabledInputIds = new HashSet<Guid>();
|
|
foreach (var idToken in enabledInputIdsArray)
|
|
{
|
|
if (JsonUtils.TryGetGuid(idToken, out var inputId))
|
|
enabledInputIds.Add(inputId);
|
|
}
|
|
|
|
childUi.SnapshotEnabledInputIds = enabledInputIds;
|
|
}
|
|
|
|
childUi.Style = JsonUtils.TryGetEnumValue(childEntry[JsonKeys.Style], out SymbolUi.Child.Styles childStyle)
|
|
? childStyle
|
|
: SymbolUi.Child.Styles.Default;
|
|
|
|
// The legacy AnnotationId marked ops collapsed into a frame - those ops are members of it
|
|
var sectionIdToken = childEntry[JsonKeys.SectionId] ?? childEntry[JsonKeys.LegacyAnnotationId];
|
|
if (JsonUtils.TryGetGuid(sectionIdToken, out var sectionId))
|
|
{
|
|
childUi.SectionId = sectionId;
|
|
}
|
|
|
|
var conStyleEntry = childEntry[JsonKeys.ConnectionStyleOverrides];
|
|
if (conStyleEntry != null)
|
|
{
|
|
var dict = childUi.ConnectionStyleOverrides;
|
|
foreach (var styleEntry in (JArray)conStyleEntry)
|
|
{
|
|
if (!JsonUtils.TryGetGuid(styleEntry[JsonKeys.Id], out var id)
|
|
|| !JsonUtils.TryGetEnumValue(styleEntry[JsonKeys.Style], out SymbolUi.Child.ConnectionStyles style))
|
|
{
|
|
Log.Warning($"Skipping connection style override for invalid id or style");
|
|
continue;
|
|
}
|
|
|
|
dict.Add(id, style);
|
|
}
|
|
}
|
|
|
|
symbolChildUis.Add(childUi);
|
|
}
|
|
|
|
return symbolChildUis;
|
|
}
|
|
|
|
private static OrderedDictionary<Guid, Section> ReadSections(JToken token)
|
|
{
|
|
var sectionDict = new OrderedDictionary<Guid, Section>();
|
|
|
|
var sectionsToken = token[JsonKeys.Sections] ?? token[JsonKeys.LegacyAnnotations];
|
|
if (sectionsToken is not JArray sectionsArray)
|
|
return sectionDict;
|
|
|
|
foreach (var sectionEntry in sectionsArray)
|
|
{
|
|
if (!JsonUtils.TryGetGuid(sectionEntry[JsonKeys.Id], out var id))
|
|
{
|
|
Log.Warning("Skipping section with missing or invalid id");
|
|
continue;
|
|
}
|
|
|
|
var section = new Section
|
|
{
|
|
Id = id,
|
|
Title = sectionEntry[JsonKeys.Title]?.Value<string>() ?? string.Empty,
|
|
Label = sectionEntry[JsonKeys.Label]?.Value<string>() ?? string.Empty,
|
|
PosOnCanvas = GetVec2OrDefault(sectionEntry[JsonKeys.Position]),
|
|
Collapsed = sectionEntry[JsonKeys.Collapsed]?.Value<bool>() ?? false,
|
|
};
|
|
|
|
var colorEntry = sectionEntry[JsonKeys.Color];
|
|
if (colorEntry != null)
|
|
{
|
|
section.Color = new Color((Vector4)_jsonToVector4(colorEntry));
|
|
}
|
|
|
|
if (JsonUtils.TryGetGuid(sectionEntry[JsonKeys.ParentSectionId], out var parentSectionId))
|
|
{
|
|
section.ParentSectionId = parentSectionId;
|
|
}
|
|
|
|
section.Size = GetVec2OrDefault(sectionEntry[JsonKeys.Size]);
|
|
sectionDict[section.Id] = section;
|
|
}
|
|
|
|
return sectionDict;
|
|
}
|
|
|
|
private static OrderedDictionary<Guid, ExternalLink> ReadLinks(JToken token)
|
|
{
|
|
var linkDict = new OrderedDictionary<Guid, ExternalLink>();
|
|
|
|
var linksToken = token[JsonKeys.Links];
|
|
if (linksToken is not JArray linksArray)
|
|
return linkDict;
|
|
|
|
foreach (var linkEntry in linksArray)
|
|
{
|
|
if (!Enum.TryParse<ExternalLink.LinkTypes>(linkEntry[JsonKeys.LinkType]?.Value<string>(), out var type))
|
|
type = ExternalLink.LinkTypes.Other;
|
|
|
|
if (!JsonUtils.TryGetGuid(linkEntry[JsonKeys.Id], out var id))
|
|
{
|
|
Log.Warning("Skipping section with missing or invalid id");
|
|
continue;
|
|
}
|
|
|
|
var link = new ExternalLink
|
|
{
|
|
Id = id,
|
|
Title = linkEntry[JsonKeys.Title]?.Value<string>() ?? string.Empty,
|
|
Description = linkEntry[JsonKeys.Description]?.Value<string>() ?? string.Empty,
|
|
Url = linkEntry[JsonKeys.LinkUrl]?.Value<string>() ?? string.Empty,
|
|
Type = type,
|
|
};
|
|
|
|
linkDict[link.Id] = link;
|
|
}
|
|
|
|
return linkDict;
|
|
}
|
|
|
|
private static List<TourPoint> ReadTourPoints(JToken token)
|
|
{
|
|
var tourPoints = new List<TourPoint>();
|
|
|
|
var tourPointsToken = token[JsonKeys.TourPoints];
|
|
if (tourPointsToken is not JArray tourPointsArray)
|
|
return tourPoints;
|
|
|
|
foreach (var tourPointEntry in tourPointsArray)
|
|
{
|
|
if (!Enum.TryParse<TourPoint.Styles>(tourPointEntry[JsonKeys.Style]?.Value<string>(), out var style))
|
|
style = TourPoint.Styles.Info;
|
|
|
|
if (!JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.Id], out var id))
|
|
{
|
|
Log.Warning("Skipping tour point with missing or invalid id");
|
|
continue;
|
|
}
|
|
|
|
JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.ChildId], out var childId);
|
|
JsonUtils.TryGetGuid(tourPointEntry[JsonKeys.InputId], out var inputId);
|
|
|
|
var description = tourPointEntry[JsonKeys.Description]?.Value<string>() ?? string.Empty;
|
|
if (string.IsNullOrEmpty(description))
|
|
{
|
|
description = tourPointEntry[JsonKeys.Title]?.Value<string>() ?? string.Empty;
|
|
}
|
|
|
|
tourPoints.Add(new TourPoint
|
|
{
|
|
Id = id,
|
|
ChildId = childId,
|
|
InputId = inputId,
|
|
Description = description,
|
|
Style = style,
|
|
});
|
|
}
|
|
|
|
return tourPoints;
|
|
}
|
|
|
|
internal static Vector2 GetVec2OrDefault(JToken? token)
|
|
{
|
|
return token == null ? default : (Vector2)_jsonToVector2(token);
|
|
}
|
|
|
|
private readonly struct JsonKeys
|
|
{
|
|
// Top-level fields (in serialization order)
|
|
public const string FormatVersion = nameof(FormatVersion);
|
|
public const string TixlVersion = nameof(TixlVersion);
|
|
public const string Id = nameof(Id);
|
|
public const string Description = nameof(Description);
|
|
public const string SymbolTags = nameof(SymbolTags);
|
|
public const string InputUis = nameof(InputUis);
|
|
public const string SymbolChildUis = nameof(SymbolChildUis);
|
|
public const string OutputUis = nameof(OutputUis);
|
|
public const string Sections = nameof(Sections);
|
|
public const string Links = nameof(Links);
|
|
public const string TourPoints = nameof(TourPoints);
|
|
// Settings is written by WriteSettings()
|
|
|
|
// InputUi fields
|
|
public const string InputId = nameof(InputId);
|
|
|
|
// OutputUi fields
|
|
public const string OutputId = nameof(OutputId);
|
|
|
|
// ChildUi fields
|
|
public const string ChildId = nameof(ChildId);
|
|
public const string Position = nameof(Position);
|
|
public const string Size = nameof(Size);
|
|
public const string Style = nameof(Style);
|
|
public const string Comment = nameof(Comment);
|
|
public const string ConnectionStyleOverrides = nameof(ConnectionStyleOverrides);
|
|
|
|
// Section fields
|
|
public const string SectionId = nameof(SectionId);
|
|
public const string ParentSectionId = nameof(ParentSectionId);
|
|
|
|
// Back-compat: files saved before the annotation->section rename
|
|
public const string LegacyAnnotations = "Annotations";
|
|
public const string LegacyAnnotationId = "AnnotationId";
|
|
public const string Title = nameof(Title);
|
|
public const string Label = nameof(Label);
|
|
public const string Color = nameof(Color);
|
|
public const string Collapsed = nameof(Collapsed);
|
|
|
|
// Link fields
|
|
public const string LinkType = nameof(LinkType);
|
|
public const string LinkUrl = nameof(LinkUrl);
|
|
}
|
|
|
|
#region Settings
|
|
|
|
private static void WriteSettings(SymbolUi symbolUi, JsonTextWriter writer)
|
|
{
|
|
// RenderSettings, OutputWindowStates and TimelineState are only meaningful for symbols
|
|
// the user marked as a playback root via "Specify settings for ..." in the Composition
|
|
// Settings window. On every other symbol the editor still keeps in-memory state while
|
|
// the user navigates or renders, but writing it would pollute the .t3ui of sub-ops the
|
|
// user never intentionally configured.
|
|
var isComposition = symbolUi.Symbol.CompositionSettings.Enabled;
|
|
var hasRenderSettings = symbolUi.RenderSettings != null && isComposition;
|
|
var hasRecordingSettings = symbolUi.RecordingSettings is { } rec && !rec.IsAtDefault();
|
|
var hasOutputWindowStates = symbolUi.OutputWindowStates is { Count: > 0 } && isComposition;
|
|
var hasTimelineState = symbolUi.TimelineState != null && isComposition;
|
|
var hasWindowLayout = !string.IsNullOrEmpty(symbolUi.WindowLayout);
|
|
|
|
if (!hasRenderSettings && !hasRecordingSettings && !hasOutputWindowStates && !hasTimelineState && !hasWindowLayout)
|
|
return;
|
|
|
|
writer.WritePropertyName("Settings");
|
|
writer.WriteStartObject();
|
|
{
|
|
if (hasRenderSettings)
|
|
symbolUi.RenderSettings!.WriteToJson(writer);
|
|
if (hasRecordingSettings)
|
|
symbolUi.RecordingSettings!.WriteToJson(writer);
|
|
if (hasOutputWindowStates)
|
|
OutputWindowState.WriteAllToJson(writer, symbolUi.OutputWindowStates);
|
|
if (hasTimelineState)
|
|
symbolUi.TimelineState!.WriteToJson(writer);
|
|
|
|
if (hasWindowLayout)
|
|
{
|
|
writer.WritePropertyName("WindowLayout");
|
|
writer.WriteValue(symbolUi.WindowLayout);
|
|
writer.WritePropertyName("WindowLayoutImGuiVersion");
|
|
writer.WriteValue(symbolUi.WindowLayoutImGuiVersion);
|
|
}
|
|
|
|
if (symbolUi.WindowVisibility is { Count: > 0 })
|
|
{
|
|
writer.WritePropertyName("WindowVisibility");
|
|
writer.WriteStartObject();
|
|
foreach (var (title, visible) in symbolUi.WindowVisibility)
|
|
{
|
|
writer.WritePropertyName(title);
|
|
writer.WriteValue(visible);
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static void ReadSettings(JToken mainObject, SymbolUi symbolUi)
|
|
{
|
|
var settingsToken = mainObject["Settings"];
|
|
if (settingsToken == null)
|
|
return;
|
|
|
|
symbolUi.RenderSettings = Gui.Windows.RenderExport.RenderSettings.ReadFromJson(settingsToken);
|
|
symbolUi.RecordingSettings = Gui.Windows.TimeLine.RecordingSettings.ReadFromJson(settingsToken);
|
|
symbolUi.OutputWindowStates = OutputWindowState.ReadAllFromJson(settingsToken);
|
|
symbolUi.TimelineState = Gui.Windows.TimeLine.TimelineState.ReadFromJson(settingsToken);
|
|
symbolUi.WindowLayout = settingsToken["WindowLayout"]?.Value<string>();
|
|
symbolUi.WindowLayoutImGuiVersion = settingsToken["WindowLayoutImGuiVersion"]?.Value<string>();
|
|
|
|
var visibilityToken = settingsToken["WindowVisibility"] as JObject;
|
|
if (visibilityToken != null)
|
|
{
|
|
symbolUi.WindowVisibility = new Dictionary<string, bool>();
|
|
foreach (var prop in visibilityToken.Properties())
|
|
{
|
|
symbolUi.WindowVisibility[prop.Name] = prop.Value.Value<bool>();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static readonly Func<JToken, object> _jsonToVector2 = JsonToTypeValueConverters.Entries[typeof(Vector2)];
|
|
private static readonly Func<JToken, object> _jsonToVector4 = JsonToTypeValueConverters.Entries[typeof(Vector4)];
|
|
private static readonly Action<JsonTextWriter, object> _vector2ToJson = TypeValueToJsonConverters.Entries[typeof(Vector2)];
|
|
private static readonly Action<JsonTextWriter, object> _vector4ToJson = TypeValueToJsonConverters.Entries[typeof(Vector4)];
|
|
} |