Browse Source

Added a possibility to change default end frame

Krzysztof Krysiński 1 month ago
parent
commit
b63e562f89

+ 4 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/DefaultEndFrame_ChangeInfo.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+public record DefaultEndFrame_ChangeInfo(int NewDefaultEndFrame) : IChangeInfo;
+

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -7,6 +7,7 @@ internal class AnimationData : IReadOnlyAnimationData
 {
     public int FrameRate { get; set; } = 24;
     public int OnionFrames { get; set; } = 1;
+    public int DefaultEndFrame { get; set; } = 24;
     public IReadOnlyList<IReadOnlyKeyFrame> KeyFrames => keyFrames;
     public double OnionOpacity { get; set; } = 50;
 

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

@@ -6,5 +6,6 @@ public interface IReadOnlyAnimationData
     public IReadOnlyList<IReadOnlyKeyFrame> KeyFrames { get; }
     public int OnionFrames { get; }
     public double OnionOpacity { get; }
+    public int DefaultEndFrame { get; }
     public bool TryFindKeyFrame<T>(Guid id, out T keyFrame) where T : IReadOnlyKeyFrame;
 }

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs

@@ -40,6 +40,11 @@ internal class CreateCel_Change : Change
             {
                 return false;
             }
+
+            if (targetLayer.KeyFrames.First()?.KeyFrameGuid == createdKeyFrameId)
+            {
+                return false;
+            }
         }
         
         return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);

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

@@ -1,4 +1,5 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
@@ -31,6 +32,17 @@ internal class KeyFrameLength_UpdateableChange : UpdateableChange
     {
         if (target.AnimationData.TryFindKeyFrame<KeyFrame>(KeyFrameGuid, out KeyFrame frame))
         {
+            var node = target.FindNode<Node>(frame.NodeId);
+            if (node is null)
+            {
+                return false;
+            }
+
+            if (node.KeyFrames.FirstOrDefault()?.KeyFrameGuid == frame.Id)
+            {
+                return false;
+            }
+
             originalStartFrame = frame.StartFrame;
             originalDuration = frame.Duration;
             return true;

+ 28 - 12
src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFramesStartPos_UpdateableChange.cs

@@ -1,17 +1,20 @@
 using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 
 namespace PixiEditor.ChangeableDocument.Changes.Animation;
 
 internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChange
 {
-    public Guid[] KeyFramesGuid { get;  }
+    public Guid[] KeyFramesGuid { get; }
     public int Delta { get; set; }
-    
+
     private Dictionary<Guid, int> originalStartFrames = new();
-    
+
     [GenerateUpdateableChangeActions]
-    public KeyFramesStartPos_UpdateableChange(List<Guid> keyFramesGuid, int delta) // do not delete, code generator uses this for update method
+    public
+        KeyFramesStartPos_UpdateableChange(List<Guid> keyFramesGuid,
+            int delta) // do not delete, code generator uses this for update method
     {
         KeyFramesGuid = keyFramesGuid.ToArray();
         Delta = 0;
@@ -22,13 +25,24 @@ internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChang
     {
         Delta += delta;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         foreach (Guid keyFrameGuid in KeyFramesGuid)
         {
             if (target.AnimationData.TryFindKeyFrame(keyFrameGuid, out KeyFrame keyFrame))
             {
+                var node = target.FindNode<Node>(keyFrame.NodeId);
+                if (node is null)
+                {
+                    return false;
+                }
+
+                if (node.KeyFrames.FirstOrDefault()?.KeyFrameGuid == keyFrameGuid)
+                {
+                    return false;
+                }
+
                 originalStartFrames[keyFrameGuid] = keyFrame.StartFrame;
             }
             else
@@ -39,7 +53,7 @@ internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChang
 
         return true;
     }
-    
+
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         List<IChangeInfo> changes = new();
@@ -49,11 +63,12 @@ internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChang
             keyFrame.StartFrame = originalStartFrames[keyFrameGuid] + Delta;
             changes.Add(new KeyFrameLength_ChangeInfo(keyFrameGuid, keyFrame.StartFrame, keyFrame.Duration));
         }
-        
+
         return changes;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         List<IChangeInfo> changes = new();
         foreach (Guid keyFrameGuid in KeyFramesGuid)
@@ -62,7 +77,7 @@ internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChang
             keyFrame.StartFrame = originalStartFrames[keyFrameGuid] + Delta;
             changes.Add(new KeyFrameLength_ChangeInfo(keyFrameGuid, keyFrame.StartFrame, keyFrame.Duration));
         }
-        
+
         ignoreInUndo = false;
         return changes;
     }
@@ -76,12 +91,13 @@ internal class KeyFramesStartPos_UpdateableChange : InterruptableUpdateableChang
             keyFrame.StartFrame = originalStartFrames[keyFrameGuid];
             changes.Add(new KeyFrameLength_ChangeInfo(keyFrameGuid, keyFrame.StartFrame, keyFrame.Duration));
         }
-        
+
         return changes;
     }
-    
+
     public override bool IsMergeableWith(Change other)
     {
-        return other is KeyFramesStartPos_UpdateableChange otherChange && otherChange.KeyFramesGuid.SequenceEqual(KeyFramesGuid);
+        return other is KeyFramesStartPos_UpdateableChange otherChange &&
+               otherChange.KeyFramesGuid.SequenceEqual(KeyFramesGuid);
     }
 }

+ 53 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/SetDefaultEndFrame_Change.cs

@@ -0,0 +1,53 @@
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class SetDefaultEndFrame_Change : Change
+{
+    public int NewDefaultEndFrame { get; }
+
+    private int originalDefaultEndFrame;
+
+    [GenerateMakeChangeAction]
+    public SetDefaultEndFrame_Change(int newDefaultEndFrame)
+    {
+        NewDefaultEndFrame = newDefaultEndFrame;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.AnimationData is null)
+        {
+            return false;
+        }
+
+        if (NewDefaultEndFrame < 0)
+        {
+            return false;
+        }
+
+        originalDefaultEndFrame = target.AnimationData.DefaultEndFrame;
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+
+        if (target.AnimationData is null)
+        {
+            return new None();
+        }
+
+        target.AnimationData.DefaultEndFrame = NewDefaultEndFrame;
+
+        return new DefaultEndFrame_ChangeInfo(NewDefaultEndFrame);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        target.AnimationData.DefaultEndFrame = originalDefaultEndFrame;
+        return new DefaultEndFrame_ChangeInfo(originalDefaultEndFrame);
+    }
+}

BIN
src/PixiEditor/Data/BetaExampleFiles/Island.pixi


BIN
src/PixiEditor/Data/BetaExampleFiles/Stars.pixi


BIN
src/PixiEditor/Data/BetaExampleFiles/Tree.pixi


+ 55 - 0
src/PixiEditor/Helpers/Converters/TwoWayFrameToTimeConverter.cs

@@ -0,0 +1,55 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace PixiEditor.Helpers.Converters;
+
+public class TwoWayFrameToTimeConverter : AvaloniaObject, IValueConverter
+{
+    public static readonly StyledProperty<int> FpsProperty = AvaloniaProperty.Register<TwoWayFrameToTimeConverter, int>(
+        nameof(Fps));
+
+    public int Fps
+    {
+        get => GetValue(FpsProperty);
+        set => SetValue(FpsProperty, value);
+    }
+
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is int frame)
+        {
+            var span = TimeSpan.FromSeconds(frame / (double)Fps).ToString("mm\\:ss\\.ff");
+            return span;
+        }
+
+        return null;
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is string timeString)
+        {
+            if (TimeSpan.TryParse(timeString, CultureInfo.InvariantCulture, out TimeSpan time))
+            {
+                return (int)(time.TotalSeconds * Fps);
+            }
+
+            if(TimeSpan.TryParseExact(timeString, "mm\\:ss\\.ff", CultureInfo.InvariantCulture, out time))
+            {
+                return (int)(time.TotalSeconds * Fps);
+            }
+
+            if (timeString.EndsWith("s"))
+            {
+                timeString = timeString.TrimEnd('s').Trim();
+                if (double.TryParse(timeString, NumberStyles.Float, CultureInfo.InvariantCulture, out double seconds))
+                {
+                    return (int)(seconds * Fps);
+                }
+            }
+        }
+
+        return null;
+    }
+}

+ 12 - 0
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -101,6 +101,11 @@ internal class DocumentViewModelBuilder
             BuildKeyFrames(animationData.KeyFrameGroups.ToList(), AnimationData.KeyFrameGroups, documentGraph);
         }
 
+        if (animationData?.DefaultEndFrame >= 0)
+        {
+            AnimationData.WithDefaultEndFrame(animationData.DefaultEndFrame);
+        }
+
         return this;
     }
 
@@ -279,6 +284,7 @@ internal class AnimationDataBuilder
     public List<KeyFrameBuilder> KeyFrameGroups { get; set; } = new List<KeyFrameBuilder>();
     public int OnionFrames { get; set; }
     public double OnionOpacity { get; set; } = 50;
+    public int DefaultEndFrame { get; set; } = -1;
 
     public AnimationDataBuilder WithFrameRate(int frameRate)
     {
@@ -303,6 +309,12 @@ internal class AnimationDataBuilder
         builder(KeyFrameGroups);
         return this;
     }
+
+    public AnimationDataBuilder WithDefaultEndFrame(int endFrame)
+    {
+        DefaultEndFrame = endFrame;
+        return this;
+    }
 }
 
 internal class KeyFrameBuilder()

+ 8 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -228,6 +228,9 @@ internal class DocumentUpdater
             case ComputedPropertyValue_ChangeInfo info:
                 ProcessComputedPropertyValue(info);
                 break;
+            case DefaultEndFrame_ChangeInfo info:
+                ProcessNewDefaultEndFrame(info);
+                break;
         }
     }
 
@@ -562,6 +565,11 @@ internal class DocumentUpdater
         doc.AnimationHandler.SetActiveFrame(info.Frame);
     }
 
+    private void ProcessNewDefaultEndFrame(DefaultEndFrame_ChangeInfo info)
+    {
+        doc.AnimationHandler.SetDefaultEndFrame(info.NewDefaultEndFrame);
+    }
+
     private void ProcessKeyFrameLength(KeyFrameLength_ChangeInfo info)
     {
         doc.AnimationHandler.SetCelLength(info.KeyFrameGuid, info.StartFrame, info.Duration);

+ 1 - 0
src/PixiEditor/Models/Handlers/IAnimationHandler.cs

@@ -27,4 +27,5 @@ internal interface IAnimationHandler : IDisposable
     public int LastFrame { get; }
     public void SetOnionFrames(int frames, double opacity);
     public void SetPlayingState(bool play);
+    public void SetDefaultEndFrame(int newEndFrame);
 }

+ 79 - 26
src/PixiEditor/Styles/Templates/Timeline.axaml

@@ -10,7 +10,8 @@
                     xmlns:input="clr-namespace:PixiEditor.Views.Input"
                     xmlns:system="clr-namespace:System;assembly=System.Runtime"
                     xmlns:ui1="clr-namespace:PixiEditor.UI.Common.Localization;assembly=PixiEditor.UI.Common"
-                    xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common">
+                    xmlns:controls="clr-namespace:PixiEditor.UI.Common.Controls;assembly=PixiEditor.UI.Common"
+                    xmlns:behaviors="clr-namespace:PixiEditor.UI.Common.Behaviors;assembly=PixiEditor.UI.Common">
     <ControlTheme TargetType="animations:Timeline" x:Key="{x:Type animations:Timeline}">
         <Setter Property="Template">
             <ControlTemplate>
@@ -27,10 +28,10 @@
 
                     <Border DockPanel.Dock="Left" BorderThickness="0 0 1 0"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}">
-                        <DockPanel Grid.Row="0" Grid.Column="0" LastChildFill="False" Margin="5 0">
+                        <DockPanel LastChildFill="False" Margin="5 0">
                             <controls:SizeInput Unit="FPS"
-                                             Width="90" Height="25" HorizontalAlignment="Left"
-                                             Size="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+                                                Width="90" Height="25" HorizontalAlignment="Left"
+                                                Size="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
 
                             <Button Classes="pixi-icon" DockPanel.Dock="Right"
                                     Content="{DynamicResource icon-settings}"
@@ -48,28 +49,33 @@
                                 </Button.Transitions>
                                 <Button.Flyout>
                                     <Flyout Placement="Top">
-                                        <StackPanel>
-                                            <TextBlock Classes="h4" Margin="5" ui1:Translator.Key="SETTINGS" />
-                                            <StackPanel Orientation="Horizontal" Spacing="5">
-                                                <TextBlock VerticalAlignment="Center"
-                                                           ui1:Translator.Key="ONION_FRAMES_COUNT" />
-                                                <controls:NumberInput
-                                                    HorizontalAlignment="Left" Width="50"
-                                                    Min="1" Decimals="0"
-                                                    Max="128"
-                                                    Value="{Binding OnionFrames, RelativeSource={RelativeSource TemplatedParent}, 
+                                        <Grid>
+                                            <Grid.RowDefinitions>
+                                                <RowDefinition Height="Auto"/>
+                                                <RowDefinition Height="Auto"/>
+                                                <RowDefinition Height="Auto"/>
+                                            </Grid.RowDefinitions>
+                                            <Grid.ColumnDefinitions>
+                                                <ColumnDefinition Width="Auto"/>
+                                                <ColumnDefinition Width="*" />
+                                            </Grid.ColumnDefinitions>
+                                            <TextBlock Grid.Row="0" Classes="h4" Margin="5" ui1:Translator.Key="SETTINGS" />
+                                            <TextBlock Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"
+                                                       ui1:Translator.Key="ONION_FRAMES_COUNT" />
+                                            <controls:NumberInput Grid.Column="1" Grid.Row="1" Margin="0, 5"
+                                                HorizontalAlignment="Left" Width="50"
+                                                Min="1" Decimals="0"
+                                                Max="128"
+                                                Value="{Binding OnionFrames, RelativeSource={RelativeSource TemplatedParent},
                                                     Mode=TwoWay}" />
-                                            </StackPanel>
-                                            <StackPanel Orientation="Horizontal" Spacing="5">
-                                                <TextBlock VerticalAlignment="Center"
-                                                           ui1:Translator.Key="ONION_OPACITY" />
-                                                <controls:SizeInput
-                                                    HorizontalAlignment="Left" Width="80"
-                                                    MaxSize="100" Unit="%"
-                                                    Size="{Binding OnionOpacity, RelativeSource={RelativeSource TemplatedParent}, 
+                                            <TextBlock Grid.Column="0" Grid.Row="2" VerticalAlignment="Center"
+                                                       ui1:Translator.Key="ONION_OPACITY" />
+                                            <controls:SizeInput Grid.Column="1" Grid.Row="2"
+                                                HorizontalAlignment="Left" Width="80"
+                                                MaxSize="100" Unit="%"
+                                                Size="{Binding OnionOpacity, RelativeSource={RelativeSource TemplatedParent},
                                                     Mode=TwoWay}" />
-                                            </StackPanel>
-                                        </StackPanel>
+                                        </Grid>
                                     </Flyout>
                                 </Button.Flyout>
                             </Button>
@@ -79,7 +85,7 @@
                         <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
                             <Button Classes="pixi-icon" Content="{DynamicResource icon-step-start}"
                                     ui1:Translator.TooltipKey="STEP_START"
-                                    Command="{Binding StepStartCommand, RelativeSource={RelativeSource TemplatedParent}}"/>
+                                    Command="{Binding StepStartCommand, RelativeSource={RelativeSource TemplatedParent}}" />
                             <Button Classes="pixi-icon"
                                     ui1:Translator.TooltipKey="STEP_BACK"
                                     Command="{Binding StepBackCommand, RelativeSource={RelativeSource TemplatedParent}}"
@@ -95,7 +101,18 @@
                                     ui1:Translator.TooltipKey="STEP_END"
                                     Command="{Binding StepEndCommand, RelativeSource={RelativeSource TemplatedParent}}"
                                     Content="{DynamicResource icon-step-end}" />
-                            <TextBlock VerticalAlignment="Center" FontSize="14">
+                            <TextBlock Name="lengthTb" VerticalAlignment="Center" FontSize="14"
+                                       IsVisible="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=
+                            !!KeyFrames.Count}">
+                                <TextBlock.ContextFlyout>
+                                    <Flyout Placement="Top">
+                                        <StackPanel>
+                                            <TextBlock Classes="h4" Margin="5"
+                                                       ui1:Translator.Key="CHANGE_KEYFRAMES_LENGTH" />
+
+                                        </StackPanel>
+                                    </Flyout>
+                                </TextBlock.ContextFlyout>
                                 <Run>
                                     <Run.Text>
                                         <MultiBinding>
@@ -121,6 +138,42 @@
                                     </Run.Text>
                                 </Run>
                             </TextBlock>
+                            <StackPanel Orientation="Horizontal"
+                                        IsVisible="{Binding ElementName=lengthTb, Path=!IsVisible}"
+                                        Margin="5, 0, 0, 0">
+                                <StackPanel.Resources>
+                                    <converters:TwoWayFrameToTimeConverter
+                                        Fps="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Fps}"
+                                        x:Key="FrameToTimeConverter" />
+                                </StackPanel.Resources>
+                                <TextBlock VerticalAlignment="Center" FontSize="14">
+                                    <Run>
+                                        <Run.Text>
+                                            <MultiBinding>
+                                                <MultiBinding.Converter>
+                                                    <converters:FrameToTimeConverter />
+                                                </MultiBinding.Converter>
+                                                <Binding Path="ActiveFrame"
+                                                         RelativeSource="{RelativeSource TemplatedParent}" />
+                                                <Binding Path="Fps" RelativeSource="{RelativeSource TemplatedParent}" />
+                                            </MultiBinding>
+                                        </Run.Text>
+                                    </Run>
+                                    <Run Text="/" />
+                                </TextBlock>
+                                <TextBox Margin="5, 0, 0, 0" MinWidth="50" VerticalAlignment="Center"
+                                         HorizontalAlignment="Left"
+                                         Text="{Binding DefaultEndFrame, UpdateSourceTrigger=LostFocus, RelativeSource={RelativeSource TemplatedParent},
+                                         Mode=TwoWay, Converter={StaticResource FrameToTimeConverter}}">
+                                    <Interaction.Behaviors>
+                                        <BehaviorCollection>
+                                            <behaviors:TextBoxFocusBehavior ConfirmOnEnter="True"
+                                                                            DeselectOnFocusLoss="True" />
+                                            <behaviours:GlobalShortcutFocusBehavior />
+                                        </BehaviorCollection>
+                                    </Interaction.Behaviors>
+                                </TextBox>
+                            </StackPanel>
                         </StackPanel>
                     </Border>
 

+ 36 - 5
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -17,6 +17,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     private int frameRateBindable = 60;
     private int onionFrames = 1;
     private double onionOpacity = 50;
+    private int defaultEndFrameBindable = 60;
+    private bool defaultEndFrameSet = false;
 
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
@@ -110,18 +112,28 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
     }
 
+    public int DefaultEndFrameBindable
+    {
+        get => defaultEndFrameBindable;
+        set
+        {
+            if (Document.BlockingUpdateableChangeActive)
+                return;
+
+            Internals.ActionAccumulator.AddFinishedActions(new SetDefaultEndFrame_Action(value));
+        }
+    }
+
     public int FirstVisibleFrame => cachedFirstFrame ??= keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 1;
 
     public int LastFrame => cachedLastFrame ??= keyFrames.Count > 0
         ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable)
-        : DefaultEndFrame;
+        : DefaultEndFrameBindable;
 
     public int FramesCount => LastFrame - 1;
 
     private double ActiveNormalizedTime => (double)(ActiveFrameBindable - 1) / (FramesCount - 1);
 
-    private int DefaultEndFrame => FrameRateBindable; // 1 second
-
     public AnimationDataViewModel(DocumentViewModel document, DocumentInternalParts internals)
     {
         Document = document;
@@ -192,8 +204,27 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     public void SetFrameRate(int newFrameRate)
     {
         frameRateBindable = newFrameRate;
+        if (!defaultEndFrameSet)
+        {
+            defaultEndFrameBindable = frameRateBindable;
+            defaultEndFrameSet = true;
+        }
+
         OnPropertyChanged(nameof(FrameRateBindable));
-        OnPropertyChanged(nameof(DefaultEndFrame));
+        OnPropertyChanged(nameof(DefaultEndFrameBindable));
+        OnPropertyChanged(nameof(LastFrame));
+        OnPropertyChanged(nameof(FramesCount));
+    }
+
+    public void SetDefaultEndFrame(int newDefaultEndFrame)
+    {
+        if (newDefaultEndFrame < 0)
+            return;
+
+        defaultEndFrameBindable = newDefaultEndFrame;
+        defaultEndFrameSet = true;
+        cachedLastFrame = null;
+        OnPropertyChanged(nameof(DefaultEndFrameBindable));
         OnPropertyChanged(nameof(LastFrame));
         OnPropertyChanged(nameof(FramesCount));
     }
@@ -442,7 +473,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     {
         return keyFrames.Count > 0
             ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable)
-            : DefaultEndFrame;
+            : DefaultEndFrameBindable;
     }
 
     public int GetVisibleFramesCount()

+ 1 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -554,6 +554,7 @@ internal partial class DocumentViewModel
         animData.FrameRate = animationData.FrameRate;
         animData.OnionFrames = animationData.OnionFrames;
         animData.OnionOpacity = animationData.OnionOpacity;
+        animData.DefaultEndFrame = animationData.DefaultEndFrame;
         BuildKeyFrames(animationData.KeyFrames, animData, graph, nodeIdMap, keyFrameIds);
 
         return animData;

+ 1 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -480,6 +480,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
             acc.AddActions(new SetFrameRate_Action(data.FrameRate));
             acc.AddActions(new SetOnionSettings_Action(data.OnionFrames, data.OnionOpacity));
+            acc.AddActions(new SetDefaultEndFrame_Action(data.DefaultEndFrame));
             foreach (var keyFrame in data.KeyFrameGroups)
             {
                 if (keyFrame is GroupKeyFrameBuilder group)

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

@@ -17,7 +17,7 @@
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateCel}"
         Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRateBindable, Mode=TwoWay}"
-        DefaultEndFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.DefaultEndFrame}"
+        DefaultEndFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.DefaultEndFrameBindable, Mode=TwoWay}"
         OnionSkinningEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionSkinningEnabledBindable, Mode=TwoWay}"
         OnionFrames="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionFramesBindable, Mode=TwoWay}"
         OnionOpacity="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.OnionOpacityBindable, Mode=TwoWay}"

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit 1c7d1a546ef948a825137eb3b147f05076cce843
+Subproject commit d7a83f53f4a0e6a0e0d011cb045ab1f2075e759b