Browse Source

Export tilesheet logic

flabbet 1 year ago
parent
commit
515340e3f5

+ 0 - 8
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -9,14 +9,6 @@
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
                     xmlns:ui="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI"
                     xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input">
-    <Design.PreviewWith>
-        <animations:Timeline>
-            <animations:Timeline.KeyFrames>
-                <document:RasterKeyFrameViewModel DurationBindable="100" />
-            </animations:Timeline.KeyFrames>
-        </animations:Timeline>
-    </Design.PreviewWith>
-
     <ControlTheme TargetType="animations:Timeline" x:Key="{x:Type animations:Timeline}">
         <Setter Property="Template">
             <ControlTemplate>

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

@@ -19,9 +19,11 @@ using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Document.TransformOverlays;
 using PixiEditor.AvaloniaUI.Views.Overlays.SymmetryOverlay;
+using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -686,6 +688,34 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
     }
 
+    public Image[] RenderFrames()
+    {
+        if (AnimationDataViewModel.KeyFrames.Count == 0)
+            return[];
+
+        var keyFrames = AnimationDataViewModel.KeyFrames;
+        var firstFrame = keyFrames.Min(x => x.StartFrameBindable);
+        var lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
+
+        int activeFrame = AnimationDataViewModel.ActiveFrameBindable;
+
+        Image[] images = new Image[lastFrame - firstFrame];
+        for (int i = firstFrame; i < lastFrame; i++)
+        {
+            Internals.Tracker.ProcessActionsSync(new List<IAction> { new ActiveFrame_Action(i), new EndActiveFrame_Action() });
+            var surface = TryRenderWholeImage();
+            if (surface.IsT0)
+            {
+                continue;
+            }
+            
+            images[i] = surface.AsT1.DrawingSurface.Snapshot();
+        }
+
+        Internals.Tracker.ProcessActionsSync(new List<IAction> { new ActiveFrame_Action(activeFrame), new EndActiveFrame_Action() });
+        return images;
+    }
+
     public void RenderFrames(string tempRenderingPath)
     {
         if (AnimationDataViewModel.KeyFrames.Count == 0)
@@ -708,7 +738,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         
         for (int i = firstFrame; i < lastFrame; i++)
         {
-            Internals.Tracker.ProcessActionsSync(new[] { new ActiveFrame_Action(i) });
+            Internals.Tracker.ProcessActionsSync(new List<IAction> { new ActiveFrame_Action(i), new EndActiveFrame_Action() });
             var surface = TryRenderWholeImage();
             if (surface.IsT0)
             {
@@ -720,7 +750,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             stream.Position = 0;
         }
         
-        Internals.Tracker.ProcessActionsSync(new[] { new ActiveFrame_Action(activeFrame) });
+        Internals.Tracker.ProcessActionsSync(new List<IAction> { new ActiveFrame_Action(activeFrame), new EndActiveFrame_Action() });
     }
 
     private static void ClearTempFolder(string tempRenderingPath)

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

@@ -1,11 +1,9 @@
 using System.Collections.ObjectModel;
-using System.Collections.Specialized;
 using System.ComponentModel;
-using PixiEditor.AvaloniaUI.Models.Handlers;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 
-internal class KeyFrameCollection : ObservableCollection<KeyFrameViewModel>
+internal class KeyFrameCollection : ObservableCollection<KeyFrameGroupViewModel>
 {
     public KeyFrameCollection()
     {

+ 54 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -1,7 +1,11 @@
-using PixiEditor.AnimationRenderer.Core;
+using ChunkyImageLib;
+using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 
@@ -45,6 +49,55 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
         
         activeDocument.Operations.SetActiveFrame(newFrame);
     }
+    
+    [Command.Basic("PixiEditor.Animation.ExportSpriteSheet", "Export Sprite Sheet", "Export the sprite sheet")]
+    public async Task ExportSpriteSheet()
+    {
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        
+        if (document is null)
+            return;
+        
+        Image[] images = document.RenderFrames();
+        // calculate rows and columns so as little empty space is left
+        // For example 3 frames should be in 3x1 grid because 2x2 would leave 1 empty space, but 4 frames should be in 2x2 grid
+        (int rows, int columns) grid = CalculateGridDimensions(images.Length);
+        
+        using Surface surface = new Surface(new VecI(document.Width * grid.columns, document.Height * grid.rows));
+        for (int i = 0; i < images.Length; i++)
+        {
+            int x = i % grid.columns;
+            int y = i / grid.columns;
+            surface.DrawingSurface.Canvas.DrawImage(images[i], x * document.Width, y * document.Height);
+        }
+        
+        surface.SaveToDesktop();
+    }
+
+    private (int rows, int columns) CalculateGridDimensions(int imagesLength)
+    {
+        int optimalRows = 1;
+        int optimalColumns = imagesLength;
+        int minDifference = Math.Abs(optimalRows - optimalColumns);
+
+        for (int rows = 1; rows <= Math.Sqrt(imagesLength); rows++)
+        {
+            int columns = (int)Math.Ceiling((double)imagesLength / rows);
+
+            if (rows * columns >= imagesLength)
+            {
+                int difference = Math.Abs(rows - columns);
+                if (difference < minDifference)
+                {
+                    minDifference = difference;
+                    optimalRows = rows;
+                    optimalColumns = columns;
+                }
+            }
+        }
+
+        return (optimalRows, optimalColumns);
+    }
 
     private static int GetActiveFrame(DocumentViewModel activeDocument, Guid targetLayer)
     {

+ 0 - 1
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml

@@ -33,7 +33,6 @@
                                 <InvokeCommandAction Command="{Binding SetBestPercentageCommand, ElementName=saveFilePopup}"/>
                             </EventTriggerBehavior>
                         </Interaction.Behaviors>
-                        <TextBlock Text="&#xE869;" FontFamily="{DynamicResource Feather}"/>
                     </TextBlock>
             </Grid>
         </Border>