Browse Source

Changed keyframe logic to be more generic

flabbet 1 year ago
parent
commit
16b35d69e7
22 changed files with 359 additions and 190 deletions
  1. 9 3
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 0
      src/ChunkyImageLib/ChunkyImageLib.csproj
  3. 30 40
      src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs
  4. 1 0
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserDocumentEx.cs
  5. 15 0
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserV3DocumentEx.cs
  6. 1 0
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  7. 50 0
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  8. 29 10
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs
  9. 30 18
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  10. 33 21
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs
  11. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs
  12. 0 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ICacheable.cs
  13. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  14. 25 89
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  15. 4 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  16. 10 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs
  17. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs
  19. 69 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/SetKeyFrameData_Change.cs
  20. 6 0
      src/PixiEditor.Common/ICacheable.cs
  21. 10 0
      src/PixiEditor.Common/PixiEditor.Common.csproj
  22. 31 0
      src/PixiEditor.sln

+ 9 - 3
src/ChunkyImageLib/ChunkyImage.cs

@@ -4,6 +4,7 @@ using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.Operations;
 using ChunkyImageLib.Operations;
 using OneOf;
 using OneOf;
 using OneOf.Types;
 using OneOf.Types;
+using PixiEditor.Common;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -41,7 +42,7 @@ namespace ChunkyImageLib;
 ///     - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
 ///     - Any other blend mode: the latest chunks contain only the things drawn by the queued operations.
 ///         They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels. 
 ///         They need to be drawn over the committed chunks to obtain the final image. In this case, operations won't have access to the existing pixels. 
 /// </summary>
 /// </summary>
-public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
+public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICacheable
 {
 {
     private struct LatestChunkData
     private struct LatestChunkData
     {
     {
@@ -767,8 +768,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
             EnqueueOperation(operation, new(FindAllChunksOutsideBounds(newSize)));
         }
         }
     }
     }
-    
-    
+
+
     public void EnqueueDrawPaint(Paint paint)
     public void EnqueueDrawPaint(Paint paint)
     {
     {
         lock (lockObject)
         lock (lockObject)
@@ -1409,4 +1410,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable
             return clone;
             return clone;
         }
         }
     }
     }
+
+    public int GetCacheHash()
+    {
+        return commitCounter + queuedOperations.Count;
+    }
 }
 }

+ 1 - 0
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -18,6 +18,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.Common\PixiEditor.Common.csproj" />
     <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
     <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 

+ 30 - 40
src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs

@@ -87,7 +87,7 @@ internal class DocumentViewModelBuilder
 
 
         if (animationData != null && animationData.KeyFrameGroups.Count > 0)
         if (animationData != null && animationData.KeyFrameGroups.Count > 0)
         {
         {
-            BuildKeyFrames(animationData.KeyFrameGroups.Cast<IKeyFrame>().ToList(), AnimationData);
+            BuildKeyFrames(animationData.KeyFrameGroups.ToList(), AnimationData);
         }
         }
 
 
         return this;
         return this;
@@ -110,42 +110,39 @@ internal class DocumentViewModelBuilder
         Graph = graph;
         Graph = graph;
         return this;
         return this;
     }
     }
-    
+
     public DocumentViewModelBuilder WithImageEncoder(string encoder)
     public DocumentViewModelBuilder WithImageEncoder(string encoder)
     {
     {
         ImageEncoderUsed = encoder;
         ImageEncoderUsed = encoder;
         return this;
         return this;
     }
     }
 
 
-    private static void BuildKeyFrames(List<IKeyFrame> root, List<KeyFrameBuilder> data)
+    private static void BuildKeyFrames(List<KeyFrameGroup> root, List<KeyFrameBuilder> data)
     {
     {
-        foreach (var keyFrame in root)
+        foreach (KeyFrameGroup group in root)
         {
         {
-            if (keyFrame is KeyFrameGroup group)
-            {
-                GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
-                    .WithVisibility(group.Enabled)
-                    .WithNodeId(group.NodeId);
+            GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
+                .WithVisibility(group.Enabled)
+                .WithNodeId(group.NodeId);
 
 
-                foreach (var child in group.Children)
+            foreach (var child in group.ChildrenIds)
+            {
+                /*if (child is KeyFrameGroup childGroup)
                 {
                 {
-                    if (child is KeyFrameGroup childGroup)
-                    {
-                        builder.WithChild<GroupKeyFrameBuilder>(x =>
-                            BuildKeyFrames(childGroup.Children, null));
-                    }
-                    else if (child is RasterKeyFrame rasterKeyFrame)
-                    {
-                        builder.WithChild<RasterKeyFrameBuilder>(x => x
-                            .WithVisibility(builder.IsVisible)
-                            .WithLayerGuid(rasterKeyFrame.NodeId)
-                            .WithStartFrame(rasterKeyFrame.StartFrame)
-                            .WithDuration(rasterKeyFrame.Duration));
-                    }
-                }
-
-                data?.Add(builder);
+                    builder.WithChild<GroupKeyFrameBuilder>(x =>
+                        BuildKeyFrames(childGroup.Children, null));
+                }*/
+                /*else if (child is RasterKeyFrame rasterKeyFrame)
+                {
+                    builder.WithChild<RasterKeyFrameBuilder>(x => x
+                        .WithVisibility(builder.IsVisible)
+                        .WithLayerGuid(rasterKeyFrame.NodeId)
+                        .WithStartFrame(rasterKeyFrame.StartFrame)
+                        .WithDuration(rasterKeyFrame.Duration));
+                }*/
             }
             }
+
+            data?.Add(builder);
         }
         }
     }
     }
 
 
@@ -259,20 +256,6 @@ internal class GroupKeyFrameBuilder : KeyFrameBuilder
     public new GroupKeyFrameBuilder WithDuration(int duration) => base.WithDuration(duration) as GroupKeyFrameBuilder;
     public new GroupKeyFrameBuilder WithDuration(int duration) => base.WithDuration(duration) as GroupKeyFrameBuilder;
 }
 }
 
 
-internal class RasterKeyFrameBuilder : KeyFrameBuilder
-{
-    public new RasterKeyFrameBuilder WithVisibility(bool isVisible) =>
-        base.WithVisibility(isVisible) as RasterKeyFrameBuilder;
-
-    public new RasterKeyFrameBuilder WithLayerGuid(int layerId) =>
-        base.WithLayerGuid(layerId) as RasterKeyFrameBuilder;
-
-    public new RasterKeyFrameBuilder WithStartFrame(int startFrame) =>
-        base.WithStartFrame(startFrame) as RasterKeyFrameBuilder;
-
-    public new RasterKeyFrameBuilder WithDuration(int duration) => base.WithDuration(duration) as RasterKeyFrameBuilder;
-}
-
 internal class NodeGraphBuilder
 internal class NodeGraphBuilder
 {
 {
     public List<NodeBuilder> AllNodes { get; set; } = new List<NodeBuilder>();
     public List<NodeBuilder> AllNodes { get; set; } = new List<NodeBuilder>();
@@ -338,6 +321,7 @@ internal class NodeGraphBuilder
         public string Name { get; set; }
         public string Name { get; set; }
         public string UniqueNodeName { get; set; }
         public string UniqueNodeName { get; set; }
         public Dictionary<string, object> InputValues { get; set; }
         public Dictionary<string, object> InputValues { get; set; }
+        public KeyFrameData[] KeyFrames { get; set; }
         public Dictionary<string, object> AdditionalData { get; set; }
         public Dictionary<string, object> AdditionalData { get; set; }
         public Dictionary<int, (string inputPropName, string outputPropName)> InputConnections { get; set; }
         public Dictionary<int, (string inputPropName, string outputPropName)> InputConnections { get; set; }
 
 
@@ -389,5 +373,11 @@ internal class NodeGraphBuilder
 
 
             return this;
             return this;
         }
         }
+
+        public NodeBuilder WithKeyFrames(KeyFrameData[] keyFrames)
+        {
+            KeyFrames = keyFrames;
+            return this;   
+        }
     }
     }
 }
 }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -53,6 +53,7 @@ internal static class PixiParserDocumentEx
                     .WithPosition(node.Position)
                     .WithPosition(node.Position)
                     .WithName(node.Name)
                     .WithName(node.Name)
                     .WithUniqueNodeName(node.UniqueNodeName)
                     .WithUniqueNodeName(node.UniqueNodeName)
+                    .WithKeyFrames(node.KeyFrames)
                     .WithInputValues(ToDictionary(node.InputPropertyValues))
                     .WithInputValues(ToDictionary(node.InputPropertyValues))
                     .WithAdditionalData(node.AdditionalData)
                     .WithAdditionalData(node.AdditionalData)
                     .WithConnections(node.InputConnections));
                     .WithConnections(node.InputConnections));

+ 15 - 0
src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserV3DocumentEx.cs

@@ -350,4 +350,19 @@ internal static class PixiParserV3DocumentEx
             return this;
             return this;
         }
         }
     }
     }
+
+    internal class RasterKeyFrameBuilder : KeyFrameBuilder
+    {
+        public new RasterKeyFrameBuilder WithVisibility(bool isVisible) =>
+            base.WithVisibility(isVisible) as RasterKeyFrameBuilder;
+
+        public new RasterKeyFrameBuilder WithLayerGuid(int layerId) =>
+            base.WithLayerGuid(layerId) as RasterKeyFrameBuilder;
+
+        public new RasterKeyFrameBuilder WithStartFrame(int startFrame) =>
+            base.WithStartFrame(startFrame) as RasterKeyFrameBuilder;
+
+        public new RasterKeyFrameBuilder WithDuration(int duration) =>
+            base.WithDuration(duration) as RasterKeyFrameBuilder;
+    }
 }
 }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -114,6 +114,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IoFileType, Mp4FileType>()
             .AddSingleton<IoFileType, Mp4FileType>()
             // Serialization Factories
             // Serialization Factories
             .AddSingleton<SerializationFactory, SurfaceSerializationFactory>()
             .AddSingleton<SerializationFactory, SurfaceSerializationFactory>()
+            .AddSingleton<SerializationFactory, ChunkyImageSerializationFactory>()
             .AddSingleton<SerializationFactory, KernelSerializationFactory>()
             .AddSingleton<SerializationFactory, KernelSerializationFactory>()
             // Palette Parsers
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()

+ 50 - 0
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs

@@ -0,0 +1,50 @@
+using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Skia;
+using PixiEditor.Numerics;
+using PixiEditor.Parser.Skia;
+
+namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+
+public class ChunkyImageSerializationFactory : SerializationFactory<byte[], ChunkyImage>
+{
+    private static SurfaceSerializationFactory surfaceFactory = new();
+
+    public override byte[] Serialize(ChunkyImage original)
+    {
+        var encoder = Config.Encoder;
+        surfaceFactory.Config = Config;
+
+        Surface surface = new Surface(original.LatestSize);
+        original.DrawMostUpToDateRegionOn(
+            new RectI(0, 0, original.LatestSize.X,
+                original.LatestSize.Y), ChunkResolution.Full, surface.DrawingSurface, new VecI(0, 0), new Paint());
+
+        return surfaceFactory.Serialize(surface);
+    }
+
+    public override bool TryDeserialize(object serialized, out ChunkyImage original)
+    {
+        if (serialized is byte[] imgBytes)
+        {
+            surfaceFactory.Config = Config;
+            if (!surfaceFactory.TryDeserialize(imgBytes, out Surface surface))
+            {
+                original = null;
+                return false;
+            }
+
+            original = new ChunkyImage(surface.Size);
+            original.EnqueueDrawImage(VecI.Zero, surface);
+            original.CommitChanges();
+            return true;
+        }
+
+        original = null;
+        return false;
+    }
+
+    public override string DeserializationId { get; } = "PixiEditor.ChunkyImage";
+}

+ 29 - 10
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -33,16 +33,17 @@ internal partial class DocumentViewModel
 {
 {
     public PixiDocument ToSerializable()
     public PixiDocument ToSerializable()
     {
     {
-        Parser.Graph.NodeGraph graph = new();
+        NodeGraph graph = new();
         ImageEncoder encoder = new QoiEncoder();
         ImageEncoder encoder = new QoiEncoder();
         var doc = Internals.Tracker.Document;
         var doc = Internals.Tracker.Document;
         
         
-        Dictionary<Guid, int> idMap = new();
+        Dictionary<Guid, int> nodeIdMap = new();
+        Dictionary<Guid, int> keyFrameIdMap = new();
 
 
         List<SerializationFactory> factories =
         List<SerializationFactory> factories =
             ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList(); // a bit ugly, sorry
             ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList(); // a bit ugly, sorry
 
 
-        AddNodes(doc.NodeGraph, graph, idMap, new SerializationConfig(encoder), factories);
+        AddNodes(doc.NodeGraph, graph, nodeIdMap, keyFrameIdMap, new SerializationConfig(encoder), factories);
 
 
         var document = new PixiDocument
         var document = new PixiDocument
         {
         {
@@ -54,14 +55,16 @@ internal partial class DocumentViewModel
             PreviewImage =
             PreviewImage =
                 (TryRenderWholeImage(0).Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
                 (TryRenderWholeImage(0).Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
             ReferenceLayer = GetReferenceLayer(doc),
             ReferenceLayer = GetReferenceLayer(doc),
-            AnimationData = ToAnimationData(doc.AnimationData, idMap),
+            AnimationData = ToAnimationData(doc.AnimationData, nodeIdMap),
             ImageEncoderUsed = encoder.EncodedFormatName
             ImageEncoderUsed = encoder.EncodedFormatName
         };
         };
 
 
         return document;
         return document;
     }
     }
 
 
-    private static void AddNodes(IReadOnlyNodeGraph graph, NodeGraph targetGraph, Dictionary<Guid, int> idMap,
+    private static void AddNodes(IReadOnlyNodeGraph graph, NodeGraph targetGraph, 
+        Dictionary<Guid, int> nodeIdMap,
+        Dictionary<Guid, int> keyFrameIdMap,
         SerializationConfig config, IReadOnlyList<SerializationFactory> allFactories)
         SerializationConfig config, IReadOnlyList<SerializationFactory> allFactories)
     {
     {
         targetGraph.AllNodes = new List<Node>();
         targetGraph.AllNodes = new List<Node>();
@@ -69,7 +72,7 @@ internal partial class DocumentViewModel
         int id = 0;
         int id = 0;
         foreach (var node in graph.AllNodes)
         foreach (var node in graph.AllNodes)
         {
         {
-            idMap[node.Id] = id + 1;
+            nodeIdMap[node.Id] = id + 1;
             id++;
             id++;
         }
         }
 
 
@@ -89,6 +92,21 @@ internal partial class DocumentViewModel
             Dictionary<string, object> additionalData = new();
             Dictionary<string, object> additionalData = new();
             node.SerializeAdditionalData(additionalData);
             node.SerializeAdditionalData(additionalData);
 
 
+            KeyFrameData[] keyFrames = new KeyFrameData[node.KeyFrames.Count];
+            
+            for (int i = 0; i < node.KeyFrames.Count; i++)
+            {
+                keyFrameIdMap[node.KeyFrames[i].KeyFrameGuid] = i + 1;
+                keyFrames[i] = new KeyFrameData
+                {
+                    Id = i + 1, 
+                    Data = SerializationUtil.SerializeObject(node.KeyFrames[i].Data, config, allFactories), 
+                    AffectedElement = node.KeyFrames[i].AffectedElement,
+                    StartFrame = node.KeyFrames[i].StartFrame, 
+                    Duration = node.KeyFrames[i].Duration
+                };
+            }
+                
             Dictionary<string, object> converted = ConvertToSerializable(additionalData, config, allFactories);
             Dictionary<string, object> converted = ConvertToSerializable(additionalData, config, allFactories);
 
 
             List<PropertyConnection> connections = new();
             List<PropertyConnection> connections = new();
@@ -99,7 +117,7 @@ internal partial class DocumentViewModel
                 {
                 {
                     connections.Add(new PropertyConnection()
                     connections.Add(new PropertyConnection()
                     {
                     {
-                        OutputNodeId = idMap[inputProp.Connection.Node.Id],
+                        OutputNodeId = nodeIdMap[inputProp.Connection.Node.Id],
                         OutputPropertyName = inputProp.Connection.InternalPropertyName,
                         OutputPropertyName = inputProp.Connection.InternalPropertyName,
                         InputPropertyName = inputProp.InternalPropertyName
                         InputPropertyName = inputProp.InternalPropertyName
                     });
                     });
@@ -108,12 +126,13 @@ internal partial class DocumentViewModel
 
 
             Node parserNode = new Node()
             Node parserNode = new Node()
             {
             {
-                Id = idMap[node.Id],
+                Id = nodeIdMap[node.Id],
                 Name = node.DisplayName,
                 Name = node.DisplayName,
                 UniqueNodeName = node.GetNodeTypeUniqueName(),
                 UniqueNodeName = node.GetNodeTypeUniqueName(),
                 Position = node.Position.ToVector2(),
                 Position = node.Position.ToVector2(),
                 InputPropertyValues = properties,
                 InputPropertyValues = properties,
                 AdditionalData = converted,
                 AdditionalData = converted,
+                KeyFrames = keyFrames,
                 InputConnections = connections.ToArray()
                 InputConnections = connections.ToArray()
             };
             };
 
 
@@ -245,11 +264,11 @@ internal partial class DocumentViewModel
                 new VecI(0, 0));
                 new VecI(0, 0));
         }
         }
 
 
-        group.Children.Add(new RasterKeyFrame()
+        /*group.Children.Add(new RasterKeyFrame()
         {
         {
             NodeId = idMap[rasterKeyFrame.NodeId],
             NodeId = idMap[rasterKeyFrame.NodeId],
             StartFrame = rasterKeyFrame.StartFrame,
             StartFrame = rasterKeyFrame.StartFrame,
             Duration = rasterKeyFrame.Duration,
             Duration = rasterKeyFrame.Duration,
-        });
+        });*/
     }
     }
 }
 }

+ 30 - 18
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -254,7 +254,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         var builderInstance = new DocumentViewModelBuilder();
         var builderInstance = new DocumentViewModelBuilder();
         builder(builderInstance);
         builder(builderInstance);
 
 
-        Dictionary<int, Guid> mappedIds = new();
+        Dictionary<int, Guid> mappedNodeIds = new();
+        Dictionary<int, Guid> mappedKeyFrameIds = new();
 
 
         var viewModel = new DocumentViewModel();
         var viewModel = new DocumentViewModel();
         viewModel.Operations.ResizeCanvas(new VecI(builderInstance.Width, builderInstance.Height), ResizeAnchor.Center);
         viewModel.Operations.ResizeCanvas(new VecI(builderInstance.Width, builderInstance.Height), ResizeAnchor.Center);
@@ -281,10 +282,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         viewModel.Swatches = new ObservableCollection<PaletteColor>(builderInstance.Swatches);
         viewModel.Swatches = new ObservableCollection<PaletteColor>(builderInstance.Swatches);
         viewModel.Palette = new ObservableRangeCollection<PaletteColor>(builderInstance.Palette);
         viewModel.Palette = new ObservableRangeCollection<PaletteColor>(builderInstance.Palette);
 
 
-        SerializationConfig config = new SerializationConfig(BuiltInEncoders.Encoders[builderInstance.ImageEncoderUsed]);
-        
-        List<SerializationFactory> allFactories = ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList();
-        
+        SerializationConfig config =
+            new SerializationConfig(BuiltInEncoders.Encoders[builderInstance.ImageEncoderUsed]);
+
+        List<SerializationFactory> allFactories =
+            ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList();
+
         AddNodes(builderInstance.Graph);
         AddNodes(builderInstance.Graph);
 
 
         if (builderInstance.Graph.AllNodes.Count == 0)
         if (builderInstance.Graph.AllNodes.Count == 0)
@@ -299,8 +302,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         viewModel.MarkAsSaved();
         viewModel.MarkAsSaved();
 
 
         return viewModel;
         return viewModel;
-        
-        
+
+
         void AddNodes(NodeGraphBuilder graph)
         void AddNodes(NodeGraphBuilder graph)
         {
         {
             foreach (var node in graph.AllNodes)
             foreach (var node in graph.AllNodes)
@@ -310,12 +313,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
             foreach (var node in graph.AllNodes)
             foreach (var node in graph.AllNodes)
             {
             {
-                Guid nodeGuid = mappedIds[node.Id];
+                Guid nodeGuid = mappedNodeIds[node.Id];
                 if (node.InputConnections != null)
                 if (node.InputConnections != null)
                 {
                 {
                     foreach (var connection in node.InputConnections)
                     foreach (var connection in node.InputConnections)
                     {
                     {
-                        if (mappedIds.TryGetValue(connection.Key, out Guid outputNodeId))
+                        if (mappedNodeIds.TryGetValue(connection.Key, out Guid outputNodeId))
                         {
                         {
                             acc.AddActions(new ConnectProperties_Action(nodeGuid, outputNodeId,
                             acc.AddActions(new ConnectProperties_Action(nodeGuid, outputNodeId,
                                 connection.Value.inputPropName, connection.Value.outputPropName));
                                 connection.Value.inputPropName, connection.Value.outputPropName));
@@ -328,7 +331,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         void AddNode(int id, NodeGraphBuilder.NodeBuilder serializedNode)
         void AddNode(int id, NodeGraphBuilder.NodeBuilder serializedNode)
         {
         {
             Guid guid = Guid.NewGuid();
             Guid guid = Guid.NewGuid();
-            mappedIds.Add(id, guid);
+            mappedNodeIds.Add(id, guid);
             acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid));
             acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid));
 
 
             if (serializedNode.InputValues != null)
             if (serializedNode.InputValues != null)
@@ -340,9 +343,22 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 }
                 }
             }
             }
 
 
+            if (serializedNode.KeyFrames != null)
+            {
+                foreach (var keyFrame in serializedNode.KeyFrames)
+                {
+                    Guid keyFrameGuid = Guid.NewGuid();
+                    mappedKeyFrameIds.Add(keyFrame.Id, keyFrameGuid);
+                    acc.AddActions(new SetKeyFrameData_Action(guid, keyFrameGuid,
+                        SerializationUtil.Deserialize(keyFrame.Data, config, allFactories), keyFrame.StartFrame,
+                        keyFrame.Duration, keyFrame.AffectedElement));
+                }
+            }
+
             if (serializedNode.AdditionalData != null && serializedNode.AdditionalData.Count > 0)
             if (serializedNode.AdditionalData != null && serializedNode.AdditionalData.Count > 0)
             {
             {
-                acc.AddActions(new DeserializeNodeAdditionalData_Action(guid, SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories)));
+                acc.AddActions(new DeserializeNodeAdditionalData_Action(guid,
+                    SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories)));
             }
             }
 
 
             if (!string.IsNullOrEmpty(serializedNode.Name))
             if (!string.IsNullOrEmpty(serializedNode.Name))
@@ -429,7 +445,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         {
         {
             foreach (var keyFrame in data)
             foreach (var keyFrame in data)
             {
             {
-                if (keyFrame is RasterKeyFrameBuilder rasterKeyFrameBuilder)
+                /*if (keyFrame is RasterKeyFrameBuilder rasterKeyFrameBuilder)
                 {
                 {
                     Guid keyFrameGuid = Guid.NewGuid();
                     Guid keyFrameGuid = Guid.NewGuid();
                     acc.AddActions(
                     acc.AddActions(
@@ -441,14 +457,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                             rasterKeyFrameBuilder.Duration),
                             rasterKeyFrameBuilder.Duration),
                         new EndKeyFrameLength_Action());
                         new EndKeyFrameLength_Action());
 
 
-                    /*PasteImage(rasterKeyFrameBuilder.LayerGuid, rasterKeyFrameBuilder.Surface,
-                        rasterKeyFrameBuilder.Surface.Surface.Size.X,
-                        rasterKeyFrameBuilder.Surface.Surface.Size.Y, 0, 0, false, rasterKeyFrameBuilder.StartFrame,
-                        rasterKeyFrameBuilder.Id);*/
-
                     acc.AddFinishedActions();
                     acc.AddFinishedActions();
                 }
                 }
-                else if (keyFrame is GroupKeyFrameBuilder groupKeyFrameBuilder)
+                else */
+                if (keyFrame is GroupKeyFrameBuilder groupKeyFrameBuilder)
                 {
                 {
                     AddAnimationData(groupKeyFrameBuilder.Children);
                     AddAnimationData(groupKeyFrameBuilder.Children);
                 }
                 }

+ 33 - 21
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs

@@ -1,18 +1,45 @@
-namespace PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.Common;
 
 
-public abstract class KeyFrameData : IDisposable
+namespace PixiEditor.ChangeableDocument.Changeables.Animations;
+
+public class KeyFrameData : IDisposable, IReadOnlyKeyFrameData
 {
 {
     public int StartFrame { get; set; }
     public int StartFrame { get; set; }
     public int Duration { get; set; }
     public int Duration { get; set; }
     public Guid KeyFrameGuid { get; }
     public Guid KeyFrameGuid { get; }
+    public string AffectedElement { get; set; }
+    public object Data { get; set; }
     
     
-    public abstract bool RequiresUpdate { get; set; }
+    private int _lastCacheHash;
 
 
-    public KeyFrameData(Guid keyFrameGuid, int startFrame, int duration)
+    public bool RequiresUpdate
+    {
+        get
+        {
+            if(Data is ICacheable cacheable)
+            {
+                return cacheable.GetCacheHash() != _lastCacheHash;
+            }
+            
+            return false;
+        }
+        set
+        {
+            if (Data is ICacheable cacheable)
+            {
+                _lastCacheHash = cacheable.GetCacheHash();
+            }
+        }
+    }
+
+    public KeyFrameData(Guid keyFrameGuid, int startFrame, int duration, string affectedElement)
     {
     {
         KeyFrameGuid = keyFrameGuid;
         KeyFrameGuid = keyFrameGuid;
         StartFrame = startFrame;
         StartFrame = startFrame;
         Duration = duration;
         Duration = duration;
+        AffectedElement = affectedElement;
     }
     }
 
 
     public bool IsInFrame(int frame)
     public bool IsInFrame(int frame)
@@ -20,26 +47,11 @@ public abstract class KeyFrameData : IDisposable
         return frame >= StartFrame && frame <= StartFrame + Duration;
         return frame >= StartFrame && frame <= StartFrame + Duration;
     }
     }
 
 
-    public abstract void Dispose();
-
-    public abstract object ToSerializable();
-}
-
-public abstract class KeyFrameData<T> : KeyFrameData
-{
-    public T Data { get; set; }
-
-    public KeyFrameData(Guid keyFrameGuid, T data, int startFrame, int duration) : base(keyFrameGuid, startFrame,
-        duration)
-    {
-        Data = data;
-    }
-
-    public override void Dispose()
+    public void Dispose()
     {
     {
         if (Data is IDisposable disposable)
         if (Data is IDisposable disposable)
         {
         {
             disposable.Dispose();
             disposable.Dispose();
-        } 
+        }
     }
     }
 }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/InputProperty.cs

@@ -2,6 +2,7 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+using PixiEditor.Common;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 

+ 0 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ICacheable.cs

@@ -1,6 +0,0 @@
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-
-public interface ICacheable
-{
-    public int GetCacheHash();
-}

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
@@ -10,6 +11,7 @@ public interface IReadOnlyNode
     public Guid Id { get; }
     public Guid Id { get; }
     public IReadOnlyList<IInputProperty> InputProperties { get; }
     public IReadOnlyList<IInputProperty> InputProperties { get; }
     public IReadOnlyList<IOutputProperty> OutputProperties { get; }
     public IReadOnlyList<IOutputProperty> OutputProperties { get; }
+    public IReadOnlyList<IReadOnlyKeyFrameData> KeyFrames { get; }
     public VecD Position { get; }
     public VecD Position { get; }
     public Surface? CachedResult { get; }
     public Surface? CachedResult { get; }
     string DisplayName { get; }
     string DisplayName { get; }

+ 25 - 89
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -13,10 +13,12 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 {
 {
     public const string ImageFramesKey = "Frames";
     public const string ImageFramesKey = "Frames";
+    public const string ImageLayerKey = "LayerImage";
 
 
     public InputProperty<bool> LockTransparency { get; }
     public InputProperty<bool> LockTransparency { get; }
 
 
     private VecI size;
     private VecI size;
+    private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
 
     private static readonly Paint clearPaint = new()
     private static readonly Paint clearPaint = new()
     {
     {
@@ -34,7 +36,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public ImageLayerNode(VecI size)
     public ImageLayerNode(VecI size)
     {
     {
         LockTransparency = CreateInput<bool>("LockTransparency", "LOCK_TRANSPARENCY", false);
         LockTransparency = CreateInput<bool>("LockTransparency", "LOCK_TRANSPARENCY", false);
-        keyFrames.Add(new ImageFrame(Guid.NewGuid(), 0, 0, new(size)));
+
+        if (keyFrames.Count == 0)
+        {
+            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size) });
+        }
+
         this.size = size;
         this.size = size;
     }
     }
 
 
@@ -51,12 +58,12 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             return Output.Value;
             return Output.Value;
         }
         }
 
 
-        var frameImage = GetFrameImage(context.FrameTime).Data;
+        var frameImage = GetFrameWithImage(context.FrameTime);
 
 
         blendPaint.Color = new Color(255, 255, 255, 255);
         blendPaint.Color = new Color(255, 255, 255, 255);
         blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
         blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
 
 
-        var renderedSurface = RenderImage(frameImage, context);
+        var renderedSurface = RenderImage(frameImage.Data as ChunkyImage, context);
 
 
         Output.Value = renderedSurface;
         Output.Value = renderedSurface;
 
 
@@ -116,28 +123,28 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         }
         }
     }
     }
 
 
-    private ImageFrame GetFrameImage(KeyFrameTime frame)
+    private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     {
     {
         var imageFrame = keyFrames.LastOrDefault(x => x.IsInFrame(frame.Frame));
         var imageFrame = keyFrames.LastOrDefault(x => x.IsInFrame(frame.Frame));
-        if (imageFrame is not ImageFrame)
+        if (imageFrame?.Data is not ChunkyImage)
         {
         {
-            return keyFrames[0] as ImageFrame;
+            return keyFrames[0];
         }
         }
 
 
-        var frameImage = imageFrame ?? keyFrames[0];
-        return frameImage as ImageFrame;
+        var frameImage = imageFrame;
+        return frameImage;
     }
     }
 
 
     protected override bool CacheChanged(RenderingContext context)
     protected override bool CacheChanged(RenderingContext context)
     {
     {
-        var frame = GetFrameImage(context.FrameTime);
+        var frame = GetFrameWithImage(context.FrameTime);
         return base.CacheChanged(context) || frame?.RequiresUpdate == true;
         return base.CacheChanged(context) || frame?.RequiresUpdate == true;
     }
     }
 
 
     protected override void UpdateCache(RenderingContext context)
     protected override void UpdateCache(RenderingContext context)
     {
     {
         base.UpdateCache(context);
         base.UpdateCache(context);
-        var imageFrame = GetFrameImage(context.FrameTime);
+        var imageFrame = GetFrameWithImage(context.FrameTime);
         if (imageFrame is not null && imageFrame.RequiresUpdate)
         if (imageFrame is not null && imageFrame.RequiresUpdate)
         {
         {
             imageFrame.RequiresUpdate = false;
             imageFrame.RequiresUpdate = false;
@@ -152,47 +159,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
             keyFrames = new List<KeyFrameData>()
             keyFrames = new List<KeyFrameData>()
             {
             {
                 // we are only copying the layer image, keyframes probably shouldn't be copied since they are controlled by AnimationData
                 // we are only copying the layer image, keyframes probably shouldn't be copied since they are controlled by AnimationData
-                new ImageFrame(Guid.NewGuid(), 0, 0, ((ImageFrame)keyFrames[0]).Data.CloneFromCommitted())
+                new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = layerImage.CloneFromCommitted() }
             }
             }
         };
         };
     }
     }
 
 
-    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
-    {
-        if (keyFrames is not null)
-        {
-            List<object> serializedFrames = new();
-            foreach (var frame in keyFrames)
-            {
-                serializedFrames.Add(frame.ToSerializable());
-            }
-
-            additionalData.Add(ImageFramesKey, serializedFrames);
-        }
-    }
-
-    internal override void DeserializeData(IReadOnlyDictionary<string, object> data)
-    {
-        if (data.TryGetValue(ImageFramesKey, out var frames))
-        {
-            using Paint paint = new Paint();
-            if (frames is not IEnumerable<Surface> list)
-            {
-                throw new InvalidOperationException("Key frames data is not in correct format.");
-            }
-
-            keyFrames.Clear();
-            foreach (var frame in list)
-            {
-                ChunkyImage image = new ChunkyImage(size);
-                image.EnqueueDrawImage(VecI.Zero, frame, paint);
-                image.CommitChanges();
-                
-                frame.Dispose();
-                keyFrames.Add(new ImageFrame(Guid.NewGuid(), 0, 0, image));
-            }
-        }
-    }
 
 
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
     IReadOnlyChunkyImage IReadOnlyImageNode.GetLayerImageAtFrame(int frame) => GetLayerImageAtFrame(frame);
 
 
@@ -215,16 +186,16 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     {
     {
         foreach (var frame in keyFrames)
         foreach (var frame in keyFrames)
         {
         {
-            if (frame is ImageFrame imageFrame)
+            if (frame.Data is ChunkyImage imageFrame)
             {
             {
-                action(imageFrame.Data);
+                action(imageFrame);
             }
             }
         }
         }
     }
     }
 
 
     public ChunkyImage GetLayerImageAtFrame(int frame)
     public ChunkyImage GetLayerImageAtFrame(int frame)
     {
     {
-        return GetFrameImage(frame).Data;
+        return GetFrameWithImage(frame).Data as ChunkyImage;
     }
     }
 
 
     public ChunkyImage GetLayerImageByKeyFrameGuid(Guid keyFrameGuid)
     public ChunkyImage GetLayerImageByKeyFrameGuid(Guid keyFrameGuid)
@@ -233,55 +204,20 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         {
         {
             if (keyFrame.KeyFrameGuid == keyFrameGuid)
             if (keyFrame.KeyFrameGuid == keyFrameGuid)
             {
             {
-                return (keyFrame as ImageFrame).Data;
+                return keyFrame.Data as ChunkyImage;
             }
             }
         }
         }
 
 
-        return (keyFrames[0] as ImageFrame).Data;
+        return layerImage;
     }
     }
 
 
     public void SetLayerImageAtFrame(int frame, ChunkyImage newLayerImage)
     public void SetLayerImageAtFrame(int frame, ChunkyImage newLayerImage)
     {
     {
         var existingFrame = keyFrames.FirstOrDefault(x => x.IsInFrame(frame));
         var existingFrame = keyFrames.FirstOrDefault(x => x.IsInFrame(frame));
-        if (existingFrame is not null && existingFrame is ImageFrame imgFrame)
+        if (existingFrame is not null && existingFrame.Data is ChunkyImage)
         {
         {
             existingFrame.Dispose();
             existingFrame.Dispose();
-            imgFrame.Data = newLayerImage;
-        }
-    }
-}
-
-class ImageFrame : KeyFrameData<ChunkyImage>
-{
-    private int lastCommitCounter = 0;
-
-    public override bool RequiresUpdate
-    {
-        get
-        {
-            return Data.QueueLength != lastQueueLength || Data.CommitCounter != lastCommitCounter;
+            existingFrame.Data = newLayerImage;
         }
         }
-        set
-        {
-            lastQueueLength = Data.QueueLength;
-            lastCommitCounter = Data.CommitCounter;
-        }
-    }
-
-    public override object ToSerializable()
-    {
-        Surface surface = new Surface(Data.LatestSize);
-        Data.DrawMostUpToDateRegionOn(
-            new RectI(0, 0, Data.LatestSize.X,
-                Data.LatestSize.Y), ChunkResolution.Full, surface.DrawingSurface, new VecI(0, 0), new Paint());
-
-        return surface;
-    }
-
-    private int lastQueueLength = 0;
-
-    public ImageFrame(Guid keyFrameGuid, int startFrame, int duration, ChunkyImage image) : base(keyFrameGuid, image,
-        startFrame, duration)
-    {
     }
     }
 }
 }

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -3,6 +3,7 @@ using System.Reflection;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
@@ -20,6 +21,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     public IReadOnlyList<InputProperty> InputProperties => inputs;
     public IReadOnlyList<InputProperty> InputProperties => inputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
     public IReadOnlyList<OutputProperty> OutputProperties => outputs;
+    public IReadOnlyList<KeyFrameData> KeyFrames => keyFrames; 
 
 
     public Surface? CachedResult
     public Surface? CachedResult
     {
     {
@@ -46,6 +48,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyList<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
+    IReadOnlyList<IReadOnlyKeyFrameData> IReadOnlyNode.KeyFrames => keyFrames;
     public VecD Position { get; set; }
     public VecD Position { get; set; }
     public abstract string DisplayName { get; set; }
     public abstract string DisplayName { get; set; }
 
 
@@ -181,7 +184,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
         }
     }
     }
 
 
-    public void AddFrame<T>(Guid id, T value) where T : KeyFrameData
+    public void AddFrame(Guid id, KeyFrameData value) 
     {
     {
         if (keyFrames.Any(x => x.KeyFrameGuid == id))
         if (keyFrames.Any(x => x.KeyFrameGuid == id))
         {
         {

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface IReadOnlyKeyFrameData
+{
+    int StartFrame { get; }
+    int Duration { get; }
+    Guid KeyFrameGuid { get; }
+    object Data { get; }
+    string AffectedElement { get; }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

@@ -44,7 +44,7 @@ internal class CreateRasterKeyFrame_Change : Change
         var keyFrame =
         var keyFrame =
             new RasterKeyFrame(createdKeyFrameId, targetNode, _frame, target, img);
             new RasterKeyFrame(createdKeyFrameId, targetNode, _frame, target, img);
         
         
-        targetNode.AddFrame(createdKeyFrameId, new ImageFrame(createdKeyFrameId, _frame, 1, img));
+        targetNode.AddFrame(createdKeyFrameId, new KeyFrameData(createdKeyFrameId, _frame, 1, ImageLayerNode.ImageLayerKey) { Data = img });
         target.AnimationData.AddKeyFrame(keyFrame);
         target.AnimationData.AddKeyFrame(keyFrame);
         ignoreInUndo = false;
         ignoreInUndo = false;
         return new CreateRasterKeyFrame_ChangeInfo(_targetLayerGuid, _frame, createdKeyFrameId, cloneFrom.HasValue);
         return new CreateRasterKeyFrame_ChangeInfo(_targetLayerGuid, _frame, createdKeyFrameId, cloneFrom.HasValue);

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/DeserializeNodeAdditionalData_Change.cs → src/PixiEditor.ChangeableDocument/Changes/NodeGraph/DeserializeNodeAdditionalData_Change.cs

@@ -1,6 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
-namespace PixiEditor.ChangeableDocument.Changes;
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
 
 internal class DeserializeNodeAdditionalData_Change : Change
 internal class DeserializeNodeAdditionalData_Change : Change
 {
 {

+ 69 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/SetKeyFrameData_Change.cs

@@ -0,0 +1,69 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class SetKeyFrameData_Change : Change
+{
+    private Guid nodeId;
+    private Guid keyFrameId;
+    private object data;
+    private int startFrame;
+    private int duration;
+    private string affectedElement;
+    
+    [GenerateMakeChangeAction]
+    public SetKeyFrameData_Change(Guid nodeId, Guid keyFrameId, object data, int startFrame, int duration, string affectedElement)
+    {
+        this.nodeId = nodeId;
+        this.keyFrameId = keyFrameId;
+        this.data = data;
+        this.startFrame = startFrame;
+        this.duration = duration;
+        this.affectedElement = affectedElement;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindNode(nodeId, out Node node);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        Node node = target.FindNode(nodeId);
+
+        KeyFrameData keyFrame = node.KeyFrames.FirstOrDefault(
+            x => x.KeyFrameGuid == keyFrameId 
+                 || IsSpecialRootKeyFrame(x)); 
+        
+        if (keyFrame is null)
+        {
+            keyFrame = new KeyFrameData(keyFrameId, startFrame, duration, affectedElement);
+        }
+        
+        keyFrame.Data = data;
+        keyFrame.StartFrame = startFrame;
+        keyFrame.Duration = duration;
+        keyFrame.AffectedElement = affectedElement;
+        
+        if (!node.KeyFrames.Contains(keyFrame))
+        {
+            node.AddFrame(keyFrameId, keyFrame);
+        }
+        
+        ignoreInUndo = false;
+        
+        return new None();
+    }
+
+    private bool IsSpecialRootKeyFrame(KeyFrameData x)
+    {
+        return (x is { StartFrame: 0, Duration: 0 } && startFrame == 0 && duration == 0 && x.AffectedElement == affectedElement);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        throw new InvalidOperationException("Cannot revert SetKeyFrameData_Change, this change is only meant for setting key frame data.");
+        return new None(); // do not remove, code generator doesn't work without it 
+    }
+}

+ 6 - 0
src/PixiEditor.Common/ICacheable.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Common;
+
+public interface ICacheable
+{
+    public int GetCacheHash();
+}

+ 10 - 0
src/PixiEditor.Common/PixiEditor.Common.csproj

@@ -0,0 +1,10 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <RootNamespace>PixiEditor.Common</RootNamespace>
+    </PropertyGroup>
+
+</Project>

+ 31 - 0
src/PixiEditor.sln

@@ -106,6 +106,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser", "..\..\PixiPar
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser.Skia", "..\..\PixiParser\src\PixiParser.Skia\PixiParser.Skia.csproj", "{F355C56A-3E47-40C0-8204-002233CEFE23}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser.Skia", "..\..\PixiParser\src\PixiParser.Skia\PixiParser.Skia.csproj", "{F355C56A-3E47-40C0-8204-002233CEFE23}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Common", "PixiEditor.Common\PixiEditor.Common.csproj", "{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
 		Debug|x64 = Debug|x64
@@ -1560,6 +1562,34 @@ Global
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|x64.Build.0 = Debug|Any CPU
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|x64.Build.0 = Debug|Any CPU
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{F355C56A-3E47-40C0-8204-002233CEFE23}.Steam|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Debug|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Release|x64.ActiveCfg = Release|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Release|x64.Build.0 = Release|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Release|ARM64.Build.0 = Release|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|x64.Build.0 = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -1606,6 +1636,7 @@ Global
 		{2BA72059-FFD7-4887-AE88-269017198933} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{2BA72059-FFD7-4887-AE88-269017198933} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{9B552A44-9587-4410-8673-254B31E2E4F7} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{9B552A44-9587-4410-8673-254B31E2E4F7} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C} = {2BA72059-FFD7-4887-AE88-269017198933}
+		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}