Browse Source

Customizable spritesheet export

flabbet 1 year ago
parent
commit
4d64fb23de

+ 1 - 1
src/PixiEditor.AvaloniaUI/Helpers/SpriteSheetUtility.cs

@@ -26,6 +26,6 @@ public static class SpriteSheetUtility
             }
         }
 
-        return (optimalRows, optimalColumns);
+        return (Math.Max(optimalRows, 1), Math.Max(optimalColumns, 1));
     }
 }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Helpers/SupportedFilesHelper.cs

@@ -64,7 +64,7 @@ internal class SupportedFilesHelper
 
     public static List<IoFileType> GetAllSupportedFileTypes(FileTypeDialogDataSet.SetKind setKind)
     {
-        var allExts = FileTypes.Where(x => x.SetKind.HasFlag(setKind)).ToList();
+        var allExts = FileTypes.Where(x => setKind.HasFlag(x.SetKind)).ToList();
         return allExts;
     }
 

+ 26 - 19
src/PixiEditor.AvaloniaUI/Models/Files/ImageFileType.cs

@@ -7,6 +7,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.AvaloniaUI.Models.Files;
@@ -24,7 +25,7 @@ internal abstract class ImageFileType : IoFileType
         if (exportConfig.ExportAsSpriteSheet)
         {
             finalSurface = GenerateSpriteSheet(document, exportConfig);
-            if(finalSurface == null)
+            if (finalSurface == null)
                 return SaveResult.UnknownError;
         }
         else
@@ -32,7 +33,13 @@ internal abstract class ImageFileType : IoFileType
             var maybeBitmap = document.TryRenderWholeImage();
             if (maybeBitmap.IsT0)
                 return SaveResult.ConcurrencyError;
+            
             finalSurface = maybeBitmap.AsT1;
+            if (maybeBitmap.AsT1.Size != exportConfig.ExportSize)
+            {
+                finalSurface = finalSurface.ResizeNearestNeighbor(exportConfig.ExportSize);
+                maybeBitmap.AsT1.Dispose();
+            }
         }
 
         EncodedImageFormat mappedFormat = EncodedImageFormat;
@@ -43,9 +50,9 @@ internal abstract class ImageFileType : IoFileType
         }
 
         UniversalFileEncoder encoder = new(mappedFormat);
-        var result = await TrySaveAs(encoder, pathWithExtension, finalSurface, exportConfig);
+        var result = await TrySaveAs(encoder, pathWithExtension, finalSurface);
         finalSurface.Dispose();
-        
+
         return result;
     }
 
@@ -54,38 +61,38 @@ internal abstract class ImageFileType : IoFileType
         if (document is null)
             return null;
 
-        int framesCount = document.AnimationDataViewModel.FramesCount;
-
-        int rows, columns;
-        if(config.SpriteSheetRows == 0 || config.SpriteSheetColumns == 0)
-            (rows, columns) = SpriteSheetUtility.CalculateGridDimensionsAuto(framesCount);
-        else
-            (rows, columns) = (config.SpriteSheetRows, config.SpriteSheetColumns);
+        var (rows, columns) = (config.SpriteSheetRows, config.SpriteSheetColumns);
+        
+        rows = Math.Max(1, rows);
+        columns = Math.Max(1, columns);
 
-        Surface surface = new Surface(new VecI(document.Width * columns, document.Height * rows));
+        Surface surface = new Surface(new VecI(config.ExportSize.X * columns, config.ExportSize.Y * rows));
 
         document.RenderFramesProgressive((frame, index) =>
         {
             int x = index % columns;
             int y = index / columns;
-            surface!.DrawingSurface.Canvas.DrawSurface(frame.DrawingSurface, x * document.Width, y * document.Height);
+            Surface target = frame;
+            if (config.ExportSize != frame.Size)
+            {
+               target =
+                    frame.ResizeNearestNeighbor(new VecI(config.ExportSize.X, config.ExportSize.Y));
+            }
+            
+            surface!.DrawingSurface.Canvas.DrawSurface(target.DrawingSurface, x * config.ExportSize.X, y * config.ExportSize.Y);
+            target.Dispose();
         });
-        
+
         return surface;
     }
 
     /// <summary>
     /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
-    private static async Task<SaveResult> TrySaveAs(IFileEncoder encoder, string savePath, Surface bitmap,
-        ExportConfig config)
+    private static async Task<SaveResult> TrySaveAs(IFileEncoder encoder, string savePath, Surface bitmap)
     {
         try
         {
-            VecI? exportSize = config.ExportSize;
-            if (exportSize is not null && exportSize != bitmap.Size)
-                bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
-
             if (!encoder.SupportsTransparency)
                 bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/Models/Files/VideoFileType.cs

@@ -13,9 +13,9 @@ internal abstract class VideoFileType : IoFileType
         
         document.RenderFrames(Paths.TempRenderingPath, surface =>
         {
-            if (config.ExportSize is not null && config.ExportSize != surface.Size)
+            if (config.ExportSize != surface.Size)
             {
-                return surface.ResizeNearestNeighbor(config.ExportSize ?? surface.Size);
+                return surface.ResizeNearestNeighbor(config.ExportSize);
             }
 
             return surface;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/IO/ExportConfig.cs

@@ -7,7 +7,7 @@ namespace PixiEditor.AvaloniaUI.Models.IO;
 public class ExportConfig
 {
    public static ExportConfig Empty { get; } = new ExportConfig();
-   public VecI? ExportSize { get; set; }
+   public VecI ExportSize { get; set; }
    public bool ExportAsSpriteSheet { get; set; } = false;
    public int SpriteSheetColumns { get; set; }
    public int SpriteSheetRows { get; set; }

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

@@ -45,8 +45,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         }
     }
 
-    public int FirstFrame => keyFrames.Min(x => x.StartFrameBindable);
-    public int LastFrame => keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
+    public int FirstFrame => keyFrames.Count > 0 ? keyFrames.Min(x => x.StartFrameBindable) : 0;
+    public int LastFrame => keyFrames.Count > 0 ? keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable) : 0;
     public int FramesCount => LastFrame - FirstFrame; 
 
     public AnimationDataViewModel(DocumentViewModel document, DocumentInternalParts internals)

+ 3 - 1
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFileDialog.cs

@@ -110,10 +110,12 @@ internal class ExportFileDialog : CustomDialog
             {
                 Size = new VecI(FileWidth, FileHeight),
                 OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
-                FrameRate = 60
+                FrameRate = document.AnimationDataViewModel.FrameRate
             }
             : null;
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;
+            ExportConfig.SpriteSheetColumns = popup.SpriteSheetColumns;
+            ExportConfig.SpriteSheetRows = popup.SpriteSheetRows;
         }
 
         return result;

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml

@@ -27,8 +27,8 @@
                     </TabItem>
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                         <StackPanel>
-                            <input:NumberInput Min="0" />
-                            <input:NumberInput Min="0" />
+                            <input:NumberInput Min="0" Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetRows, Mode=TwoWay}"/>
+                            <input:NumberInput Min="0" Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetColumns, Mode=TwoWay}"/>
                         </StackPanel>
                     </TabItem>
                 </TabControl.Items>

+ 61 - 7
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -53,6 +53,26 @@ internal partial class ExportFilePopup : PixiEditorPopup
         AvaloniaProperty.Register<ExportFilePopup, bool>(
             nameof(IsGeneratingPreview), false);
 
+    public static readonly StyledProperty<int> SpriteSheetColumnsProperty =
+        AvaloniaProperty.Register<ExportFilePopup, int>(
+            nameof(SpriteSheetColumns), 1);
+
+    public static readonly StyledProperty<int> SpriteSheetRowsProperty =
+        AvaloniaProperty.Register<ExportFilePopup, int>(
+            nameof(SpriteSheetRows), 1);
+
+    public int SpriteSheetRows
+    {
+        get => GetValue(SpriteSheetRowsProperty);
+        set => SetValue(SpriteSheetRowsProperty, value);
+    }
+
+    public int SpriteSheetColumns
+    {
+        get => GetValue(SpriteSheetColumnsProperty);
+        set => SetValue(SpriteSheetColumnsProperty, value);
+    }
+
     public bool IsGeneratingPreview
     {
         get => GetValue(IsGeneratingPreviewProperty);
@@ -130,6 +150,8 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
         SaveWidthProperty.Changed.Subscribe(RerenderPreview);
         SaveHeightProperty.Changed.Subscribe(RerenderPreview);
+        SpriteSheetColumnsProperty.Changed.Subscribe(RerenderPreview);
+        SpriteSheetRowsProperty.Changed.Subscribe(RerenderPreview);
         SelectedExportIndexProperty.Changed.Subscribe(RerenderPreview);
     }
 
@@ -154,6 +176,12 @@ internal partial class ExportFilePopup : PixiEditorPopup
         };
         videoPreviewTimer.Tick += OnVideoPreviewTimerOnTick;
 
+        int framesCount = document.AnimationDataViewModel.FramesCount;
+
+        var (rows, columns) = SpriteSheetUtility.CalculateGridDimensionsAuto(framesCount);
+        SpriteSheetColumns = columns;
+        SpriteSheetRows = rows;
+
         RenderPreview();
     }
 
@@ -218,16 +246,42 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
         if (IsSpriteSheetExport)
         {
-            //GenerateSpriteSheetPreview();
+            GenerateSpriteSheetPreview();
+        }
+        else
+        {
+            var rendered = document.TryRenderWholeImage();
+            if (rendered.IsT1)
+            {
+                VecI previewSize = CalculatePreviewSize(rendered.AsT1.Size);
+                ExportPreview = rendered.AsT1.ResizeNearestNeighbor(previewSize);
+                rendered.AsT1.Dispose();
+            }
         }
 
-        var rendered = document.TryRenderWholeImage();
-        if (rendered.IsT1)
+        IsGeneratingPreview = false;
+    }
+
+    private void GenerateSpriteSheetPreview()
+    {
+        int clampedColumns = Math.Max(SpriteSheetColumns, 1);
+        int clampedRows = Math.Max(SpriteSheetRows, 1);
+        
+        VecI previewSize = CalculatePreviewSize(new VecI(SaveWidth * clampedColumns, SaveHeight * clampedRows));
+        VecI singleFrameSize = new VecI(previewSize.X / Math.Max(clampedColumns, 1), previewSize.Y / Math.Max(clampedRows, 1));
+        if (previewSize != ExportPreview.Size)
         {
-            VecI previewSize = CalculatePreviewSize(rendered.AsT1.Size);
-            ExportPreview = rendered.AsT1.ResizeNearestNeighbor(previewSize);
-            rendered.AsT1.Dispose();
-            IsGeneratingPreview = false;
+            ExportPreview?.Dispose();
+            ExportPreview = new Surface(previewSize);
+
+            document.RenderFramesProgressive((frame, index) =>
+            {
+                int x = index % clampedColumns;
+                int y = index / clampedColumns;
+                var resized = frame.ResizeNearestNeighbor(new VecI(singleFrameSize.X, singleFrameSize.Y));
+                ExportPreview!.DrawingSurface.Canvas.DrawSurface(resized.DrawingSurface, x * singleFrameSize.X, y * singleFrameSize.Y);
+                resized.Dispose();
+            });
         }
     }
 

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

@@ -16,6 +16,7 @@
         KeyFrames="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.KeyFrames}" 
         ActiveFrame="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Mode=TwoWay}"
         NewKeyFrameCommand="{xaml:Command PixiEditor.Animation.CreateRasterKeyFrame}"
+        Fps="{Binding DocumentManagerSubViewModel.ActiveDocument.AnimationDataViewModel.FrameRate, Mode=TwoWay}"
         DuplicateKeyFrameCommand="{xaml:Command PixiEditor.Animation.DuplicateRasterKeyFrame}"
         DeleteKeyFrameCommand="{xaml:Command PixiEditor.Animation.DeleteKeyFrames, UseProvided=True}"
         ChangeKeyFramesLengthCommand="{xaml:Command PixiEditor.Animation.ChangeKeyFramesStartPos, UseProvided=True}"/>