345 lines
16 KiB
C#
345 lines
16 KiB
C#
using System.Diagnostics;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using T3.Core.Operator;
|
|
using T3.Core.Operator.Slots;
|
|
using T3.Editor.Compilation;
|
|
using T3.Editor.Gui.InputUi.VectorInputs;
|
|
using T3.Editor.Gui.Interaction.Variations;
|
|
using T3.Editor.Gui.Interaction.Variations.Model;
|
|
using T3.Editor.UiModel.Commands;
|
|
using T3.Editor.UiModel.Commands.Graph;
|
|
using GraphUtils = T3.Editor.UiModel.Helpers.GraphUtils;
|
|
|
|
namespace T3.Editor.UiModel.Modification;
|
|
|
|
internal static class Duplicate
|
|
{
|
|
public static Symbol DuplicateAsNewType(SymbolUi compositionUi, EditableSymbolProject project, Guid symbolId, string newTypeName, string nameSpace,
|
|
string description, Vector2 posOnCanvas)
|
|
{
|
|
var sourceSymbol = EditorSymbolPackage.AllSymbols.FirstOrDefault(x => x.Id == symbolId);
|
|
if (sourceSymbol == null)
|
|
{
|
|
Log.Warning("Can't find symbol to duplicate");
|
|
return null;
|
|
}
|
|
|
|
//var sourceSymbol = symbolChildToDuplicate.Symbol;
|
|
|
|
var syntaxTree = GraphUtils.GetSyntaxTree(sourceSymbol);
|
|
if (syntaxTree == null)
|
|
{
|
|
Log.Error($"Error getting syntax tree from symbol '{sourceSymbol.Name}' source.");
|
|
return null;
|
|
}
|
|
|
|
// create new source on basis of original type
|
|
var root = syntaxTree.GetRoot();
|
|
var classRenamer = new ClassRenameRewriter(newTypeName);
|
|
root = classRenamer.Visit(root);
|
|
|
|
var memberRewriter = new Duplicate.MemberDuplicateRewriter(sourceSymbol.Name, newTypeName);
|
|
|
|
root = memberRewriter.Visit(root);
|
|
var oldToNewIdMap = memberRewriter.OldToNewGuidDict;
|
|
var newSource = root.GetText().ToString();
|
|
|
|
var newSymbolId = Guid.NewGuid();
|
|
newSource = newSource.Replace(sourceSymbol.Namespace, nameSpace);
|
|
newSource = ReplaceGuidAttributeWith(newSymbolId, newSource);
|
|
Log.Debug(newSource);
|
|
|
|
var sourceSymbolUi = sourceSymbol.GetSymbolUi();
|
|
|
|
if (!project.TryCompile(newSource, newTypeName, newSymbolId, nameSpace, out var newSymbol, out _, out var failureLog))
|
|
{
|
|
Log.Error($"Could not compile new symbol '{newTypeName}': {failureLog}");
|
|
return null;
|
|
}
|
|
|
|
var newSymbolUi = sourceSymbolUi.CloneForNewSymbol(newSymbol, oldToNewIdMap);
|
|
newSymbolUi.Description = description;
|
|
newSymbolUi.ReadOnly = false;
|
|
|
|
project.ReplaceSymbolUi(newSymbolUi);
|
|
|
|
// Apply content to a new symbol
|
|
var cmd = new CopySymbolChildrenCommand(sourceSymbolUi,
|
|
null,
|
|
null,
|
|
newSymbolUi,
|
|
Vector2.One);
|
|
cmd.Do();
|
|
cmd.OldToNewChildIds.ToList().ForEach(x => oldToNewIdMap.Add(x.Key, x.Value));
|
|
|
|
// Now copy connection from/to inputs/outputs that are not copied with the command
|
|
// todo: same code as in Symbol.SetInstanceType, factor out common code
|
|
var connectionsToCopy = sourceSymbol.Connections.FindAll(c => c.IsConnectedToSymbolInput || c.IsConnectedToSymbolOutput);
|
|
var connectionEntriesToReplace = new List<ConnectionEntry>(connectionsToCopy.Count);
|
|
foreach (var con in connectionsToCopy)
|
|
{
|
|
var entry = new Duplicate.ConnectionEntry
|
|
{
|
|
Connection = con,
|
|
MultiInputIndex = sourceSymbol.Connections.FindAll(c => c.TargetParentOrChildId == con.TargetParentOrChildId
|
|
&& c.TargetSlotId == con.TargetSlotId)
|
|
// ReSharper disable once PossibleUnintendedReferenceComparison
|
|
.FindIndex(cc => cc == con)
|
|
};
|
|
connectionEntriesToReplace.Add(entry);
|
|
}
|
|
|
|
foreach (var conEntry in connectionEntriesToReplace)
|
|
{
|
|
var conToCopy = conEntry.Connection;
|
|
var isInputConnection = conToCopy.IsConnectedToSymbolInput;
|
|
var newSourceSlotId = isInputConnection ? oldToNewIdMap[conToCopy.SourceSlotId] : conToCopy.SourceSlotId;
|
|
var newSourceId = isInputConnection ? conToCopy.SourceParentOrChildId : oldToNewIdMap[conToCopy.SourceParentOrChildId];
|
|
|
|
var isOutputConnection = conToCopy.IsConnectedToSymbolOutput;
|
|
var newTargetSlotId = isOutputConnection ? oldToNewIdMap[conToCopy.TargetSlotId] : conToCopy.TargetSlotId;
|
|
var newTargetId = isOutputConnection ? conToCopy.TargetParentOrChildId : oldToNewIdMap[conToCopy.TargetParentOrChildId];
|
|
|
|
var newConnection = new Symbol.Connection(newSourceId, newSourceSlotId, newTargetId, newTargetSlotId);
|
|
newSymbol.AddConnection(newConnection, conEntry.MultiInputIndex);
|
|
}
|
|
|
|
// Copy the values of the input of the duplicated type: default values of symbol and the ones in composition context
|
|
foreach (var sourceInputDef in sourceSymbol.InputDefinitions)
|
|
{
|
|
Guid newInputId = oldToNewIdMap[sourceInputDef.Id];
|
|
var correspondingInputDef = newSymbol.InputDefinitions.Find(newInputDef => newInputDef.Id == newInputId);
|
|
correspondingInputDef.DefaultValue = sourceInputDef.DefaultValue.Clone();
|
|
}
|
|
|
|
// Copy the values of the input of the duplicated type: default values of symbol and the ones in composition context
|
|
foreach (var (sourceInputId, sourceInputUi) in sourceSymbolUi.InputUis)
|
|
{
|
|
var newInputId = oldToNewIdMap[sourceInputId];
|
|
if (newSymbolUi.InputUis.TryGetValue(newInputId, out var newInputUi))
|
|
{
|
|
newInputUi.AddPadding = sourceInputUi.AddPadding;
|
|
newInputUi.GroupTitle = sourceInputUi.GroupTitle;
|
|
newInputUi.Description = sourceInputUi.Description;
|
|
newInputUi.ExcludedFromPresets = sourceInputUi.ExcludedFromPresets;
|
|
newInputUi.Relevancy = sourceInputUi.Relevancy;
|
|
|
|
// This is a very unfortunate code, indeed.
|
|
// But implementing this as a generic turned out to be rather tricky
|
|
// because you can't cast to a generic or infer the specific type without
|
|
// adding a non-generic abstract base type.
|
|
switch (sourceInputUi)
|
|
{
|
|
case FloatInputUi srcFloatInput when
|
|
newInputUi is FloatInputUi newFloatInput:
|
|
srcFloatInput.CopyTo(newFloatInput);
|
|
break;
|
|
case Vector2InputUi srcVec2Input when
|
|
newInputUi is Vector2InputUi newVec2Input:
|
|
srcVec2Input.CopyTo(newVec2Input);
|
|
newVec2Input.UseVec2Control = srcVec2Input.UseVec2Control;
|
|
break;
|
|
case Vector3InputUi srcVec3Input when
|
|
newInputUi is Vector3InputUi newVec3Input:
|
|
srcVec3Input.CopyTo(newVec3Input);
|
|
break;
|
|
case Vector4InputUi srcVec4Input when
|
|
newInputUi is Vector4InputUi newVec4Input:
|
|
srcVec4Input.CopyTo(newVec4Input);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create instance
|
|
var addCommand = new AddSymbolChildCommand(compositionUi.Symbol, newSymbol.Id) { PosOnCanvas = posOnCanvas };
|
|
UndoRedoStack.AddAndExecute(addCommand);
|
|
|
|
// Update the positions
|
|
var sourceSelectables = sourceSymbolUi.GetSelectables().ToArray();
|
|
var newSelectables = newSymbolUi.GetSelectables().ToArray();
|
|
Debug.Assert(sourceSelectables.Length == newSelectables.Length);
|
|
for (int i = 0; i < sourceSelectables.Length && i < newSelectables.Length; i++)
|
|
{
|
|
newSelectables[i].PosOnCanvas = sourceSelectables[i].PosOnCanvas; // todo: check if this is enough or if id check needed
|
|
}
|
|
|
|
DuplicateVariations(sourceSymbol.Id, newSymbol.Id, oldToNewIdMap);
|
|
|
|
Log.Debug($"Created new symbol '{newTypeName}'");
|
|
|
|
newSymbolUi.FlagAsModified();
|
|
compositionUi.FlagAsModified();
|
|
project.SaveModifiedSymbols();
|
|
|
|
// Creating a new symbol/assembly can't be cleanly undone (undoing the instance add would
|
|
// orphan the new operator), so drop the history rather than leave it inconsistent.
|
|
UndoRedoStack.Clear();
|
|
|
|
return newSymbol;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the source symbol's presets and snapshots into the new symbol's variation pool.
|
|
/// Presets are keyed by the symbol's own input ids and snapshots by child ids — both were
|
|
/// regenerated during duplication, so they are remapped through the id map. Entries that
|
|
/// don't resolve (stale references in the source variation file) are skipped.
|
|
/// </summary>
|
|
private static void DuplicateVariations(Guid sourceSymbolId, Guid newSymbolId, Dictionary<Guid, Guid> oldToNewIdMap)
|
|
{
|
|
var sourcePool = VariationHandling.GetOrLoadVariations(sourceSymbolId);
|
|
if (sourcePool.AllVariations.Count == 0)
|
|
return;
|
|
|
|
var newPool = VariationHandling.GetOrLoadVariations(newSymbolId);
|
|
|
|
foreach (var variation in sourcePool.AllVariations)
|
|
{
|
|
var newVariation = variation.Clone();
|
|
var remappedSets = new Dictionary<Guid, Dictionary<Guid, InputValue>>(newVariation.ParameterSetsForChildIds.Count);
|
|
|
|
foreach (var (childId, parameterSet) in newVariation.ParameterSetsForChildIds)
|
|
{
|
|
if (childId == Guid.Empty)
|
|
{
|
|
// Preset values reference the duplicated symbol's own inputs.
|
|
var remappedParameters = new Dictionary<Guid, InputValue>(parameterSet.Count);
|
|
foreach (var (inputId, value) in parameterSet)
|
|
{
|
|
if (oldToNewIdMap.TryGetValue(inputId, out var newInputId))
|
|
{
|
|
remappedParameters[newInputId] = value;
|
|
}
|
|
}
|
|
|
|
if (remappedParameters.Count > 0)
|
|
{
|
|
remappedSets[Guid.Empty] = remappedParameters;
|
|
}
|
|
}
|
|
else if (oldToNewIdMap.TryGetValue(childId, out var newChildId))
|
|
{
|
|
// Snapshot values are keyed by the child's own symbol inputs, which are unchanged.
|
|
remappedSets[newChildId] = parameterSet;
|
|
}
|
|
}
|
|
|
|
newVariation.ParameterSetsForChildIds = remappedSets;
|
|
newPool.AddUserVariation(newVariation);
|
|
}
|
|
|
|
newPool.SaveVariationsToFile();
|
|
}
|
|
|
|
private static string ReplaceGuidAttributeWith(Guid newSymbolId, string newSource)
|
|
{
|
|
const string guidTagStart = "Guid(\"";
|
|
int start = newSource.IndexOf(guidTagStart, StringComparison.Ordinal);
|
|
if (start < 0)
|
|
return newSource;
|
|
|
|
start += guidTagStart.Length;
|
|
int end = newSource.IndexOf("\")", start, StringComparison.Ordinal);
|
|
if (end < 0)
|
|
return newSource;
|
|
|
|
var oldGuid = newSource[start..end];
|
|
var newGuid = newSymbolId.ToString();
|
|
return newSource.Replace(oldGuid, newGuid);
|
|
}
|
|
|
|
private sealed class ConnectionEntry
|
|
{
|
|
public Symbol.Connection Connection { get; set; }
|
|
public int MultiInputIndex { get; set; }
|
|
}
|
|
|
|
private sealed class MemberDuplicateRewriter : CSharpSyntaxRewriter
|
|
{
|
|
private readonly string _oldSymbolName;
|
|
private readonly string _newSymbolName;
|
|
|
|
public MemberDuplicateRewriter(string oldSymbolName, string newSymbolName)
|
|
{
|
|
_oldSymbolName = oldSymbolName;
|
|
_newSymbolName = newSymbolName;
|
|
}
|
|
|
|
public override SyntaxNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
|
|
{
|
|
// Only rewrite constructors of the original top-level class
|
|
if (node.Identifier.Text != _oldSymbolName)
|
|
return node;
|
|
|
|
return node.WithIdentifier(
|
|
SyntaxFactory.Identifier(_newSymbolName)
|
|
.WithTriviaFrom(node.Identifier));
|
|
}
|
|
|
|
public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node)
|
|
{
|
|
if (node.Declaration.Type is not GenericNameSyntax nameSyntax)
|
|
return node;
|
|
|
|
var idValue = nameSyntax.Identifier.ValueText;
|
|
|
|
// Only process specific slot types
|
|
if (idValue is not ("InputSlot" or "MultiInputSlot" or "Slot" or "TimeClipSlot" or "TransformCallbackSlot"))
|
|
return node;
|
|
|
|
// Iterate through all attribute lists associated with the field
|
|
foreach (var attrList in node.AttributeLists)
|
|
{
|
|
foreach (var attribute in attrList.Attributes)
|
|
{
|
|
// Check if the attribute has a GUID in it
|
|
var match = _guidRegex.Match(attribute.ToString());
|
|
if (!match.Success)
|
|
continue; // Skip attributes without a GUID
|
|
|
|
var oldGuidString = match.Value;
|
|
if (!Guid.TryParse(oldGuidString, out var oldGuid))
|
|
{
|
|
Log.Debug("Skipping input with inconsistent GUID format: " + node);
|
|
continue;
|
|
}
|
|
|
|
// Generate a new GUID and update the dictionary
|
|
var newGuid = Guid.NewGuid();
|
|
OldToNewGuidDict[oldGuid] = newGuid;
|
|
|
|
// Replace the old GUID with the new one in the attribute's argument list
|
|
if (attribute.ArgumentList != null)
|
|
{
|
|
var updatedArgs = attribute.ArgumentList.ToString().Replace(oldGuidString, newGuid.ToString());
|
|
var newArgList = SyntaxFactory.ParseAttributeArgumentList(updatedArgs);
|
|
|
|
if (newArgList != null)
|
|
{
|
|
// Replace the old argument list with the new one
|
|
node = node.ReplaceNode(attribute.ArgumentList, newArgList);
|
|
}
|
|
else
|
|
{
|
|
Log.Debug("Skipping input with inconsistent argument list: " + node);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Debug("Skipping input without argument list: " + node);
|
|
}
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
private readonly Regex _guidRegex = new(@"(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public Dictionary<Guid, Guid> OldToNewGuidDict { get; } = new(10);
|
|
}
|
|
} |