Browse Source

backend frame rearrangement

flabbet 1 year ago
parent
commit
ed1e56a6ea
22 changed files with 243 additions and 65 deletions
  1. 10 1
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  2. 2 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs
  3. 1 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IKeyFrameGroupHandler.cs
  4. 4 3
      src/PixiEditor.AvaloniaUI/Models/Handlers/IKeyFrameHandler.cs
  5. 1 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/IRasterKeyFrameHandler.cs
  6. 4 4
      src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml
  7. 10 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs
  8. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  9. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameCollection.cs
  10. 6 4
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs
  11. 68 14
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs
  12. 5 3
      src/PixiEditor.AvaloniaUI/ViewModels/Document/RasterKeyFrameViewModel.cs
  13. 2 7
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs
  14. 24 11
      src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs
  15. 2 2
      src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs
  16. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/KeyFrameLength_ChangeInfo.cs
  17. 5 5
      src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrame.cs
  19. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyAnimationData.cs
  20. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/ActiveFrame_UpdateableChange.cs
  21. 9 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  22. 80 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFrameLength_UpdateableChange.cs

+ 10 - 1
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -136,6 +136,9 @@ internal class DocumentUpdater
             case ActiveFrame_ChangeInfo info:
                 ProcessActiveFrame(info);
                 break;
+            case KeyFrameLength_ChangeInfo info:
+                ProcessKeyFrameLength(info);
+                break;
         }
     }
 
@@ -405,7 +408,8 @@ internal class DocumentUpdater
     
     private void ProcessCreateRasterKeyFrame(CreateRasterKeyFrame_ChangeInfo info)
     {
-        doc.AnimationHandler.AddKeyFrame(new RasterKeyFrameViewModel(info.TargetLayerGuid, info.Frame, 1, info.KeyFrameId));
+        doc.AnimationHandler.AddKeyFrame(new RasterKeyFrameViewModel(info.TargetLayerGuid, info.Frame, 1, info.KeyFrameId, 
+            (DocumentViewModel)doc, helper));
     }
     
     private void ProcessDeleteKeyFrame(DeleteKeyFrame_ChangeInfo info)
@@ -417,4 +421,9 @@ internal class DocumentUpdater
     {
         doc.AnimationHandler.SetActiveFrame(info.ActiveFrame);
     }
+    
+    private void ProcessKeyFrameLength(KeyFrameLength_ChangeInfo info)
+    {
+        doc.AnimationHandler.SetFrameLength(info.KeyFrameGuid, info.StartFrame, info.Duration);
+    }
 }

+ 2 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs

@@ -1,11 +1,12 @@
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface IAnimationHandler
+internal interface IAnimationHandler
 {
     public IReadOnlyCollection<IKeyFrameHandler> KeyFrames { get; }
     public int ActiveFrameBindable { get; set; }
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, bool cloneFromExisting);
     public void SetActiveFrame(int newFrame);
+    public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);
     public bool FindKeyFrame<T>(Guid guid, out T keyFrameHandler) where T : IKeyFrameHandler;
     internal void AddKeyFrame(IKeyFrameHandler keyFrame);
     internal void RemoveKeyFrame(Guid keyFrameId);

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IKeyFrameGroupHandler.cs

@@ -3,7 +3,7 @@ using ChunkyImageLib;
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface IKeyFrameGroupHandler : IKeyFrameHandler
+internal interface IKeyFrameGroupHandler : IKeyFrameHandler
 {
     public ObservableCollection<IKeyFrameHandler> Children { get; }
 }

+ 4 - 3
src/PixiEditor.AvaloniaUI/Models/Handlers/IKeyFrameHandler.cs

@@ -2,11 +2,12 @@
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface IKeyFrameHandler
+internal interface IKeyFrameHandler
 {
     public Surface? PreviewSurface { get; set; }
-    public int StartFrame { get; }
-    public int Duration { get; }
+    public int StartFrameBindable { get; }
+    public int DurationBindable { get; }
     public Guid LayerGuid { get; }
     public Guid Id { get; }
+    public IDocument Document { get; }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/IRasterKeyFrameHandler.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
-public interface IRasterKeyFrameHandler : IKeyFrameHandler
+internal interface IRasterKeyFrameHandler : IKeyFrameHandler
 {
 
 }

+ 4 - 4
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -11,7 +11,7 @@
     <Design.PreviewWith>
         <animations:Timeline>
             <animations:Timeline.KeyFrames>
-                <document:RasterKeyFrameViewModel Duration="100" />
+                <document:RasterKeyFrameViewModel DurationBindable="100" />
             </animations:Timeline.KeyFrames>
         </animations:Timeline>
     </Design.PreviewWith>
@@ -83,7 +83,7 @@
                                         <Setter Property="ClipToBounds" Value="False" />
                                         <Setter Property="Margin">
                                             <MultiBinding Converter="{converters:DurationToMarginConverter}">
-                                                <Binding Path="DataContext.StartFrame" RelativeSource="{RelativeSource Self}" />
+                                                <Binding Path="DataContext.StartFrameBindable" RelativeSource="{RelativeSource Self}" />
                                                 <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" Path="Scale" />
                                             </MultiBinding>
                                         </Setter>
@@ -113,7 +113,7 @@
                                     <TreeDataTemplate DataType="document:KeyFrameGroupViewModel"
                                                       ItemsSource="{Binding Children}">
                                         <Grid Width="200">
-                                            <TextBlock Text="{Binding StartFrame}" />
+                                            <TextBlock Text="{Binding StartFrameBindable}" />
                                         </Grid>
                                     </TreeDataTemplate>
                                     <DataTemplate DataType="document:RasterKeyFrameViewModel">
@@ -122,7 +122,7 @@
                                                              Item="{Binding}">
                                             <animations:KeyFrame.Width>
                                                 <MultiBinding Converter="{converters:DurationToWidthConverter}">
-                                                    <Binding Path="Duration" />
+                                                    <Binding Path="DurationBindable" />
                                                     <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" Path="Scale" />
                                                 </MultiBinding>
                                             </animations:KeyFrame.Width>

+ 10 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -51,6 +51,15 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         OnPropertyChanged(nameof(ActiveFrameBindable));
     }
 
+    public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration)
+    {
+        if(TryFindKeyFrame(keyFrameId, out KeyFrameViewModel keyFrame))
+        {
+            keyFrame.SetStartFrame(newStartFrame);
+            keyFrame.SetDuration(newDuration);
+        }
+    }
+
     public void AddKeyFrame(IKeyFrameHandler keyFrame)
     {
         Guid id = keyFrame.LayerGuid;
@@ -61,7 +70,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         else
         {
             KeyFrameGroupViewModel createdGroup =
-                new KeyFrameGroupViewModel(keyFrame.StartFrame, keyFrame.Duration, id, id);
+                new KeyFrameGroupViewModel(keyFrame.StartFrameBindable, keyFrame.DurationBindable, id, id, Document, Internals);
             createdGroup.Children.Add((KeyFrameViewModel)keyFrame);
             keyFrames.Add(createdGroup);
         }

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -701,8 +701,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
 
         var keyFrames = AnimationDataViewModel.KeyFrames;
-        var firstFrame = keyFrames.Min(x => x.StartFrame);
-        var lastFrame = keyFrames.Max(x => x.StartFrame + x.Duration);
+        var firstFrame = keyFrames.Min(x => x.StartFrameBindable);
+        var lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
 
         int activeFrame = AnimationDataViewModel.ActiveFrameBindable;
         

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameCollection.cs

@@ -5,7 +5,7 @@ using PixiEditor.AvaloniaUI.Models.Handlers;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
-public class KeyFrameCollection : ObservableCollection<KeyFrameViewModel>
+internal class KeyFrameCollection : ObservableCollection<KeyFrameViewModel>
 {
     public KeyFrameCollection()
     {
@@ -19,6 +19,6 @@ public class KeyFrameCollection : ObservableCollection<KeyFrameViewModel>
 
     public int FrameCount
     {
-        get => Items.Count == 0 ? 0 : Items.Max(x => x.StartFrame + x.Duration);
+        get => Items.Count == 0 ? 0 : Items.Max(x => x.StartFrameBindable + x.DurationBindable);
     }
 }

+ 6 - 4
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs

@@ -1,16 +1,18 @@
 using System.Collections.ObjectModel;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
-public class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
+internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
 {
     public ObservableCollection<IKeyFrameHandler> Children { get; } = new ObservableCollection<IKeyFrameHandler>();
 
-    public override int StartFrame => Children.Count > 0 ? Children.Min(x => x.StartFrame) : 0;
-    public override int Duration => Children.Count > 0 ? Children.Max(x => x.StartFrame + x.Duration) : 0;
+    public override int StartFrameBindable => Children.Count > 0 ? Children.Min(x => x.StartFrameBindable) : 0;
+    public override int DurationBindable => Children.Count > 0 ? Children.Max(x => x.StartFrameBindable + x.DurationBindable) : 0;
 
-    public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id) : base(startFrame, duration, layerGuid, id)
+    public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
+        : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {
     }
 }

+ 68 - 14
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs

@@ -1,48 +1,102 @@
 using ChunkyImageLib;
 using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.ChangeableDocument.Actions.Generated;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
-public abstract class KeyFrameViewModel(int startFrame, int duration, Guid layerGuid, Guid id) : ObservableObject, IKeyFrameHandler
+internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
 {
     private Surface? previewSurface;
-    private int _startFrame = startFrame;
-    private int _duration = duration;
-    
+    private int startFrameBindable;
+    private int durationBindable;
+
+    public DocumentViewModel Document { get; }
+    protected DocumentInternalParts Internals { get; }
+
+    IDocument IKeyFrameHandler.Document => Document;
+
     public Surface? PreviewSurface
     {
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
     }
 
-    public virtual int StartFrame
+    public virtual int StartFrameBindable
     {
-        get => _startFrame;
+        get => startFrameBindable;
         set
         {
             if (value < 0)
             {
                 value = 0;
             }
-            
-            SetProperty(ref _startFrame, value);
+
+            if (!Document.UpdateableChangeActive)
+            {
+                Internals.ActionAccumulator.AddFinishedActions(
+                    new KeyFrameLength_Action(Id, value, DurationBindable),
+                    new EndKeyFrameLength_Action());
+            }
         }
     }
-    public virtual int Duration
+
+    public virtual int DurationBindable
     {
-        get => _duration;
+        get => durationBindable;
         set
         {
             if (value < 1)
             {
                 value = 1;
             }
-            
-            SetProperty(ref _duration, value);
+
+            if (!Document.UpdateableChangeActive)
+            {
+                Internals.ActionAccumulator.AddFinishedActions(
+                    new KeyFrameLength_Action(Id, StartFrameBindable, value),
+                    new EndKeyFrameLength_Action());
+            }
         }
     }
 
-    public Guid LayerGuid { get; } = layerGuid;
-    public Guid Id { get; } = id;
+    public Guid LayerGuid { get; }
+    public Guid Id { get; }
+
+    protected KeyFrameViewModel(int startFrame, int duration, Guid layerGuid, Guid id,
+        DocumentViewModel document, DocumentInternalParts internalParts)
+    {
+        startFrameBindable = startFrame;
+        durationBindable = duration;
+        LayerGuid = layerGuid;
+        Id = id;
+        Document = document;
+        Internals = internalParts;
+    }
+
+    public void SetStartFrame(int newStartFrame)
+    {
+        startFrameBindable = newStartFrame;
+        OnPropertyChanged(nameof(StartFrameBindable));
+    }
+
+    public void SetDuration(int newDuration)
+    {
+        durationBindable = newDuration;
+        OnPropertyChanged(nameof(DurationBindable));
+    }
+    
+    public void ChangeFrameLength(int newStartFrame, int newDuration)
+    {
+        newStartFrame = Math.Max(0, newStartFrame);
+        newDuration = Math.Max(1, newDuration);
+        Internals.ActionAccumulator.AddActions(
+            new KeyFrameLength_Action(Id, newStartFrame, newDuration));
+    }
+    
+    public void EndChangeFrameLength()
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new EndKeyFrameLength_Action());
+    }
 }

+ 5 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Document/RasterKeyFrameViewModel.cs

@@ -1,10 +1,12 @@
-using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.AvaloniaUI.Models.DocumentModels;
+using PixiEditor.AvaloniaUI.Models.Handlers;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
-public class RasterKeyFrameViewModel : KeyFrameViewModel, IRasterKeyFrameHandler
+internal class RasterKeyFrameViewModel : KeyFrameViewModel, IRasterKeyFrameHandler
 {
-    public RasterKeyFrameViewModel(Guid targetLayerGuid, int startFrame, int duration, Guid id) : base(startFrame, duration, targetLayerGuid, id)
+    public RasterKeyFrameViewModel(Guid targetLayerGuid, int startFrame, int duration, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
+        : base(startFrame, duration, targetLayerGuid, id, doc, internalParts)
     {
         
     }

+ 2 - 7
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -31,17 +31,12 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
     public void CreateRasterKeyFrame(bool duplicate)
     {
         var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        if (activeDocument == null || activeDocument.SelectedStructureMember is null)
+        if (activeDocument?.SelectedStructureMember is null)
         {
             return;
         }
 
-        int newFrame = 0;
-
-        if (activeDocument.AnimationDataViewModel.TryFindKeyFrame(activeDocument.SelectedStructureMember.GuidValue, out KeyFrameGroupViewModel group))
-        {
-            newFrame = group.StartFrame + group.Duration;
-        }
+        int newFrame = activeDocument.AnimationDataViewModel.ActiveFrameBindable;
         
         activeDocument.AnimationDataViewModel.CreateRasterKeyFrame(
             activeDocument.SelectedStructureMember.GuidValue, 

+ 24 - 11
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -10,7 +10,7 @@ namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 [TemplatePart("PART_ResizePanelRight", typeof(InputElement))]
 [TemplatePart("PART_ResizePanelLeft", typeof(InputElement))]
-public class KeyFrame : TemplatedControl
+internal class KeyFrame : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameViewModel> ItemProperty = AvaloniaProperty.Register<KeyFrame, KeyFrameViewModel>(
         nameof(Item));
@@ -45,9 +45,13 @@ public class KeyFrame : TemplatedControl
 
         _resizePanelLeft.PointerPressed += CapturePointer;
         _resizePanelLeft.PointerMoved += ResizePanelLeftOnPointerMoved;
+        
+        _resizePanelLeft.PointerCaptureLost += UpdateKeyFrame;
+        _resizePanelRight.PointerCaptureLost += UpdateKeyFrame;
 
         PointerPressed += CapturePointer;
         PointerMoved += DragOnPointerMoved;
+        PointerCaptureLost += UpdateKeyFrame;
     }
     
     private void CapturePointer(object? sender, PointerPressedEventArgs e)
@@ -58,7 +62,7 @@ public class KeyFrame : TemplatedControl
         }
         
         e.Pointer.Capture(sender as IInputElement);
-        clickFrameOffset = Item.StartFrame - (int)Math.Floor(e.GetPosition(this.FindAncestorOfType<Canvas>()).X / Scale);
+        clickFrameOffset = Item.StartFrameBindable - (int)Math.Floor(e.GetPosition(this.FindAncestorOfType<Grid>()).X / Scale);
         e.Handled = true;
     }
 
@@ -71,7 +75,7 @@ public class KeyFrame : TemplatedControl
         
         if (e.GetCurrentPoint(_resizePanelRight).Properties.IsLeftButtonPressed)
         {
-            Item.Duration = MousePosToFrame(e) - Item.StartFrame;
+            Item.ChangeFrameLength(Item.StartFrameBindable, MousePosToFrame(e) - Item.StartFrameBindable);
         }
         
         e.Handled = true;
@@ -88,15 +92,13 @@ public class KeyFrame : TemplatedControl
         {
             int frame = MousePosToFrame(e);
             
-            
-            if (frame >= Item.StartFrame + Item.Duration)
+            if (frame >= Item.StartFrameBindable + Item.DurationBindable)
             {
-                frame = Item.StartFrame + Item.Duration - 1;
+                frame = Item.StartFrameBindable + Item.DurationBindable - 1;
             }
             
-            int oldStartFrame = Item.StartFrame;
-            Item.StartFrame = frame;
-            Item.Duration += oldStartFrame - Item.StartFrame;
+            int oldStartFrame = Item.StartFrameBindable;
+            Item.ChangeFrameLength(frame, Item.DurationBindable + oldStartFrame - frame);
         }
         
         e.Handled = true;
@@ -112,13 +114,13 @@ public class KeyFrame : TemplatedControl
         if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
         {
             var frame = MousePosToFrame(e, false);
-            Item.StartFrame = frame + clickFrameOffset;
+            Item.ChangeFrameLength(frame + clickFrameOffset, Item.DurationBindable);
         }
     }
 
     private int MousePosToFrame(PointerEventArgs e, bool round = true)
     {
-        double x = e.GetPosition(this.FindAncestorOfType<Canvas>()).X;
+        double x = e.GetPosition(this.FindAncestorOfType<Grid>()).X;
         int frame;
         if (round)
         {
@@ -129,6 +131,17 @@ public class KeyFrame : TemplatedControl
             frame = (int)Math.Floor(x / Scale);
         }
         
+        frame = Math.Max(0, frame);
         return frame;
     }
+    
+    private void UpdateKeyFrame(object? sender, PointerCaptureLostEventArgs e)
+    {
+        if (Item is null || e.Source is not KeyFrame)
+        {
+            return;
+        }
+        
+        Item.EndChangeFrameLength();
+    }
 }

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -11,7 +11,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Document;
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 [TemplatePart("PART_PlayToggle", typeof(ToggleButton))]
-public class Timeline : TemplatedControl
+internal class Timeline : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameCollection> KeyFramesProperty =
         AvaloniaProperty.Register<Timeline, KeyFrameCollection>(
@@ -98,7 +98,7 @@ public class Timeline : TemplatedControl
     {
         ActiveFrame++;
         
-        if (ActiveFrame >= KeyFrames.FrameCount)
+        if (ActiveFrame >= KeyFrames.FrameCount - 1)
         {
             ActiveFrame = 0;
         }

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/KeyFrameLength_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+public record KeyFrameLength_ChangeInfo(Guid KeyFrameGuid, int StartFrame, int Duration) : IChangeInfo;

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -32,7 +32,7 @@ public class AnimationData : IReadOnlyAnimationData
     public void AddKeyFrame(KeyFrame keyFrame)
     {
         Guid id = keyFrame.LayerGuid;
-        if (TryFindKeyFrame(id, out GroupKeyFrame group))
+        if (TryFindKeyFrameCallback(id, out GroupKeyFrame group))
         {
             group.Children.Add(keyFrame);
         }
@@ -46,7 +46,7 @@ public class AnimationData : IReadOnlyAnimationData
 
     public void RemoveKeyFrame(Guid createdKeyFrameId)
     {
-        TryFindKeyFrame<KeyFrame>(createdKeyFrameId, out _, (frame, parent) =>
+        TryFindKeyFrameCallback<KeyFrame>(createdKeyFrameId, out _, (frame, parent) =>
         {
             if (parent != null)
             {
@@ -55,12 +55,12 @@ public class AnimationData : IReadOnlyAnimationData
         });
     }
     
-    public bool FindKeyFrame(Guid id, out IReadOnlyKeyFrame keyFrame)
+    public bool TryFindKeyFrame<T>(Guid id, out T keyFrame) where T : IReadOnlyKeyFrame
     {
-        return TryFindKeyFrame(id, out keyFrame, null);
+        return TryFindKeyFrameCallback(id, out keyFrame, null);
     }
 
-    private bool TryFindKeyFrame<T>(Guid id, out T? foundKeyFrame, Action<KeyFrame, GroupKeyFrame?> onFound = null) where T : IReadOnlyKeyFrame
+    private bool TryFindKeyFrameCallback<T>(Guid id, out T? foundKeyFrame, Action<KeyFrame, GroupKeyFrame?> onFound = null) where T : IReadOnlyKeyFrame
     {
         return TryFindKeyFrame(keyFrames, null, id, out foundKeyFrame, onFound);
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrame.cs

@@ -7,7 +7,7 @@ public abstract class KeyFrame : IReadOnlyKeyFrame
     public int StartFrame { get; set; }
     public int Duration { get; set; }
     public Guid LayerGuid { get; }
-    public Guid Id { get; protected init; }
+    public Guid Id { get; set; }
 
     protected KeyFrame(Guid layerGuid, int startFrame)
     {

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyAnimationData.cs

@@ -4,5 +4,5 @@ public interface IReadOnlyAnimationData
 {
     public int ActiveFrame { get; }
     public IReadOnlyList<IReadOnlyKeyFrame> KeyFrames { get; }
-    public bool FindKeyFrame(Guid id, out IReadOnlyKeyFrame keyFrame);
+    public bool TryFindKeyFrame<T>(Guid id, out T keyFrame) where T : IReadOnlyKeyFrame;
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/ActiveFrame_UpdateableChange.cs

@@ -11,6 +11,7 @@ internal class ActiveFrame_UpdateableChange : UpdateableChange
     public ActiveFrame_UpdateableChange(int activeFrame)
     {
         newFrame = activeFrame;
+        originalFrame = activeFrame;
     }
     
     [UpdateChangeMethod]

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

@@ -28,7 +28,15 @@ internal class CreateRasterKeyFrame_Change : Change
     {
         var keyFrame =
             new RasterKeyFrame(_targetLayerGuid, _frame, target, _cloneFromExisting ? _layer.LayerImage : null);
-        createdKeyFrameId = keyFrame.Id;
+        if (firstApply)
+        {
+            createdKeyFrameId = keyFrame.Id;
+        }
+        else
+        {
+            keyFrame.Id = createdKeyFrameId;
+        }
+
         target.AnimationData.AddKeyFrame(keyFrame);
         ignoreInUndo = false;
         return new CreateRasterKeyFrame_ChangeInfo(_targetLayerGuid, _frame, createdKeyFrameId, _cloneFromExisting);

+ 80 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFrameLength_UpdateableChange.cs

@@ -0,0 +1,80 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class KeyFrameLength_UpdateableChange : UpdateableChange
+{
+    public Guid KeyFrameGuid { get;  }
+    public int StartFrame { get; private set; }
+    public int Duration { get; private set; }
+    
+    private int originalStartFrame;
+    private int originalDuration;
+    
+    [GenerateUpdateableChangeActions]
+    public KeyFrameLength_UpdateableChange(Guid keyFrameGuid, int startFrame, int duration)
+    {
+        StartFrame = startFrame;
+        Duration = duration;
+        KeyFrameGuid = keyFrameGuid;
+    }
+    
+    [UpdateChangeMethod]
+    public void Update(int startFrame, int duration)
+    {
+        StartFrame = startFrame;
+        Duration = duration;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.AnimationData.TryFindKeyFrame<KeyFrame>(KeyFrameGuid, out KeyFrame frame))
+        {
+            originalStartFrame = frame.StartFrame;
+            originalDuration = frame.Duration;
+            return true;
+        }
+
+        return false;
+    }
+    
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        target.AnimationData.TryFindKeyFrame(KeyFrameGuid, out KeyFrame keyFrame);
+        keyFrame.StartFrame = StartFrame;
+        keyFrame.Duration = Duration;
+        return new KeyFrameLength_ChangeInfo(KeyFrameGuid, StartFrame, Duration);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        if (originalStartFrame == StartFrame && originalDuration == Duration)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+        target.AnimationData.TryFindKeyFrame(KeyFrameGuid, out KeyFrame keyFrame);
+        keyFrame.StartFrame = StartFrame;
+        keyFrame.Duration = Duration;
+        ignoreInUndo = false;
+        return new KeyFrameLength_ChangeInfo(KeyFrameGuid, StartFrame, Duration);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        if (originalStartFrame == StartFrame && originalDuration == Duration)
+        {
+            return new None();
+        }
+        target.AnimationData.TryFindKeyFrame(KeyFrameGuid, out KeyFrame keyFrame);
+        keyFrame.StartFrame = originalStartFrame;
+        keyFrame.Duration = originalDuration;
+        return new KeyFrameLength_ChangeInfo(KeyFrameGuid, originalStartFrame, originalDuration);
+    }
+    
+    public override bool IsMergeableWith(Change other)
+    {
+        return other is KeyFrameLength_UpdateableChange change && change.KeyFrameGuid == KeyFrameGuid;
+    }
+}