Browse Source

Delete key frame, active key frame

flabbet 1 year ago
parent
commit
eb022158e5

+ 34 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/AreEqualConverter.cs

@@ -0,0 +1,34 @@
+using System.Globalization;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class AreEqualConverter : SingleInstanceMultiValueConverter<AreEqualConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return Convert(new[] { value }, targetType, parameter, culture);
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (values.Count < 2)
+        {
+            return false;
+        }
+
+        for (int i = 1; i < values.Count; i++)
+        {
+            if(values[i] == null || values[i - 1] == null)
+            {
+                return false;
+            }
+            
+            if (!values[i].Equals(values[i - 1]))
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

+ 4 - 0
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -112,6 +112,10 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
                     break;
+                case DeleteKeyFrame_ChangeInfo info:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToEveryImagePreview();
+                    break;
             }
         }
     }

+ 11 - 3
src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml

@@ -9,17 +9,17 @@
         <Setter Property="Template">
             <ControlTemplate>
                 <Grid>
-                    <Border CornerRadius="{DynamicResource ControlCornerRadius}"
+                    <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
                             Background="{DynamicResource ThemeBackgroundBrush1}" Height="20"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
                             <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
-                            <Panel Margin="-20, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                            <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
                         </Grid>
                     </Border>
                     
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
-                            BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="False"
+                            BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             RenderOptions.BitmapInterpolationMode="None">
                         <Border.Background>
@@ -44,6 +44,14 @@
                 </Grid>
             </ControlTemplate>
         </Setter>
+        
+        <Style Selector="^:selected /template/ Border#MainBorder">
+            <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
+        </Style>
+        
+        <Style Selector="^:selected /template/ Border#PreviewBorder">
+            <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
+        </Style>
     </ControlTheme>
 
 </ResourceDictionary>

+ 31 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -31,6 +31,11 @@
                         <Button DockPanel.Dock="Left" Classes="pixi-icon"
                                 Content="{DynamicResource icon-duplicate}"
                                 Command="{TemplateBinding DuplicateKeyFrameCommand}" />
+                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
+                                Content="{DynamicResource icon-trash}"
+                                Command="{TemplateBinding DeleteKeyFrameCommand}" 
+                                IsEnabled="{Binding !!SelectedKeyFrame, RelativeSource={RelativeSource TemplatedParent}}"
+                                CommandParameter="{Binding SelectedKeyFrame.Id, RelativeSource={RelativeSource TemplatedParent}}"/>
                         <TextBlock DockPanel.Dock="Left" Text="Scale:" />
                         <Slider Minimum="1" Maximum="100" Value="{TemplateBinding Scale, Mode=TwoWay}" Width="100" />
                         <TextBlock Text="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" />
@@ -88,6 +93,13 @@
                             <ScrollViewer Background="{DynamicResource ThemeBackgroundBrush}"
                                           Grid.Column="1" VerticalScrollBarVisibility="Disabled"
                                           HorizontalScrollBarVisibility="Auto">
+                                <Interaction.Behaviors>
+                                    <EventTriggerBehavior EventName="PointerPressed">
+                                        <InvokeCommandAction Command="{Binding SelectKeyFrameCommand,
+                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                                             CommandParameter="{x:Null}"/>
+                                    </EventTriggerBehavior>
+                                </Interaction.Behaviors>
                                 <ItemsControl
                                     ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
@@ -102,7 +114,7 @@
                                                 </ItemsControl.ItemContainerTheme>
                                                 <ItemsControl.ItemsPanel>
                                                     <ItemsPanelTemplate>
-                                                        <Grid Margin="30, 0, 0, 0"/>
+                                                        <Grid Margin="30, 0, 0, 0" />
                                                     </ItemsPanelTemplate>
                                                 </ItemsControl.ItemsPanel>
                                             </ItemsControl>
@@ -119,6 +131,24 @@
                                                             Path="Scale" />
                                                     </MultiBinding>
                                                 </animations:KeyFrame.Width>
+                                                <animations:KeyFrame.IsSelected>
+                                                    <MultiBinding>
+                                                        <MultiBinding.Converter>
+                                                            <converters:AreEqualConverter />
+                                                        </MultiBinding.Converter>
+                                                        <Binding Path="SelectedKeyFrame" RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" />
+                                                        <Binding
+                                                            RelativeSource="{RelativeSource Self}"
+                                                            Path="Item" />
+                                                    </MultiBinding>
+                                                </animations:KeyFrame.IsSelected>
+                                                <Interaction.Behaviors>
+                                                    <EventTriggerBehavior EventName="PointerPressed">
+                                                        <InvokeCommandAction Command="{Binding SelectKeyFrameCommand,
+                                                        RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                                                             CommandParameter="{Binding}"/>
+                                                    </EventTriggerBehavior>
+                                                </Interaction.Behaviors>
                                             </animations:KeyFrame>
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>

+ 16 - 7
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
@@ -45,6 +46,14 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
     }
 
+    public void DeleteKeyFrame(Guid keyFrameId)
+    {
+        if (!Document.UpdateableChangeActive)
+        {
+            Internals.ActionAccumulator.AddFinishedActions(new DeleteKeyFrame_Action(keyFrameId));
+        }
+    }
+    
     public void SetActiveFrame(int newFrame)
     {
         _activeFrameBindable = newFrame;
@@ -64,19 +73,19 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     public void AddKeyFrame(IKeyFrameHandler keyFrame)
     {
         Guid id = keyFrame.LayerGuid;
-        if (TryFindKeyFrame(id, out KeyFrameGroupViewModel group))
+        if (TryFindKeyFrame(id, out KeyFrameGroupViewModel foundGroup))
         {
-            group.Children.Add((KeyFrameViewModel)keyFrame);
+            foundGroup.Children.Add((KeyFrameViewModel)keyFrame);
         }
         else
         {
-            KeyFrameGroupViewModel createdGroup =
+            var group =
                 new KeyFrameGroupViewModel(keyFrame.StartFrameBindable, keyFrame.DurationBindable, id, id, Document, Internals);
-            createdGroup.Children.Add((KeyFrameViewModel)keyFrame);
-            keyFrames.Add(createdGroup);
+            group.Children.Add((KeyFrameViewModel)keyFrame);
+            keyFrames.Add(group);
         }
 
-        keyFrames.NotifyCollectionChanged();
+        keyFrames.NotifyCollectionChanged(NotifyCollectionChangedAction.Add, (KeyFrameViewModel)keyFrame);
     }
 
     public void RemoveKeyFrame(Guid keyFrameId)
@@ -84,9 +93,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         TryFindKeyFrame<KeyFrameViewModel>(keyFrameId, out _, (frame, parent) =>
         {
             parent.Children.Remove(frame);
+            keyFrames.NotifyCollectionChanged(NotifyCollectionChangedAction.Remove, (KeyFrameViewModel)frame);
         });
         
-        keyFrames.NotifyCollectionChanged();
     }
     
     public bool FindKeyFrame<T>(Guid guid, out T keyFrameHandler) where T : IKeyFrameHandler

+ 18 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameCollection.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.ComponentModel;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -9,12 +10,28 @@ internal class KeyFrameCollection : ObservableCollection<KeyFrameGroupViewModel>
     {
         
     }
-
+    
+    public event Action<KeyFrameViewModel> KeyFrameAdded; 
+    public event Action<KeyFrameViewModel> KeyFrameRemoved; 
+    
     public void NotifyCollectionChanged()
     {
         OnPropertyChanged(new PropertyChangedEventArgs(nameof(FrameCount)));
     }
 
+    public void NotifyCollectionChanged(NotifyCollectionChangedAction action, KeyFrameViewModel keyFrame)
+    {
+        NotifyCollectionChanged();
+        if (action == NotifyCollectionChangedAction.Add)
+        {
+            KeyFrameAdded?.Invoke(keyFrame);
+        }
+        else if (action == NotifyCollectionChangedAction.Remove)
+        {
+            KeyFrameRemoved?.Invoke(keyFrame);
+        }
+    }
+
     public int FrameCount
     {
         get => Items.Count == 0 ? 0 : Items.Max(x => x.StartFrameBindable + x.DurationBindable);

+ 10 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -50,6 +50,16 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         activeDocument.Operations.SetActiveFrame(newFrame);
     }
     
+    [Command.Basic("PixiEditor.Animation.DeleteKeyFrame", "Delete Key Frame", "Delete a key frame")]
+    public void DeleteKeyFrame(Guid keyFrameId)
+    {
+        var activeDocument = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        if (activeDocument is null || !activeDocument.AnimationDataViewModel.FindKeyFrame<KeyFrameViewModel>(keyFrameId, out _))
+            return;
+        
+        activeDocument.AnimationDataViewModel.DeleteKeyFrame(keyFrameId);
+    }
+    
     [Command.Basic("PixiEditor.Animation.ExportSpriteSheet", "Export Sprite Sheet", "Export the sprite sheet")]
     public async Task ExportSpriteSheet()
     {

+ 25 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -13,6 +13,7 @@ namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 [TemplatePart("PART_ResizePanelRight", typeof(InputElement))]
 [TemplatePart("PART_ResizePanelLeft", typeof(InputElement))]
+[PseudoClasses(":selected")]
 internal class KeyFrame : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameViewModel> ItemProperty = AvaloniaProperty.Register<KeyFrame, KeyFrameViewModel>(
@@ -20,6 +21,15 @@ internal class KeyFrame : TemplatedControl
 
     public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<KeyFrame, double>(nameof(Scale), 100);
 
+    public static readonly StyledProperty<bool> IsSelectedProperty = AvaloniaProperty.Register<KeyFrame, bool>(
+        nameof(IsSelected));
+
+    public bool IsSelected
+    {
+        get => GetValue(IsSelectedProperty);
+        set => SetValue(IsSelectedProperty, value);
+    }
+
     public KeyFrameViewModel Item
     {
         get => GetValue(ItemProperty);
@@ -36,6 +46,11 @@ internal class KeyFrame : TemplatedControl
     private InputElement _resizePanelLeft;
 
     private int clickFrameOffset;
+    
+    static KeyFrame()
+    {
+        IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
+    }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
@@ -162,4 +177,14 @@ internal class KeyFrame : TemplatedControl
         
         Item.EndChangeFrameLength();
     }
+    
+    private static void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not KeyFrame keyFrame)
+        {
+            return;
+        }
+
+        keyFrame.PseudoClasses.Set(":selected", keyFrame.IsSelected);
+    }
 }

+ 61 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
@@ -6,6 +7,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Interactivity;
 using Avalonia.Threading;
+using CommunityToolkit.Mvvm.Input;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
 namespace PixiEditor.AvaloniaUI.Views.Animations;
@@ -32,6 +34,15 @@ internal class Timeline : TemplatedControl
     
     public static readonly StyledProperty<int> FpsProperty = AvaloniaProperty.Register<Timeline, int>(nameof(Fps), 60);
 
+    public static readonly StyledProperty<KeyFrameViewModel> SelectedKeyFrameProperty = AvaloniaProperty.Register<Timeline, KeyFrameViewModel>(
+        "SelectedKeyFrame");
+
+    public KeyFrameViewModel SelectedKeyFrame
+    {
+        get => GetValue(SelectedKeyFrameProperty);
+        set => SetValue(SelectedKeyFrameProperty, value);
+    }
+
     public double Scale
     {
         get => GetValue(ScaleProperty);
@@ -46,8 +57,17 @@ internal class Timeline : TemplatedControl
 
     public static readonly StyledProperty<ICommand> DuplicateKeyFrameCommandProperty =
         AvaloniaProperty.Register<Timeline, ICommand>(
-            "DuplicateKeyFrameCommand");
+            nameof(DuplicateKeyFrameCommand));
 
+    public static readonly StyledProperty<ICommand> DeleteKeyFrameCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
+        nameof(DeleteKeyFrameCommand));
+
+    public ICommand DeleteKeyFrameCommand
+    {
+        get => GetValue(DeleteKeyFrameCommandProperty);
+        set => SetValue(DeleteKeyFrameCommandProperty, value);
+    }
+    
     public ICommand DuplicateKeyFrameCommand
     {
         get => GetValue(DuplicateKeyFrameCommandProperty);
@@ -78,6 +98,8 @@ internal class Timeline : TemplatedControl
         set { SetValue(FpsProperty, value); }
     }
 
+    public ICommand SelectKeyFrameCommand { get; }
+
     private ToggleButton? _playToggle;
     private DispatcherTimer _playTimer;
 
@@ -85,12 +107,17 @@ internal class Timeline : TemplatedControl
     {
         IsPlayingProperty.Changed.Subscribe(IsPlayingChanged);
         FpsProperty.Changed.Subscribe(FpsChanged);
+        KeyFramesProperty.Changed.Subscribe(OnKeyFramesChanged);
     }
 
     public Timeline()
     {
         _playTimer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(1000f / Fps) };
         _playTimer.Tick += PlayTimerOnTick;
+        SelectKeyFrameCommand = new RelayCommand<KeyFrameViewModel>(keyFrame =>
+        {
+            SelectedKeyFrame = keyFrame;
+        });
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -169,4 +196,37 @@ internal class Timeline : TemplatedControl
 
         timeline._playTimer.Interval = TimeSpan.FromMilliseconds(1000f / timeline.Fps);
     }
+    
+    private static void OnKeyFramesChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not Timeline timeline)
+        {
+            return;
+        }
+
+        if(e.OldValue is KeyFrameCollection oldCollection)
+        {
+            oldCollection.KeyFrameAdded -= timeline.KeyFrames_KeyFrameAdded;
+            oldCollection.KeyFrameRemoved -= timeline.KeyFrames_KeyFrameRemoved;
+        }
+        
+        if(e.NewValue is KeyFrameCollection newCollection)
+        {
+            newCollection.KeyFrameAdded += timeline.KeyFrames_KeyFrameAdded;
+            newCollection.KeyFrameRemoved += timeline.KeyFrames_KeyFrameRemoved;
+        }
+    }
+    
+    private void KeyFrames_KeyFrameAdded(KeyFrameViewModel keyFrame)
+    {
+        SelectedKeyFrame = keyFrame;
+    }
+    
+    private void KeyFrames_KeyFrameRemoved(KeyFrameViewModel keyFrame)
+    {
+        if (SelectedKeyFrame == keyFrame)
+        {
+            SelectedKeyFrame = null;
+        }
+    }
 }

+ 2 - 1
src/PixiEditor.AvaloniaUI/Views/Dock/TimelineDockView.axaml

@@ -16,5 +16,6 @@
         KeyFrames="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.KeyFrames}" 
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
-        DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"/>
+        DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
+        DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteKeyFrame, UseProvided=True}"/>
 </UserControl>

+ 0 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/DeleteKeyFrame_ChangeInfo.cs

@@ -1,5 +1,4 @@
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 public record DeleteKeyFrame_ChangeInfo(
-    int Frame,
     Guid DeletedKeyFrameId) : IChangeInfo;

+ 1 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/IChangeInfo.cs

@@ -2,4 +2,5 @@
 
 public interface IChangeInfo
 {
+    
 }

+ 11 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/GroupKeyFrame.cs

@@ -32,4 +32,15 @@ internal class GroupKeyFrame : KeyFrame
     {
         return frame >= StartFrame && frame < EndFrame + 1;
     }
+
+    public override KeyFrame Clone()
+    {
+        var clone = new GroupKeyFrame(LayerGuid, StartFrame, document) { Id = this.Id };
+        foreach (var child in Children)
+        {
+            clone.Children.Add(child.Clone());
+        }
+
+        return clone;
+    }
 }

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

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ChangeableDocument.Changeables.Animations;
 
-public abstract class KeyFrame : IReadOnlyKeyFrame
+public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
 {
     private int startFrame;
     private int duration;
@@ -59,4 +59,11 @@ public abstract class KeyFrame : IReadOnlyKeyFrame
     {
         return frame >= StartFrame && frame < EndFrame;
     }
+
+    public abstract KeyFrame Clone();
+
+    public virtual void Dispose()
+    {
+        
+    }
 }

+ 11 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/RasterKeyFrame.cs

@@ -20,4 +20,15 @@ internal class RasterKeyFrame : KeyFrame
             layer.LayerImage = Image;
         }
     }
+
+    public override KeyFrame Clone()
+    {
+        var image = Image.CloneFromCommitted();
+        return new RasterKeyFrame(LayerGuid, StartFrame, Document, image) { Id = this.Id };
+    }
+
+    public override void Dispose()
+    {
+        Image.Dispose();
+    }
 }

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

@@ -45,6 +45,6 @@ internal class CreateRasterKeyFrame_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.AnimationData.RemoveKeyFrame(createdKeyFrameId);
-        return new DeleteKeyFrame_ChangeInfo(_frame, createdKeyFrameId);
+        return new DeleteKeyFrame_ChangeInfo(createdKeyFrameId);
     }
 }

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs

@@ -0,0 +1,46 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class DeleteKeyFrame_Change : Change
+{
+    private readonly Guid _keyFrameId;
+    private KeyFrame clonedKeyFrame;
+    
+    [GenerateMakeChangeAction]
+    public DeleteKeyFrame_Change(Guid keyFrameId)
+    {
+        _keyFrameId = keyFrameId;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.AnimationData.TryFindKeyFrame(_keyFrameId, out KeyFrame keyFrame))
+        {
+            clonedKeyFrame = keyFrame.Clone();
+            return true;
+        }
+
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        target.AnimationData.RemoveKeyFrame(_keyFrameId);
+        ignoreInUndo = false;
+        return new DeleteKeyFrame_ChangeInfo(_keyFrameId);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        target.AnimationData.AddKeyFrame(clonedKeyFrame.Clone());
+        return new CreateRasterKeyFrame_ChangeInfo(clonedKeyFrame.LayerGuid, clonedKeyFrame.StartFrame, clonedKeyFrame.Id, false);   
+    }
+
+    public override void Dispose()
+    {
+        clonedKeyFrame.Dispose();
+    }
+}