Browse Source

FFMpeg rendering

flabbet 1 year ago
parent
commit
e3213557e1

+ 6 - 0
src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.AnimationRenderer.Core;
+
+public interface IAnimationRenderer
+{
+    public Task<bool> RenderAsync(string framesPath, int frameRate = 60);
+}

+ 9 - 0
src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj

@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+</Project>

+ 28 - 0
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -0,0 +1,28 @@
+using FFMpegCore;
+using FFMpegCore.Enums;
+using PixiEditor.AnimationRenderer.Core;
+
+namespace PixiEditor.AnimationRenderer.FFmpeg;
+
+public class FFMpegRenderer : IAnimationRenderer
+{
+    public async Task<bool> RenderAsync(string framesPath, int frameRate = 60)
+    {
+        string[] frames = Directory.GetFiles(framesPath, "*.png");
+        if (frames.Length == 0)
+        {
+            return false;
+        }
+        
+        GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = @"C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin" });
+        
+        return await FFMpegArguments
+            .FromConcatInput(frames)
+            .OutputToFile($"{framesPath}/output.mp4", true, options =>
+            {
+                options.WithVideoCodec(VideoCodec.LibX264)
+                    .WithFramerate(frameRate);
+            })
+            .ProcessAsynchronously();
+    }
+}

+ 19 - 0
src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <PackageReference Include="FFMpegCore" Version="5.1.0" />
+    </ItemGroup>
+  
+  <!--TODO: Publish-time binaries embedding-->
+
+</Project>

+ 3 - 0
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -1,6 +1,8 @@
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.AnimationRenderer.Core;
+using PixiEditor.AnimationRenderer.FFmpeg;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Commands;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.ExtensionServices;
 using PixiEditor.AvaloniaUI.Models.ExtensionServices;
@@ -113,6 +115,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<PaletteFileParser, PixiPaletteParser>()
             .AddSingleton<PaletteFileParser, PixiPaletteParser>()
             // Palette data sources
             // Palette data sources
             .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>()
             .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>()
+            .AddSingleton<IAnimationRenderer, FFMpegRenderer>()
             .AddMenuBuilders();
             .AddMenuBuilders();
     }
     }
 
 

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

@@ -118,7 +118,7 @@ internal class Exporter
             return TrySaveAsPixi(document, pathWithExtension);
             return TrySaveAsPixi(document, pathWithExtension);
         }
         }
         
         
-        var maybeBitmap = document.MaybeRenderWholeImage();
+        var maybeBitmap = document.TryRenderWholeImage();
         if (maybeBitmap.IsT0)
         if (maybeBitmap.IsT0)
             return SaveResult.ConcurrencyError;
             return SaveResult.ConcurrencyError;
         var bitmap = maybeBitmap.AsT1;
         var bitmap = maybeBitmap.AsT1;

+ 2 - 1
src/PixiEditor.AvaloniaUI/Models/IO/Paths.cs

@@ -6,7 +6,6 @@ public static class Paths
 {
 {
     public static string DataResourceUri { get; } = $"avares://{typeof(Paths).Assembly.GetName().Name}/Data/";
     public static string DataResourceUri { get; } = $"avares://{typeof(Paths).Assembly.GetName().Name}/Data/";
     public static string DataFullPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Data");
     public static string DataFullPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Data");
-
     public static string ExtensionPackagesPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Extensions");
     public static string ExtensionPackagesPath { get; } = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Extensions");
     public static string UserExtensionsPath { get; } = Path.Combine(
     public static string UserExtensionsPath { get; } = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
@@ -18,4 +17,6 @@ public static class Paths
 
 
     public static string InternalResourceDataPath { get; } =
     public static string InternalResourceDataPath { get; } =
         $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data";
         $"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Data";
+    
+    public static string TempRenderingPath { get; } = Path.Combine(Path.GetTempPath(), "PixiEditor", "Rendering");
 }
 }

+ 2 - 0
src/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -80,6 +80,8 @@
   <ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\..\PixiDocks\src\PixiDocks.Avalonia\PixiDocks.Avalonia.csproj"/>
     <ProjectReference Include="..\..\..\PixiDocks\src\PixiDocks.Avalonia\PixiDocks.Avalonia.csproj"/>
     <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj"/>
     <ProjectReference Include="..\ChunkyImageLib\ChunkyImageLib.csproj"/>
+    <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj" />
+    <ProjectReference Include="..\PixiEditor.AnimationRenderer.FFmpeg\PixiEditor.AnimationRenderer.FFmpeg.csproj" />
     <ProjectReference Include="..\PixiEditor.ChangeableDocument.Gen\PixiEditor.ChangeableDocument.Gen.csproj"/>
     <ProjectReference Include="..\PixiEditor.ChangeableDocument.Gen\PixiEditor.ChangeableDocument.Gen.csproj"/>
     <ProjectReference Include="..\PixiEditor.ChangeableDocument\PixiEditor.ChangeableDocument.csproj"/>
     <ProjectReference Include="..\PixiEditor.ChangeableDocument\PixiEditor.ChangeableDocument.csproj"/>
     <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj"/>
     <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj"/>

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

@@ -34,7 +34,7 @@ internal partial class DocumentViewModel
         {
         {
             Width = Width, Height = Height,
             Width = Width, Height = Height,
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
             Swatches = ToCollection(Swatches), Palette = ToCollection(Palette),
-            RootFolder = root, PreviewImage = (MaybeRenderWholeImage().Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
+            RootFolder = root, PreviewImage = (TryRenderWholeImage().Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
             ReferenceLayer = GetReferenceLayer(doc)
             ReferenceLayer = GetReferenceLayer(doc)
         };
         };
 
 

+ 120 - 30
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -53,6 +53,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
 
 
     private string coordinatesString = "";
     private string coordinatesString = "";
+
     public string CoordinatesString
     public string CoordinatesString
     {
     {
         get => coordinatesString;
         get => coordinatesString;
@@ -60,6 +61,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     }
     }
 
 
     private string? fullFilePath = null;
     private string? fullFilePath = null;
+
     public string? FullFilePath
     public string? FullFilePath
     {
     {
         get => fullFilePath;
         get => fullFilePath;
@@ -69,13 +71,14 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             OnPropertyChanged(nameof(FileName));
             OnPropertyChanged(nameof(FileName));
         }
         }
     }
     }
-    
+
     public string FileName
     public string FileName
     {
     {
         get => fullFilePath is null ? new LocalizedString("UNNAMED") : Path.GetFileName(fullFilePath);
         get => fullFilePath is null ? new LocalizedString("UNNAMED") : Path.GetFileName(fullFilePath);
     }
     }
 
 
     private Guid? lastChangeOnSave = null;
     private Guid? lastChangeOnSave = null;
+
     public bool AllChangesSaved
     public bool AllChangesSaved
     {
     {
         get
         get
@@ -87,28 +90,33 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public DateTime OpenedUTC { get; } = DateTime.UtcNow;
     public DateTime OpenedUTC { get; } = DateTime.UtcNow;
 
 
     private bool horizontalSymmetryAxisEnabled;
     private bool horizontalSymmetryAxisEnabled;
+
     public bool HorizontalSymmetryAxisEnabledBindable
     public bool HorizontalSymmetryAxisEnabledBindable
     {
     {
         get => horizontalSymmetryAxisEnabled;
         get => horizontalSymmetryAxisEnabled;
         set
         set
         {
         {
             if (!Internals.ChangeController.IsChangeActive)
             if (!Internals.ChangeController.IsChangeActive)
-                Internals.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Horizontal, value));
+                Internals.ActionAccumulator.AddFinishedActions(
+                    new SymmetryAxisState_Action(SymmetryAxisDirection.Horizontal, value));
         }
         }
     }
     }
 
 
     private bool verticalSymmetryAxisEnabled;
     private bool verticalSymmetryAxisEnabled;
+
     public bool VerticalSymmetryAxisEnabledBindable
     public bool VerticalSymmetryAxisEnabledBindable
     {
     {
         get => verticalSymmetryAxisEnabled;
         get => verticalSymmetryAxisEnabled;
         set
         set
         {
         {
             if (!Internals.ChangeController.IsChangeActive)
             if (!Internals.ChangeController.IsChangeActive)
-                Internals.ActionAccumulator.AddFinishedActions(new SymmetryAxisState_Action(SymmetryAxisDirection.Vertical, value));
+                Internals.ActionAccumulator.AddFinishedActions(
+                    new SymmetryAxisState_Action(SymmetryAxisDirection.Vertical, value));
         }
         }
     }
     }
 
 
-    public bool AnySymmetryAxisEnabledBindable => HorizontalSymmetryAxisEnabledBindable || VerticalSymmetryAxisEnabledBindable;
+    public bool AnySymmetryAxisEnabledBindable =>
+        HorizontalSymmetryAxisEnabledBindable || VerticalSymmetryAxisEnabledBindable;
 
 
     private VecI size = new VecI(64, 64);
     private VecI size = new VecI(64, 64);
     public int Width => size.X;
     public int Width => size.X;
@@ -124,7 +132,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     private readonly HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
     private readonly HashSet<StructureMemberViewModel> softSelectedStructureMembers = new();
 
 
     public bool UpdateableChangeActive => Internals.ChangeController.IsChangeActive;
     public bool UpdateableChangeActive => Internals.ChangeController.IsChangeActive;
-    public bool PointerDragChangeInProgress => Internals.ChangeController.IsChangeActive && Internals.ChangeController.LeftMousePressed;
+
+    public bool PointerDragChangeInProgress =>
+        Internals.ChangeController.IsChangeActive && Internals.ChangeController.LeftMousePressed;
+
     public bool HasSavedUndo => Internals.Tracker.HasSavedUndo;
     public bool HasSavedUndo => Internals.Tracker.HasSavedUndo;
     public bool HasSavedRedo => Internals.Tracker.HasSavedRedo;
     public bool HasSavedRedo => Internals.Tracker.HasSavedRedo;
 
 
@@ -133,8 +144,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     public DocumentToolsModule Tools { get; }
     public DocumentToolsModule Tools { get; }
     public DocumentOperationsModule Operations { get; }
     public DocumentOperationsModule Operations { get; }
     public DocumentEventsModule EventInlet { get; }
     public DocumentEventsModule EventInlet { get; }
-    public ActionDisplayList ActionDisplays { get; } = new(() => ViewModelMain.Current.NotifyToolActionDisplayChanged());
+
+    public ActionDisplayList ActionDisplays { get; } =
+        new(() => ViewModelMain.Current.NotifyToolActionDisplayChanged());
+
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
     public IStructureMemberHandler? SelectedStructureMember { get; private set; } = null;
+
     //TODO: It was DrawingSurface before, check if it's correct
     //TODO: It was DrawingSurface before, check if it's correct
     public Dictionary<ChunkResolution, Surface> Surfaces { get; set; } = new()
     public Dictionary<ChunkResolution, Surface> Surfaces { get; set; } = new()
     {
     {
@@ -201,7 +216,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);
         TransformViewModel.TransformMoved += (_, args) => Internals.ChangeController.TransformMovedInlet(args);
 
 
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel = new();
-        LineToolOverlayViewModel.LineMoved += (_, args) => Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
+        LineToolOverlayViewModel.LineMoved += (_, args) =>
+            Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
 
 
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
         VecI previewSize = StructureMemberViewModel.CalculatePreviewSize(SizeBindable);
         PreviewSurface = new Surface(new VecI(previewSize.X, previewSize.Y));
         PreviewSurface = new Surface(new VecI(previewSize.X, previewSize.Y));
@@ -223,8 +239,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
         var acc = viewModel.Internals.ActionAccumulator;
         var acc = viewModel.Internals.ActionAccumulator;
 
 
-        viewModel.Internals.ChangeController.SymmetryDraggedInlet(new SymmetryAxisDragInfo(SymmetryAxisDirection.Horizontal, builderInstance.Height / 2));
-        viewModel.Internals.ChangeController.SymmetryDraggedInlet(new SymmetryAxisDragInfo(SymmetryAxisDirection.Vertical, builderInstance.Width / 2));
+        viewModel.Internals.ChangeController.SymmetryDraggedInlet(
+            new SymmetryAxisDragInfo(SymmetryAxisDirection.Horizontal, builderInstance.Height / 2));
+        viewModel.Internals.ChangeController.SymmetryDraggedInlet(
+            new SymmetryAxisDragInfo(SymmetryAxisDirection.Vertical, builderInstance.Width / 2));
 
 
         acc.AddActions(
         acc.AddActions(
             new SymmetryAxisPosition_Action(SymmetryAxisDirection.Horizontal, (double)builderInstance.Height / 2),
             new SymmetryAxisPosition_Action(SymmetryAxisDirection.Horizontal, (double)builderInstance.Height / 2),
@@ -235,7 +253,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (builderInstance.ReferenceLayer is { } refLayer)
         if (builderInstance.ReferenceLayer is { } refLayer)
         {
         {
             acc
             acc
-                .AddActions(new SetReferenceLayer_Action(refLayer.Shape, refLayer.ImageBgra8888Bytes.ToImmutableArray(), refLayer.ImageSize));
+                .AddActions(new SetReferenceLayer_Action(refLayer.Shape, refLayer.ImageBgra8888Bytes.ToImmutableArray(),
+                    refLayer.ImageSize));
         }
         }
 
 
         viewModel.Swatches = new ObservableCollection<PaletteColor>(builderInstance.Swatches);
         viewModel.Swatches = new ObservableCollection<PaletteColor>(builderInstance.Swatches);
@@ -251,15 +270,18 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         void AddMember(Guid parentGuid, DocumentViewModelBuilder.StructureMemberBuilder member)
         void AddMember(Guid parentGuid, DocumentViewModelBuilder.StructureMemberBuilder member)
         {
         {
             acc.AddActions(
             acc.AddActions(
-                new CreateStructureMember_Action(parentGuid, member.GuidValue, 0, member is DocumentViewModelBuilder.LayerBuilder ? StructureMemberType.Layer : StructureMemberType.Folder),
+                new CreateStructureMember_Action(parentGuid, member.GuidValue, 0,
+                    member is DocumentViewModelBuilder.LayerBuilder
+                        ? StructureMemberType.Layer
+                        : StructureMemberType.Folder),
                 new StructureMemberName_Action(member.GuidValue, member.Name)
                 new StructureMemberName_Action(member.GuidValue, member.Name)
             );
             );
 
 
             if (!member.IsVisible)
             if (!member.IsVisible)
                 acc.AddActions(new StructureMemberIsVisible_Action(member.IsVisible, member.GuidValue));
                 acc.AddActions(new StructureMemberIsVisible_Action(member.IsVisible, member.GuidValue));
-            
+
             acc.AddActions(new StructureMemberBlendMode_Action(member.BlendMode, member.GuidValue));
             acc.AddActions(new StructureMemberBlendMode_Action(member.BlendMode, member.GuidValue));
-            
+
             acc.AddActions(new StructureMemberClipToMemberBelow_Action(member.ClipToMemberBelow, member.GuidValue));
             acc.AddActions(new StructureMemberClipToMemberBelow_Action(member.ClipToMemberBelow, member.GuidValue));
 
 
             if (member is DocumentViewModelBuilder.LayerBuilder layerBuilder)
             if (member is DocumentViewModelBuilder.LayerBuilder layerBuilder)
@@ -269,9 +291,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
 
             if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             {
             {
-                PasteImage(member.GuidValue, layer.Surface, layer.Width, layer.Height, layer.OffsetX, layer.OffsetY, false);
+                PasteImage(member.GuidValue, layer.Surface, layer.Width, layer.Height, layer.OffsetX, layer.OffsetY,
+                    false);
             }
             }
-            
+
             acc.AddActions(
             acc.AddActions(
                 new StructureMemberOpacity_Action(member.GuidValue, member.Opacity),
                 new StructureMemberOpacity_Action(member.GuidValue, member.Opacity),
                 new EndStructureMemberOpacity_Action());
                 new EndStructureMemberOpacity_Action());
@@ -296,10 +319,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             }
             }
         }
         }
 
 
-        void PasteImage(Guid guid, DocumentViewModelBuilder.SurfaceBuilder surface, int width, int height, int offsetX, int offsetY, bool onMask)
+        void PasteImage(Guid guid, DocumentViewModelBuilder.SurfaceBuilder surface, int width, int height, int offsetX,
+            int offsetY, bool onMask)
         {
         {
             acc.AddActions(
             acc.AddActions(
-                new PasteImage_Action(surface.Surface, new(new RectD(new VecD(offsetX, offsetY), new(width, height))), guid, true, onMask),
+                new PasteImage_Action(surface.Surface, new(new RectD(new VecD(offsetX, offsetY), new(width, height))),
+                    guid, true, onMask),
                 new EndPasteImage_Action());
                 new EndPasteImage_Action());
         }
         }
 
 
@@ -333,7 +358,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// Tries rendering the whole document
     /// Tries rendering the whole document
     /// </summary>
     /// </summary>
     /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
     /// <returns><see cref="Error"/> if the ChunkyImage was disposed, otherwise a <see cref="Surface"/> of the rendered document</returns>
-    public OneOf<Error, Surface> MaybeRenderWholeImage()
+    public OneOf<Error, Surface> TryRenderWholeImage()
     {
     {
         try
         try
         {
         {
@@ -343,13 +368,16 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
             {
                 for (int j = 0; j < sizeInChunks.Y; j++)
                 for (int j = 0; j < sizeInChunks.Y; j++)
                 {
                 {
-                    var maybeChunk = ChunkRenderer.MergeWholeStructure(new(i, j), ChunkResolution.Full, Internals.Tracker.Document.StructureRoot);
+                    var maybeChunk = ChunkRenderer.MergeWholeStructure(new(i, j), ChunkResolution.Full,
+                        Internals.Tracker.Document.StructureRoot);
                     if (maybeChunk.IsT1)
                     if (maybeChunk.IsT1)
                         continue;
                         continue;
                     using Chunk chunk = maybeChunk.AsT0;
                     using Chunk chunk = maybeChunk.AsT0;
-                    finalSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, i * ChunkyImage.FullChunkSize, j * ChunkyImage.FullChunkSize);
-                } 
+                    finalSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface,
+                        i * ChunkyImage.FullChunkSize, j * ChunkyImage.FullChunkSize);
+                }
             }
             }
+
             return finalSurface;
             return finalSurface;
         }
         }
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
@@ -362,7 +390,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// Takes the selected area and converts it into a surface
     /// Takes the selected area and converts it into a surface
     /// </summary>
     /// </summary>
     /// <returns><see cref="Error"/> on error, <see cref="None"/> for empty <see cref="Surface"/>, <see cref="Surface"/> otherwise.</returns>
     /// <returns><see cref="Error"/> on error, <see cref="None"/> for empty <see cref="Surface"/>, <see cref="Surface"/> otherwise.</returns>
-    public OneOf<Error, None, (Surface, RectI)> MaybeExtractSelectedArea(IStructureMemberHandler? layerToExtractFrom = null)
+    public OneOf<Error, None, (Surface, RectI)> MaybeExtractSelectedArea(
+        IStructureMemberHandler? layerToExtractFrom = null)
     {
     {
         layerToExtractFrom ??= SelectedStructureMember;
         layerToExtractFrom ??= SelectedStructureMember;
         if (layerToExtractFrom is not LayerViewModel layerVm)
         if (layerToExtractFrom is not LayerViewModel layerVm)
@@ -385,6 +414,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         {
         {
             return new Error();
             return new Error();
         }
         }
+
         if (memberImageBounds is null)
         if (memberImageBounds is null)
             return new None();
             return new None();
         bounds = bounds.Intersect(memberImageBounds.Value);
         bounds = bounds.Intersect(memberImageBounds.Value);
@@ -407,6 +437,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             output.Dispose();
             output.Dispose();
             return new Error();
             return new Error();
         }
         }
+
         output.DrawingSurface.Canvas.Restore();
         output.DrawingSurface.Canvas.Restore();
 
 
         return (output, bounds);
         return (output, bounds);
@@ -418,7 +449,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// <param name="includeReference">Should the color be picked from the reference layer</param>
     /// <param name="includeReference">Should the color be picked from the reference layer</param>
     /// <param name="includeCanvas">Should the color be picked from the canvas</param>
     /// <param name="includeCanvas">Should the color be picked from the canvas</param>
     /// <param name="referenceTopmost">Is the reference layer topmost. (Only affects the result is includeReference and includeCanvas are set.)</param>
     /// <param name="referenceTopmost">Is the reference layer topmost. (Only affects the result is includeReference and includeCanvas are set.)</param>
-    public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas, bool referenceTopmost = false)
+    public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas,
+        bool referenceTopmost = false)
     {
     {
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
             includeReference = false;
             includeReference = false;
@@ -435,12 +467,14 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 return ColorHelpers.BlendColors(referenceColor, canvasColor);
                 return ColorHelpers.BlendColors(referenceColor, canvasColor);
             }
             }
 
 
-            byte referenceAlpha = canvasColor.A == 0 ? referenceColor.A : (byte)(referenceColor.A * ReferenceLayerViewModel.TopMostOpacity);
+            byte referenceAlpha = canvasColor.A == 0
+                ? referenceColor.A
+                : (byte)(referenceColor.A * ReferenceLayerViewModel.TopMostOpacity);
 
 
             referenceColor = new Color(referenceColor.R, referenceColor.G, referenceColor.B, referenceAlpha);
             referenceColor = new Color(referenceColor.R, referenceColor.G, referenceColor.B, referenceAlpha);
             return ColorHelpers.BlendColors(canvasColor, referenceColor);
             return ColorHelpers.BlendColors(canvasColor, referenceColor);
-
         }
         }
+
         if (includeCanvas)
         if (includeCanvas)
             return PickColorFromCanvas((VecI)pos, scope);
             return PickColorFromCanvas((VecI)pos, scope);
         if (includeReference)
         if (includeReference)
@@ -453,7 +487,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         Surface? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
         Surface? bitmap = ReferenceLayerViewModel.ReferenceBitmap;
         if (bitmap is null)
         if (bitmap is null)
             return null;
             return null;
-        
+
         Matrix matrix = ReferenceLayerViewModel.ReferenceTransformMatrix;
         Matrix matrix = ReferenceLayerViewModel.ReferenceTransformMatrix;
         matrix = matrix.Invert();
         matrix = matrix.Invert();
         var transformed = matrix.Transform(new Point(pos.X, pos.Y));
         var transformed = matrix.Transform(new Point(pos.X, pos.Y));
@@ -474,7 +508,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (scope == DocumentScope.AllLayers)
             if (scope == DocumentScope.AllLayers)
             {
             {
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
                 VecI chunkPos = OperationHelper.GetChunkPos(pos, ChunkyImage.FullChunkSize);
-                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full, Internals.Tracker.Document.StructureRoot, new RectI(pos, VecI.One))
+                return ChunkRenderer.MergeWholeStructure(chunkPos, ChunkResolution.Full,
+                        Internals.Tracker.Document.StructureRoot, new RectI(pos, VecI.One))
                     .Match<Color>(
                     .Match<Color>(
                         (Chunk chunk) =>
                         (Chunk chunk) =>
                         {
                         {
@@ -501,6 +536,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     }
     }
 
 
     #region Internal Methods
     #region Internal Methods
+
     // these are intended to only be called from DocumentUpdater
     // these are intended to only be called from DocumentUpdater
 
 
     public void RaiseLayersChanged(LayersChangedEventArgs args) => LayersChanged?.Invoke(this, args);
     public void RaiseLayersChanged(LayersChangedEventArgs args) => LayersChanged?.Invoke(this, args);
@@ -573,8 +609,13 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     }
     }
 
 
     public void ClearSoftSelectedMembers() => softSelectedStructureMembers.Clear();
     public void ClearSoftSelectedMembers() => softSelectedStructureMembers.Clear();
-    public void AddSoftSelectedMember(IStructureMemberHandler member) => softSelectedStructureMembers.Add((StructureMemberViewModel)member);
-    public void RemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
+
+    public void AddSoftSelectedMember(IStructureMemberHandler member) =>
+        softSelectedStructureMembers.Add((StructureMemberViewModel)member);
+
+    public void RemoveSoftSelectedMember(StructureMemberViewModel member) =>
+        softSelectedStructureMembers.Remove(member);
+
     #endregion
     #endregion
 
 
     /// <summary>
     /// <summary>
@@ -604,7 +645,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             var foundMember = StructureHelper.Find(member);
             var foundMember = StructureHelper.Find(member);
             if (foundMember != null)
             if (foundMember != null)
             {
             {
-                if (foundMember is LayerViewModel layer && selectedMembers.Contains(foundMember.GuidValue) && !result.Contains(layer.GuidValue))
+                if (foundMember is LayerViewModel layer && selectedMembers.Contains(foundMember.GuidValue) &&
+                    !result.Contains(layer.GuidValue))
                 {
                 {
                     result.Add(layer.GuidValue);
                     result.Add(layer.GuidValue);
                 }
                 }
@@ -616,6 +658,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 }
                 }
             }
             }
         }
         }
+
         return result;
         return result;
     }
     }
 
 
@@ -642,4 +685,51 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             }
             }
         }
         }
     }
     }
+
+    public void RenderFrames(string tempRenderingPath)
+    {
+        if (AnimationDataViewModel.KeyFrames.Count == 0)
+            return;
+
+        if (!Directory.Exists(tempRenderingPath))
+        {
+            Directory.CreateDirectory(tempRenderingPath);
+        }
+        else
+        {
+            ClearTempFolder(tempRenderingPath);
+        }
+
+        var keyFrames = AnimationDataViewModel.KeyFrames;
+        var firstFrame = keyFrames.Min(x => x.StartFrame);
+        var lastFrame = keyFrames.Max(x => x.StartFrame + x.Duration);
+
+        int activeFrame = AnimationDataViewModel.ActiveFrameBindable;
+        
+        for (int i = firstFrame; i < lastFrame; i++)
+        {
+            Internals.Tracker.ProcessActionsSync(new[] { new ActiveFrame_Action(i) });
+            var surface = TryRenderWholeImage();
+            if (surface.IsT0)
+            {
+                continue;
+            }
+
+            using var stream = new FileStream(Path.Combine(tempRenderingPath, $"{i}.png"), FileMode.Create);
+            surface.AsT1.DrawingSurface.Snapshot().Encode().SaveTo(stream);
+            stream.Position = 0;
+        }
+        
+        Internals.Tracker.ProcessActionsSync(new[] { new ActiveFrame_Action(activeFrame) });
+    }
+
+    private static void ClearTempFolder(string tempRenderingPath)
+    {
+        string[] files = Directory.GetFiles(tempRenderingPath);
+        for (var i = 0; i < files.Length; i++)
+        {
+            var file = files[i];
+            File.Delete(file);
+        }
+    }
 }
 }

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

@@ -1,4 +1,6 @@
-using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AnimationRenderer.Core;
+using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
+using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
@@ -6,9 +8,22 @@ namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
 [Command.Group("PixiEditor.Animations", "ANIMATIONS")]
 [Command.Group("PixiEditor.Animations", "ANIMATIONS")]
 internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 internal class AnimationsViewModel : SubViewModel<ViewModelMain>
 {
 {
-    public AnimationsViewModel(ViewModelMain owner) : base(owner)
+    private IAnimationRenderer animationRenderer;
+    public AnimationsViewModel(ViewModelMain owner, IAnimationRenderer renderer) : base(owner)
     {
     {
+        animationRenderer = renderer;
+    }
+    
+    [Command.Basic("PixiEditor.Animations.RenderAnimation", "Render Animation (MP4)", "Renders the animation as an MP4 file")]
+    public async Task RenderAnimation()
+    {
+        var document = Owner.DocumentManagerSubViewModel.ActiveDocument;
+        
+        if (document is null)
+            return;
         
         
+        document.RenderFrames(Paths.TempRenderingPath);
+        await animationRenderer.RenderAsync(Paths.TempRenderingPath);
     }
     }
     
     
     [Command.Basic("PixiEditor.Animation.CreateRasterKeyFrame", "Create Raster Key Frame", "Create a raster key frame", Parameter = false)]
     [Command.Basic("PixiEditor.Animation.CreateRasterKeyFrame", "Create Raster Key Frame", "Create a raster key frame", Parameter = false)]

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

@@ -8,7 +8,6 @@ internal class ActiveFrame_UpdateableChange : UpdateableChange
     private int originalFrame;
     private int originalFrame;
     
     
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
-
     public ActiveFrame_UpdateableChange(int activeFrame)
     public ActiveFrame_UpdateableChange(int activeFrame)
     {
     {
         newFrame = activeFrame;
         newFrame = activeFrame;

+ 65 - 0
src/PixiEditor.sln

@@ -96,6 +96,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.MSPac
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Extensions.CommonApi.Diagnostics", "PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj", "{D72E70F3-BF37-432F-B78B-5B247C873852}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Extensions.CommonApi.Diagnostics", "PixiEditor.Extensions.CommonApi.Diagnostics\PixiEditor.Extensions.CommonApi.Diagnostics.csproj", "{D72E70F3-BF37-432F-B78B-5B247C873852}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.AnimationRenderer.Core", "PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj", "{9B552A44-9587-4410-8673-254B31E2E4F7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.AnimationRenderer.FFmpeg", "PixiEditor.AnimationRenderer.FFmpeg\PixiEditor.AnimationRenderer.FFmpeg.csproj", "{CD863C88-72E3-40F4-9AAE-5696BBB4460C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AnimationRendering", "AnimationRendering", "{2BA72059-FFD7-4887-AE88-269017198933}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
 		Debug|x64 = Debug|x64
@@ -1438,6 +1444,62 @@ Global
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.Build.0 = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Steam|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Debug|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Release|x64.ActiveCfg = Release|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Release|x64.Build.0 = Release|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Release|ARM64.Build.0 = Release|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Steam|x64.Build.0 = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{9B552A44-9587-4410-8673-254B31E2E4F7}.Steam|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Debug|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Release|x64.ActiveCfg = Release|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Release|x64.Build.0 = Release|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Release|ARM64.Build.0 = Release|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|x64.Build.0 = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -1481,6 +1543,9 @@ Global
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188} = {2CC7ED59-C25E-4EED-8FED-D48E13EB9CC0}
 		{AE200ADC-9E85-4275-A373-E975CD6D518C} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{AE200ADC-9E85-4275-A373-E975CD6D518C} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{D72E70F3-BF37-432F-B78B-5B247C873852} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{D72E70F3-BF37-432F-B78B-5B247C873852} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
+		{2BA72059-FFD7-4887-AE88-269017198933} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{9B552A44-9587-4410-8673-254B31E2E4F7} = {2BA72059-FFD7-4887-AE88-269017198933}
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C} = {2BA72059-FFD7-4887-AE88-269017198933}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}