Browse Source

Merge branch 'master' into pixiauth

Krzysztof Krysiński 2 months ago
parent
commit
dfbc825ab1
26 changed files with 381 additions and 61 deletions
  1. 2 2
      README.md
  2. 1 1
      src/ColorPicker
  3. 1 1
      src/Drawie
  4. 9 0
      src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs
  5. 11 1
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  6. 14 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs
  7. 4 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextOnPathNode.cs
  8. 5 2
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateCel_Change.cs
  9. 5 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs
  10. 6 0
      src/PixiEditor.UI.Common/Controls/NumberInput.cs
  11. 8 1
      src/PixiEditor/Data/Localization/Languages/en.json
  12. 83 0
      src/PixiEditor/Helpers/Behaviours/SliderBindingBehavior.cs
  13. 40 10
      src/PixiEditor/Helpers/Extensions/ColorHelpers.cs
  14. 1 1
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  15. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  16. 1 0
      src/PixiEditor/Models/IO/ExportConfig.cs
  17. 54 0
      src/PixiEditor/Models/IO/Exporter.cs
  18. 10 1
      src/PixiEditor/Models/Serialization/Factories/TextSerializationFactory.cs
  19. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  20. 4 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  21. 2 2
      src/PixiEditor/Views/Animations/Timeline.cs
  22. 5 2
      src/PixiEditor/Views/Dialogs/ExportFileDialog.cs
  23. 21 1
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml
  24. 73 24
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  25. 8 2
      src/PixiEditor/Views/Nodes/Properties/DoublePropertyView.axaml
  26. 10 0
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

+ 2 - 2
README.md

@@ -1,7 +1,7 @@
 <img src="https://github.com/user-attachments/assets/bd08c8bd-f610-449d-b1e2-6a990e562518">
 <img src="https://github.com/user-attachments/assets/bd08c8bd-f610-449d-b1e2-6a990e562518">
 
 
 
 
-**PixiEditor** is a universal 2D platform that aims to provide you with tools and features for all your 2D needs. Create beautiful sprites for your games, animations, edit images, create logos. All packed in an eye-friendly dark theme.     
+**PixiEditor** is a universal 2D editor that aims to provide you with tools and features for all your 2D needs. Create beautiful sprites for your games, animations, edit images, create logos. All packed in an eye-friendly dark theme.     
 
 
 
 
 [![Release](https://img.shields.io/github/v/release/flabbet/PixiEditor)](https://github.com/flabbet/PixiEditor/releases) 
 [![Release](https://img.shields.io/github/v/release/flabbet/PixiEditor)](https://github.com/flabbet/PixiEditor/releases) 
@@ -14,7 +14,7 @@
 
 
 ## About PixiEditor
 ## About PixiEditor
 
 
-PixiEditor aims to be all-in-one solution for 2D image editing, we aim to achieve this by building a solid foundation with basic functionalities, and exposing complex extension system, that would customize PixiEditor for all your needs.
+PixiEditor aims to be all-in-one solution for 2D image editing, we want to achieve this by building a solid foundation with built-in tools for editing raster and vector graphics, procedural artworks, animations and more. To fully customize PixiEditor for all of your 2D needs, we built advanced Node Graph rendering, that allows for creating basically anything. From tiled texturing workspace, procedural animations that wouldn't be possible to make by hand, to even rendering 3D shapes.
 
 
 The project started as a pixel-art editor, but quickly evolved into something much more complex. Version 1.0 was downloaded over 100 000 times on all platforms and received 93% positive rating on Steam.
 The project started as a pixel-art editor, but quickly evolved into something much more complex. Version 1.0 was downloaded over 100 000 times on all platforms and received 93% positive rating on Steam.
 
 

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 56faff5afcc6fc96b44b5caa6f15d0b3a902cd0a
+Subproject commit 66bae8cf20153b9273b10c7d37ac90dc57ef15bb

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 3b9c90113ed6b7600d1b80828d3ddf1d70ead90a
+Subproject commit 34ce7ab4d8e14f5dbad86dea076fe4c7cda15da3

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

@@ -7,3 +7,12 @@ public interface IAnimationRenderer
     public Task<bool> RenderAsync(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public Task<bool> RenderAsync(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public bool Render(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
     public bool Render(List<Image> imageStream, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback);
 }
 }
+
+public enum QualityPreset
+{
+    VeryLow = 0,
+    Low = 1,
+    Medium = 2,
+    High = 3,
+    VeryHigh = 4,
+}

+ 11 - 1
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -17,6 +17,7 @@ public class FFMpegRenderer : IAnimationRenderer
     public int FrameRate { get; set; } = 60;
     public int FrameRate { get; set; } = 60;
     public string OutputFormat { get; set; } = "mp4";
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
     public VecI Size { get; set; }
+    public QualityPreset QualityPreset { get; set; } = QualityPreset.VeryHigh;
 
 
     public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
     public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
         Action<double>? progressCallback = null)
         Action<double>? progressCallback = null)
@@ -215,11 +216,20 @@ public class FFMpegRenderer : IAnimationRenderer
 
 
     private FFMpegArgumentProcessor GetMp4Arguments(FFMpegArguments args, string outputPath)
     private FFMpegArgumentProcessor GetMp4Arguments(FFMpegArguments args, string outputPath)
     {
     {
+        int qscale = QualityPreset switch
+        {
+            QualityPreset.VeryLow => 31,
+            QualityPreset.Low => 25,
+            QualityPreset.Medium => 19,
+            QualityPreset.High => 10,
+            QualityPreset.VeryHigh => 1,
+            _ => 2
+        };
         return args
         return args
             .OutputToFile(outputPath, true, options =>
             .OutputToFile(outputPath, true, options =>
             {
             {
                 options.WithFramerate(FrameRate)
                 options.WithFramerate(FrameRate)
-                    .WithVideoBitrate(1800)
+                    .WithCustomArgument($"-qscale:v {qscale}")
                     .WithVideoCodec("mpeg4")
                     .WithVideoCodec("mpeg4")
                     .ForcePixelFormat("yuv420p");
                     .ForcePixelFormat("yuv420p");
             });
             });

+ 14 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs

@@ -153,6 +153,7 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
 
 
     public FontFamilyName? MissingFontFamily { get; set; }
     public FontFamilyName? MissingFontFamily { get; set; }
     public string MissingFontText { get; set; }
     public string MissingFontText { get; set; }
+    public VecD PathOffset { get; set; }
 
 
     private RichText richText;
     private RichText richText;
     private RectD lastBounds;
     private RectD lastBounds;
@@ -226,7 +227,7 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
 
 
     private void PaintText(Canvas canvas, Paint paint)
     private void PaintText(Canvas canvas, Paint paint)
     {
     {
-        richText.Paint(canvas, Position, Font, paint, Path);
+        richText.Paint(canvas, Position, Font, paint, Path, PathOffset);
     }
     }
 
 
     public override bool IsValid()
     public override bool IsValid()
@@ -239,11 +240,14 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
         if (copy is TextVectorData textData)
         if (copy is TextVectorData textData)
         {
         {
             textData.Font = Font.FromFontFamily(Font.Family);
             textData.Font = Font.FromFontFamily(Font.Family);
-            textData.Font.Size = Font.Size;
-            textData.Font.Edging = Font.Edging;
-            textData.Font.SubPixel = Font.SubPixel;
-            textData.Font.Bold = Font.Bold;
-            textData.Font.Italic = Font.Italic;
+            if (textData.Font != null)
+            {
+                textData.Font.Size = Font.Size;
+                textData.Font.Edging = Font.Edging;
+                textData.Font.SubPixel = Font.SubPixel;
+                textData.Font.Bold = Font.Bold;
+                textData.Font.Italic = Font.Italic;
+            }
 
 
             textData.lastBounds = lastBounds;
             textData.lastBounds = lastBounds;
         }
         }
@@ -262,6 +266,7 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
         hash.Add(MaxWidth);
         hash.Add(MaxWidth);
         hash.Add(Bold);
         hash.Add(Bold);
         hash.Add(Italic);
         hash.Add(Italic);
+        hash.Add(PathOffset);
 
 
         return hash.ToHashCode();
         return hash.ToHashCode();
     }
     }
@@ -288,7 +293,8 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
     protected bool Equals(TextVectorData other)
     protected bool Equals(TextVectorData other)
     {
     {
         return base.Equals(other) && Position.Equals(other.Position) && MaxWidth.Equals(other.MaxWidth) && AntiAlias == other.AntiAlias && Nullable.Equals(MissingFontFamily, other.MissingFontFamily) && MissingFontText == other.MissingFontText
         return base.Equals(other) && Position.Equals(other.Position) && MaxWidth.Equals(other.MaxWidth) && AntiAlias == other.AntiAlias && Nullable.Equals(MissingFontFamily, other.MissingFontFamily) && MissingFontText == other.MissingFontText
-            && Text == other.Text && Font.Equals(other.Font) && Spacing.Equals(other.Spacing) && Path == other.Path && Bold == other.Bold && Italic == other.Italic;
+            && Text == other.Text && Font.Equals(other.Font) && Spacing.Equals(other.Spacing) && Path == other.Path && Bold == other.Bold && Italic == other.Italic
+            && PathOffset.Equals(other.PathOffset);
     }
     }
 
 
     public override bool Equals(object? obj)
     public override bool Equals(object? obj)
@@ -313,6 +319,6 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
 
 
     public override int GetHashCode()
     public override int GetHashCode()
     {
     {
-        return HashCode.Combine(base.GetHashCode(), Position, MaxWidth, AntiAlias, MissingFontFamily, MissingFontText, Font, HashCode.Combine(Text, Spacing, Path));
+        return HashCode.Combine(base.GetHashCode(), Position, MaxWidth, AntiAlias, MissingFontFamily, MissingFontText, Font, HashCode.Combine(Text, Spacing, Path, PathOffset));
     }
     }
 }
 }

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextOnPathNode.cs

@@ -1,4 +1,5 @@
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 
 
@@ -9,6 +10,7 @@ public class TextOnPathNode : Node
 {
 {
     public InputProperty<TextVectorData> TextData { get; }
     public InputProperty<TextVectorData> TextData { get; }
     public InputProperty<ShapeVectorData> PathData { get; }
     public InputProperty<ShapeVectorData> PathData { get; }
+    public InputProperty<VecD> Offset { get; }
 
 
     public OutputProperty<TextVectorData> Output { get; }
     public OutputProperty<TextVectorData> Output { get; }
 
 
@@ -18,6 +20,7 @@ public class TextOnPathNode : Node
     {
     {
         TextData = CreateInput<TextVectorData>("Text", "TEXT_LABEL", null);
         TextData = CreateInput<TextVectorData>("Text", "TEXT_LABEL", null);
         PathData = CreateInput<ShapeVectorData>("Path", "SHAPE_LABEL", null);
         PathData = CreateInput<ShapeVectorData>("Path", "SHAPE_LABEL", null);
+        Offset = CreateInput<VecD>("Offset", "OFFSET", VecD.Zero);
 
 
         Output = CreateOutput<TextVectorData>("Output", "TEXT_LABEL", null);
         Output = CreateOutput<TextVectorData>("Output", "TEXT_LABEL", null);
     }
     }
@@ -40,6 +43,7 @@ public class TextOnPathNode : Node
         lastPath.Transform(pathData.TransformationMatrix);
         lastPath.Transform(pathData.TransformationMatrix);
 
 
         cloned.Path = lastPath;
         cloned.Path = lastPath;
+        cloned.PathOffset = Offset.Value;
 
 
         Output.Value = cloned;
         Output.Value = cloned;
     }
     }

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

@@ -34,9 +34,12 @@ internal class CreateCel_Change : Change
             return false;
             return false;
         }
         }
         
         
-        if(_frame == -1 && targetLayer.KeyFrames.All(x => x.KeyFrameGuid != createdKeyFrameId))
+        if(_frame == -1)
         {
         {
-            return false;
+            if (targetLayer.KeyFrames.All(x => x.KeyFrameGuid != createdKeyFrameId) || targetLayer.KeyFrames.Count <= 1)
+            {
+                return false;
+            }
         }
         }
         
         
         return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);
         return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);

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

@@ -26,6 +26,11 @@ internal class DeleteKeyFrame_Change : Change
                 return false;
                 return false;
             }
             }
 
 
+            if(node.KeyFrames.FirstOrDefault()?.KeyFrameGuid == keyFrame.Id) // If the keyframe is the first one, we cannot delete it.
+            {
+                return false;
+            }
+
             clonedKeyFrame = keyFrame.Clone();
             clonedKeyFrame = keyFrame.Clone();
             
             
             KeyFrameData data = node.KeyFrames.FirstOrDefault(x => x.KeyFrameGuid == keyFrame.Id);
             KeyFrameData data = node.KeyFrames.FirstOrDefault(x => x.KeyFrameGuid == keyFrame.Id);

+ 6 - 0
src/PixiEditor.UI.Common/Controls/NumberInput.cs

@@ -314,6 +314,12 @@ public partial class NumberInput : TextBox
 
 
     private static bool TryParse(string s, out double value)
     private static bool TryParse(string s, out double value)
     {
     {
+        if (s == null)
+        {
+            value = 0;
+            return false;
+        }
+
         s = s.Replace(",", ".");
         s = s.Replace(",", ".");
 
 
         if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
         if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value))

+ 8 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1091,5 +1091,12 @@
   "STEP_START": "Step back to closest cel",
   "STEP_START": "Step back to closest cel",
   "STEP_END": "Step forward to closest cel",
   "STEP_END": "Step forward to closest cel",
   "STEP_FORWARD": "Step forward one frame",
   "STEP_FORWARD": "Step forward one frame",
-  "STEP_BACK": "Step back one frame"
+  "STEP_BACK": "Step back one frame",
+  "ANIMATION_QUALITY_PRESET": "Quality Preset",
+  "VERY_LOW_QUALITY_PRESET": "Very Low",
+  "LOW_QUALITY_PRESET": "Low",
+  "MEDIUM_QUALITY_PRESET": "Medium",
+  "HIGH_QUALITY_PRESET": "High",
+  "VERY_HIGH_QUALITY_PRESET": "Very High",
+  "EXPORT_FRAMES": "Export Frames"
 }
 }

+ 83 - 0
src/PixiEditor/Helpers/Behaviours/SliderBindingBehavior.cs

@@ -0,0 +1,83 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Xaml.Interactivity;
+
+namespace PixiEditor.Helpers.Behaviours;
+
+public class SliderBindingBehavior : Behavior<Slider>
+{
+    public static readonly StyledProperty<bool> CanBindProperty = AvaloniaProperty.Register<SliderBindingBehavior, bool>(
+        nameof(CanBind));
+
+    public static readonly StyledProperty<IBinding?> ValueBindingProperty = AvaloniaProperty.Register<SliderBindingBehavior, IBinding?>(
+        nameof(ValueBinding));
+
+    [AssignBinding]
+    public IBinding? ValueBinding
+    {
+        get => GetValue(ValueBindingProperty);
+        set => SetValue(ValueBindingProperty, value);
+    }
+
+    public bool CanBind
+    {
+        get => GetValue(CanBindProperty);
+        set => SetValue(CanBindProperty, value);
+    }
+
+    static SliderBindingBehavior()
+    {
+        CanBindProperty.Changed.Subscribe(OnCanBindChanged);
+        ValueBindingProperty.Changed.Subscribe(OnBindingChanged);
+    }
+
+    protected override void OnAttached()
+    {
+        base.OnAttached();
+        if (AssociatedObject != null)
+        {
+            if (CanBind)
+            {
+                AssociatedObject.Bind(
+                    RangeBase.ValueProperty,
+                    ValueBinding);
+            }
+        }
+    }
+
+    private static void OnCanBindChanged(AvaloniaPropertyChangedEventArgs<bool> e)
+    {
+        if (e.Sender is SliderBindingBehavior behavior && behavior.AssociatedObject != null)
+        {
+            if (e.NewValue.Value && behavior.ValueBinding != null)
+            {
+                behavior.AssociatedObject.Bind(
+                    RangeBase.ValueProperty,
+                    behavior.ValueBinding);
+            }
+            else
+            {
+                behavior.AssociatedObject.ClearValue(RangeBase.ValueProperty);
+            }
+        }
+    }
+
+    private static void OnBindingChanged(AvaloniaPropertyChangedEventArgs<IBinding> e)
+    {
+        if (e.Sender is SliderBindingBehavior behavior && behavior.AssociatedObject != null)
+        {
+            if (behavior.CanBind && e.NewValue.Value != null)
+            {
+                behavior.AssociatedObject.Bind(
+                    RangeBase.ValueProperty,
+                    e.NewValue.Value);
+            }
+            else
+            {
+                behavior.AssociatedObject.ClearValue(RangeBase.ValueProperty);
+            }
+        }
+    }
+}

+ 40 - 10
src/PixiEditor/Helpers/Extensions/ColorHelpers.cs

@@ -1,7 +1,10 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Media;
 using Avalonia.Media;
+using Avalonia.Skia;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using Drawie.Skia;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using BackendColor = Drawie.Backend.Core.ColorsImpl.Color;
 using BackendColor = Drawie.Backend.Core.ColorsImpl.Color;
 using GradientStop = Drawie.Backend.Core.ColorsImpl.Paintables.GradientStop;
 using GradientStop = Drawie.Backend.Core.ColorsImpl.Paintables.GradientStop;
@@ -47,17 +50,31 @@ internal static class ColorHelpers
                 new VecD(linearGradientBrush.StartPoint.Point.X, linearGradientBrush.StartPoint.Point.Y),
                 new VecD(linearGradientBrush.StartPoint.Point.X, linearGradientBrush.StartPoint.Point.Y),
                 new VecD(linearGradientBrush.EndPoint.Point.X, linearGradientBrush.EndPoint.Point.Y),
                 new VecD(linearGradientBrush.EndPoint.Point.X, linearGradientBrush.EndPoint.Point.Y),
                 linearGradientBrush.GradientStops.Select(stop =>
                 linearGradientBrush.GradientStops.Select(stop =>
-                    new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset))),
+                    new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset)))
+            {
+                AbsoluteValues = linearGradientBrush.StartPoint.Unit == RelativeUnit.Absolute ||
+                                 linearGradientBrush.EndPoint.Unit == RelativeUnit.Absolute,
+                Transform = linearGradientBrush.Transform != null ? ToDrawieMatrix(linearGradientBrush.Transform.Value) : null
+            },
         IRadialGradientBrush radialGradientBrush => new RadialGradientPaintable(
         IRadialGradientBrush radialGradientBrush => new RadialGradientPaintable(
             new VecD(radialGradientBrush.Center.Point.X, radialGradientBrush.Center.Point.Y),
             new VecD(radialGradientBrush.Center.Point.X, radialGradientBrush.Center.Point.Y),
             radialGradientBrush.RadiusX.Scalar,
             radialGradientBrush.RadiusX.Scalar,
             radialGradientBrush.GradientStops.Select(stop =>
             radialGradientBrush.GradientStops.Select(stop =>
-                new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset))),
+                new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset)))
+        {
+            AbsoluteValues = radialGradientBrush.Center.Unit == RelativeUnit.Absolute ||
+                             radialGradientBrush.RadiusX.Unit == RelativeUnit.Absolute,
+            Transform = radialGradientBrush.Transform != null ? ToDrawieMatrix(radialGradientBrush.Transform.Value) : null
+        },
         IConicGradientBrush conicGradientBrush => new SweepGradientPaintable(
         IConicGradientBrush conicGradientBrush => new SweepGradientPaintable(
             new VecD(conicGradientBrush.Center.Point.X, conicGradientBrush.Center.Point.Y),
             new VecD(conicGradientBrush.Center.Point.X, conicGradientBrush.Center.Point.Y),
             conicGradientBrush.Angle,
             conicGradientBrush.Angle,
             conicGradientBrush.GradientStops.Select(stop =>
             conicGradientBrush.GradientStops.Select(stop =>
-                new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset))),
+                new GradientStop(new BackendColor(stop.Color.R, stop.Color.G, stop.Color.B, stop.Color.A), stop.Offset)))
+        {
+            AbsoluteValues = conicGradientBrush.Center.Unit == RelativeUnit.Absolute,
+            Transform = conicGradientBrush.Transform != null ? ToDrawieMatrix(conicGradientBrush.Transform.Value) : null
+        },
         null => null,
         null => null,
 
 
     };
     };
@@ -67,22 +84,25 @@ internal static class ColorHelpers
         ColorPaintable color => new SolidColorBrush(color.Color.ToColor()),
         ColorPaintable color => new SolidColorBrush(color.Color.ToColor()),
         LinearGradientPaintable linearGradientPaintable => new LinearGradientBrush
         LinearGradientPaintable linearGradientPaintable => new LinearGradientBrush
         {
         {
-            StartPoint = new RelativePoint(linearGradientPaintable.Start.X, linearGradientPaintable.Start.Y, RelativeUnit.Absolute),
-            EndPoint = new RelativePoint(linearGradientPaintable.End.X, linearGradientPaintable.End.Y, RelativeUnit.Absolute),
-            GradientStops = ToAvaloniaGradientStops(linearGradientPaintable.GradientStops)
+            StartPoint = new RelativePoint(linearGradientPaintable.Start.X, linearGradientPaintable.Start.Y, paintable.AbsoluteValues ? RelativeUnit.Absolute : RelativeUnit.Relative),
+            EndPoint = new RelativePoint(linearGradientPaintable.End.X, linearGradientPaintable.End.Y, paintable.AbsoluteValues ? RelativeUnit.Absolute : RelativeUnit.Relative),
+            GradientStops = ToAvaloniaGradientStops(linearGradientPaintable.GradientStops),
+            Transform = linearGradientPaintable.Transform.HasValue ? new MatrixTransform(ToAvaloniaMatrix(linearGradientPaintable.Transform.Value)) : null
         },
         },
         RadialGradientPaintable radialGradientPaintable => new RadialGradientBrush
         RadialGradientPaintable radialGradientPaintable => new RadialGradientBrush
         {
         {
-            Center = new RelativePoint(radialGradientPaintable.Center.X, radialGradientPaintable.Center.Y, RelativeUnit.Absolute),
+            Center = new RelativePoint(radialGradientPaintable.Center.X, radialGradientPaintable.Center.Y, paintable.AbsoluteValues ? RelativeUnit.Absolute : RelativeUnit.Relative),
             RadiusX = new RelativeScalar(radialGradientPaintable.Radius, RelativeUnit.Absolute),
             RadiusX = new RelativeScalar(radialGradientPaintable.Radius, RelativeUnit.Absolute),
             RadiusY = new RelativeScalar(radialGradientPaintable.Radius, RelativeUnit.Absolute),
             RadiusY = new RelativeScalar(radialGradientPaintable.Radius, RelativeUnit.Absolute),
-            GradientStops = ToAvaloniaGradientStops(radialGradientPaintable.GradientStops)
+            GradientStops = ToAvaloniaGradientStops(radialGradientPaintable.GradientStops),
+            Transform = radialGradientPaintable.Transform.HasValue ? new MatrixTransform(ToAvaloniaMatrix(radialGradientPaintable.Transform.Value)) : null
         },
         },
         SweepGradientPaintable conicGradientPaintable => new ConicGradientBrush
         SweepGradientPaintable conicGradientPaintable => new ConicGradientBrush
         {
         {
             Angle = conicGradientPaintable.Angle,
             Angle = conicGradientPaintable.Angle,
-            Center = new RelativePoint(conicGradientPaintable.Center.X, conicGradientPaintable.Center.Y, RelativeUnit.Absolute),
-            GradientStops = ToAvaloniaGradientStops(conicGradientPaintable.GradientStops)
+            Center = new RelativePoint(conicGradientPaintable.Center.X, conicGradientPaintable.Center.Y, paintable.AbsoluteValues ? RelativeUnit.Absolute : RelativeUnit.Relative),
+            GradientStops = ToAvaloniaGradientStops(conicGradientPaintable.GradientStops),
+            Transform = conicGradientPaintable.Transform.HasValue ? new MatrixTransform(ToAvaloniaMatrix(conicGradientPaintable.Transform.Value)) : null
         },
         },
         null => null,
         null => null,
         _ => throw new NotImplementedException()
         _ => throw new NotImplementedException()
@@ -98,4 +118,14 @@ internal static class ColorHelpers
 
 
         return stops;
         return stops;
     }
     }
+
+    private static Matrix ToAvaloniaMatrix(Matrix3X3 matrix)
+    {
+        return new Matrix(matrix.ScaleX, matrix.SkewY, matrix.SkewX, matrix.ScaleY, matrix.TransX, matrix.TransY);
+    }
+
+    private static Matrix3X3 ToDrawieMatrix(Matrix matrix)
+    {
+        return matrix.ToSKMatrix().ToMatrix3X3();
+    }
 }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -454,7 +454,7 @@ internal class DocumentUpdater
 
 
             if (closestMember == null)
             if (closestMember == null)
             {
             {
-                closestMember = doc.NodeGraphHandler.StructureTree.Members.FirstOrDefault();
+                closestMember = doc.NodeGraphHandler.StructureTree.Members.FirstOrDefault(x => x.Id != info.Id);
             }
             }
 
 
             if (closestMember != null)
             if (closestMember != null)

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -180,6 +180,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             VectorShapeChangeType changeType = name switch
             VectorShapeChangeType changeType = name switch
             {
             {
                 nameof(IFillableShapeToolbar.Fill) => VectorShapeChangeType.Fill,
                 nameof(IFillableShapeToolbar.Fill) => VectorShapeChangeType.Fill,
+                nameof(IFillableShapeToolbar.FillBrush) => VectorShapeChangeType.Fill,
                 nameof(IShapeToolbar.StrokeBrush) => VectorShapeChangeType.Stroke,
                 nameof(IShapeToolbar.StrokeBrush) => VectorShapeChangeType.Stroke,
                 nameof(IShapeToolbar.ToolSize) => VectorShapeChangeType.Stroke,
                 nameof(IShapeToolbar.ToolSize) => VectorShapeChangeType.Stroke,
                 nameof(IShapeToolbar.AntiAliasing) => VectorShapeChangeType.OtherVisuals,
                 nameof(IShapeToolbar.AntiAliasing) => VectorShapeChangeType.OtherVisuals,

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

@@ -14,6 +14,7 @@ public class ExportConfig
    
    
    public VectorExportConfig? VectorExportConfig { get; set; }
    public VectorExportConfig? VectorExportConfig { get; set; }
    public string ExportOutput { get; set; }
    public string ExportOutput { get; set; }
+   public bool ExportFramesToFolder { get; set; }
 
 
    public ExportConfig(VecI exportSize)
    public ExportConfig(VecI exportSize)
    {
    {

+ 54 - 0
src/PixiEditor/Models/IO/Exporter.cs

@@ -5,10 +5,12 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 
 
 namespace PixiEditor.Models.IO;
 namespace PixiEditor.Models.IO;
@@ -114,6 +116,23 @@ internal class Exporter
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
         if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
             return new SaveResult(SaveResultType.InvalidPath);
             return new SaveResult(SaveResultType.InvalidPath);
 
 
+        if (exportConfig.ExportFramesToFolder)
+        {
+            try
+            {
+                await ExportFramesToFolderAsync(document, directory, exportConfig, job);
+                job?.Finish();
+                return new SaveResult(SaveResultType.Success);
+            }
+            catch (Exception e)
+            {
+                job?.Finish();
+                Console.WriteLine(e);
+                CrashHelper.SendExceptionInfo(e);
+                return new SaveResult(SaveResultType.UnknownError);
+            }
+        }
+
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
         var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
 
 
         if (typeFromPath is null)
         if (typeFromPath is null)
@@ -161,6 +180,41 @@ internal class Exporter
         }
         }
     }
     }
 
 
+    private static async Task ExportFramesToFolderAsync(DocumentViewModel document, string directory,
+        ExportConfig exportConfig, ExportJob? job)
+    {
+        if (!Directory.Exists(directory))
+        {
+            Directory.CreateDirectory(directory);
+        }
+
+        int totalFrames = document.AnimationDataViewModel.GetVisibleFramesCount();
+        document.RenderFramesProgressive(
+            (surface, frame) =>
+        {
+            job?.CancellationTokenSource.Token.ThrowIfCancellationRequested();
+            job?.Report(((double)frame / totalFrames),
+                new LocalizedString("RENDERING_FRAME", frame, totalFrames));
+            if (exportConfig.ExportSize != surface.Size)
+            {
+                var resized = surface.ResizeNearestNeighbor(exportConfig.ExportSize);
+                SaveAsPng(Path.Combine(directory, $"{frame}.png"), resized);
+            }
+            else
+            {
+                SaveAsPng(Path.Combine(directory, $"{frame}.png"), surface);
+            }
+
+        }, CancellationToken.None, exportConfig.ExportOutput);
+    }
+
+    public static void SaveAsPng(string path, Surface surface)
+    {
+        using var snapshot = surface.DrawingSurface.Snapshot();
+        using var fileStream = new FileStream(path, FileMode.Create);
+        snapshot.Encode(EncodedImageFormat.Png).SaveTo(fileStream);
+    }
+
     public static void SaveAsGZippedBytes(string path, Surface surface)
     public static void SaveAsGZippedBytes(string path, Surface surface)
     {
     {
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));

+ 10 - 1
src/PixiEditor/Models/Serialization/Factories/TextSerializationFactory.cs

@@ -39,6 +39,8 @@ internal class TextSerializationFactory : VectorShapeSerializationFactory<TextVe
         {
         {
             builder.AddString(original.Path.ToSvgPathData());
             builder.AddString(original.Path.ToSvgPathData());
         }
         }
+
+        builder.AddVecD(original.PathOffset);
     }
     }
 
 
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Paintable strokePaintable,
     protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Paintable strokePaintable,
@@ -71,6 +73,12 @@ internal class TextSerializationFactory : VectorShapeSerializationFactory<TextVe
             path = VectorPath.FromSvgPath(DeserializeStringCompatible(extractor, serializerData));
             path = VectorPath.FromSvgPath(DeserializeStringCompatible(extractor, serializerData));
         }
         }
 
 
+        VecD pathOffset = VecD.Zero;
+        if (!IsFilePreVersion(serializerData, new Version(2, 0, 0, 95)))
+        {
+            pathOffset = extractor.GetVecD();
+        }
+
         FontFamilyName family =
         FontFamilyName family =
             new FontFamilyName(fontFamily) { FontUri = isFontFromFile ? new Uri(fontPath, UriKind.Absolute) : null };
             new FontFamilyName(fontFamily) { FontUri = isFontFromFile ? new Uri(fontPath, UriKind.Absolute) : null };
         Font font = Font.FromFontFamily(family);
         Font font = Font.FromFontFamily(family);
@@ -107,7 +115,8 @@ internal class TextSerializationFactory : VectorShapeSerializationFactory<TextVe
             Path = path,
             Path = path,
             MissingFontFamily = missingFamily,
             MissingFontFamily = missingFamily,
             MissingFontText = new LocalizedString("MISSING_FONT"),
             MissingFontText = new LocalizedString("MISSING_FONT"),
-            AntiAlias = antiAlias
+            AntiAlias = antiAlias,
+            PathOffset = pathOffset
         };
         };
 
 
         return true;
         return true;

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.92")]
-[assembly: AssemblyFileVersion("2.0.0.92")]
+[assembly: AssemblyVersion("2.0.0.95")]
+[assembly: AssemblyFileVersion("2.0.0.95")]

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

@@ -401,7 +401,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                         SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories,
                         SerializationUtil.DeserializeDict(serializedNode.AdditionalData, config, allFactories,
                             serializerData)));
                             serializerData)));
                 }
                 }
+            }
 
 
+            foreach (var node in graph.AllNodes)
+            {
+                Guid nodeGuid = mappedNodeIds[node.Id];
                 if (node.InputConnections != null)
                 if (node.InputConnections != null)
                 {
                 {
                     foreach (var connections in node.InputConnections)
                     foreach (var connections in node.InputConnections)

+ 2 - 2
src/PixiEditor/Views/Animations/Timeline.cs

@@ -300,9 +300,9 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
     {
     {
         if (dragged)
         if (dragged)
         {
         {
-            if (draggedKeyFrames.Length > 0)
+            if (draggedKeyFrames is { Length: > 0 })
             {
             {
-                ChangeKeyFramesLengthCommand.Execute((draggedKeyFrames.ToArray(), 0, true));
+                ChangeKeyFramesLengthCommand?.Execute((draggedKeyFrames.ToArray(), 0, true));
             }
             }
         }
         }
 
 

+ 5 - 2
src/PixiEditor/Views/Dialogs/ExportFileDialog.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 
 
 namespace PixiEditor.Views.Dialogs;
 namespace PixiEditor.Views.Dialogs;
@@ -143,14 +144,16 @@ internal class ExportFileDialog : CustomDialog
             FilePath = popup.SavePath;
             FilePath = popup.SavePath;
             ChosenFormat = popup.SaveFormat;
             ChosenFormat = popup.SaveFormat;
             ExportOutput = popup.ExportOutput;
             ExportOutput = popup.ExportOutput;
-            
+
             ExportConfig.ExportSize = new VecI(FileWidth, FileHeight);
             ExportConfig.ExportSize = new VecI(FileWidth, FileHeight);
             ExportConfig.ExportOutput = ExportOutput.Name;
             ExportConfig.ExportOutput = ExportOutput.Name;
+            ExportConfig.ExportFramesToFolder = popup.FolderExport;
             ExportConfig.AnimationRenderer = ChosenFormat is VideoFileType ? new FFMpegRenderer()
             ExportConfig.AnimationRenderer = ChosenFormat is VideoFileType ? new FFMpegRenderer()
             {
             {
                 Size = new VecI(FileWidth, FileHeight),
                 Size = new VecI(FileWidth, FileHeight),
                 OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
                 OutputFormat = ChosenFormat.PrimaryExtension.Replace(".", ""),
-                FrameRate = document.AnimationDataViewModel.FrameRateBindable
+                FrameRate = document.AnimationDataViewModel.FrameRateBindable,
+                QualityPreset = (QualityPreset)popup.AnimationPresetIndex
             }
             }
             : null;
             : null;
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;
             ExportConfig.ExportAsSpriteSheet = popup.IsSpriteSheetExport;

+ 21 - 1
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml

@@ -7,6 +7,7 @@
                          xmlns:indicators="clr-namespace:PixiEditor.Views.Indicators"
                          xmlns:indicators="clr-namespace:PixiEditor.Views.Indicators"
                          xmlns:input1="clr-namespace:PixiEditor.Views.Input"
                          xmlns:input1="clr-namespace:PixiEditor.Views.Input"
                          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:converters="clr-namespace:PixiEditor.Helpers.Converters"
                          CanResize="False"
                          CanResize="False"
                          CanMinimize="False"
                          CanMinimize="False"
                          SizeToContent="WidthAndHeight"
                          SizeToContent="WidthAndHeight"
@@ -27,7 +28,26 @@
                 </TabControl.Styles>
                 </TabControl.Styles>
                 <TabControl.Items>
                 <TabControl.Items>
                     <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER" />
                     <TabItem IsSelected="True" ui1:Translator.Key="EXPORT_IMAGE_HEADER" />
-                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER" />
+                    <TabItem ui1:Translator.Key="EXPORT_ANIMATION_HEADER">
+                        <StackPanel Orientation="Vertical" Spacing="5">
+                            <StackPanel Spacing="5" Orientation="Horizontal">
+                                <TextBlock ui1:Translator.Key="ANIMATION_QUALITY_PRESET" />
+                                <ComboBox
+                                    SelectedIndex="{Binding ElementName=saveFilePopup, Path=AnimationPresetIndex}"
+                                    ItemsSource="{Binding ElementName=saveFilePopup, Path=QualityPresetValues}">
+                                    <ComboBox.ItemTemplate>
+                                        <DataTemplate>
+                                            <TextBlock
+                                                ui1:Translator.Key="{Binding Converter={converters:EnumToLocalizedStringConverter}}" />
+                                        </DataTemplate>
+                                    </ComboBox.ItemTemplate>
+                                </ComboBox>
+
+                                <CheckBox IsChecked="{Binding ElementName=saveFilePopup, Path=FolderExport, Mode=TwoWay}"
+                                          ui1:Translator.Key="EXPORT_FRAMES" />
+                            </StackPanel>
+                        </StackPanel>
+                    </TabItem>
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                     <TabItem ui1:Translator.Key="EXPORT_SPRITESHEET_HEADER">
                         <Grid>
                         <Grid>
                             <Grid.ColumnDefinitions>
                             <Grid.ColumnDefinitions>

+ 73 - 24
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -13,6 +13,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.AnimationRenderer.Core;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using Image = Drawie.Backend.Core.Surfaces.ImageData.Image;
 using Image = Drawie.Backend.Core.Surfaces.ImageData.Image;
@@ -76,6 +77,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
     public static readonly StyledProperty<string> SizeHintProperty = AvaloniaProperty.Register<ExportFilePopup, string>(
     public static readonly StyledProperty<string> SizeHintProperty = AvaloniaProperty.Register<ExportFilePopup, string>(
         nameof(SizeHint));
         nameof(SizeHint));
 
 
+    public static readonly StyledProperty<bool> FolderExportProperty = AvaloniaProperty.Register<ExportFilePopup, bool>(
+        nameof(FolderExport));
+
+    public bool FolderExport
+    {
+        get => GetValue(FolderExportProperty);
+        set => SetValue(FolderExportProperty, value);
+    }
     public string SizeHint
     public string SizeHint
     {
     {
         get => GetValue(SizeHintProperty);
         get => GetValue(SizeHintProperty);
@@ -171,6 +180,14 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
 
     public bool IsSpriteSheetExport => SelectedExportIndex == 2;
     public bool IsSpriteSheetExport => SelectedExportIndex == 2;
 
 
+    public int AnimationPresetIndex
+    {
+        get { return (int)GetValue(AnimationPresetIndexProperty); }
+        set { SetValue(AnimationPresetIndexProperty, value); }
+    }
+
+    public Array QualityPresetValues { get; }
+
     private DocumentViewModel document;
     private DocumentViewModel document;
     private Image[]? videoPreviewFrames = [];
     private Image[]? videoPreviewFrames = [];
     private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
     private DispatcherTimer videoPreviewTimer = new DispatcherTimer();
@@ -179,6 +196,9 @@ internal partial class ExportFilePopup : PixiEditorPopup
 
 
     private Task? generateSpriteSheetTask;
     private Task? generateSpriteSheetTask;
 
 
+    public static readonly StyledProperty<int> AnimationPresetIndexProperty
+        = AvaloniaProperty.Register<ExportFilePopup, int>("AnimationPresetIndex", 4);
+
     static ExportFilePopup()
     static ExportFilePopup()
     {
     {
         SaveWidthProperty.Changed.Subscribe(RerenderPreview);
         SaveWidthProperty.Changed.Subscribe(RerenderPreview);
@@ -193,8 +213,10 @@ internal partial class ExportFilePopup : PixiEditorPopup
     {
     {
         SaveWidth = imageWidth;
         SaveWidth = imageWidth;
         SaveHeight = imageHeight;
         SaveHeight = imageHeight;
+        QualityPresetValues = Enum.GetValues(typeof(QualityPreset));
 
 
         InitializeComponent();
         InitializeComponent();
+
         DataContext = this;
         DataContext = this;
         Loaded += (_, _) => sizePicker.FocusWidthPicker();
         Loaded += (_, _) => sizePicker.FocusWidthPicker();
 
 
@@ -467,37 +489,64 @@ internal partial class ExportFilePopup : PixiEditorPopup
     /// </summary>
     /// </summary>
     private async Task<string?> ChoosePath()
     private async Task<string?> ChoosePath()
     {
     {
-        FilePickerSaveOptions options = new FilePickerSaveOptions
-        {
-            Title = new LocalizedString("EXPORT_SAVE_TITLE"),
-            SuggestedFileName = SuggestedName,
-            SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
-                ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
-                : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
-            FileTypeChoices =
-                SupportedFilesHelper.BuildSaveFilter(SelectedExportIndex == 1
-                    ? FileTypeDialogDataSet.SetKind.Video
-                    : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector),
-            ShowOverwritePrompt = true
-        };
+        bool folderExport = FolderExport && SelectedExportIndex == 1;
 
 
-        IStorageFile file = await GetTopLevel(this).StorageProvider.SaveFilePickerAsync(options);
-        if (file != null)
+        if (folderExport)
         {
         {
-            if (string.IsNullOrEmpty(file.Name) == false)
+            FolderPickerOpenOptions options = new FolderPickerOpenOptions()
             {
             {
-                SaveFormat = SupportedFilesHelper.GetSaveFileType(
-                    SelectedExportIndex == 1
-                        ? FileTypeDialogDataSet.SetKind.Video
-                        : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector, file);
-                if (SaveFormat == null)
+                Title = new LocalizedString("EXPORT_SAVE_TITLE"),
+                SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
+                    ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
+                    : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
+                AllowMultiple = false,
+            };
+
+            var folders = await GetTopLevel(this).StorageProvider.OpenFolderPickerAsync(options);
+            if (folders.Count > 0)
+            {
+                IStorageFolder folder = folders[0];
+                if (folder != null)
                 {
                 {
-                    return null;
+                    SavePath = folder.Path.LocalPath;
+                    return SavePath;
                 }
                 }
+            }
+        }
+        else
+        {
+            FilePickerSaveOptions options = new FilePickerSaveOptions
+            {
+                Title = new LocalizedString("EXPORT_SAVE_TITLE"),
+                SuggestedFileName = SuggestedName,
+                SuggestedStartLocation = string.IsNullOrEmpty(document.FullFilePath)
+                    ? await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents)
+                    : await GetTopLevel(this).StorageProvider.TryGetFolderFromPathAsync(document.FullFilePath),
+                FileTypeChoices =
+                    SupportedFilesHelper.BuildSaveFilter(SelectedExportIndex == 1
+                        ? FileTypeDialogDataSet.SetKind.Video
+                        : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector),
+                ShowOverwritePrompt = true
+            };
 
 
-                string fileName = SupportedFilesHelper.FixFileExtension(file.Path.LocalPath, SaveFormat);
+            IStorageFile file = await GetTopLevel(this).StorageProvider.SaveFilePickerAsync(options);
+            if (file != null)
+            {
+                if (string.IsNullOrEmpty(file.Name) == false)
+                {
+                    SaveFormat = SupportedFilesHelper.GetSaveFileType(
+                        SelectedExportIndex == 1
+                            ? FileTypeDialogDataSet.SetKind.Video
+                            : FileTypeDialogDataSet.SetKind.Image | FileTypeDialogDataSet.SetKind.Vector, file);
+                    if (SaveFormat == null)
+                    {
+                        return null;
+                    }
+
+                    string fileName = SupportedFilesHelper.FixFileExtension(file.Path.LocalPath, SaveFormat);
 
 
-                return fileName;
+                    return fileName;
+                }
             }
             }
         }
         }
 
 

+ 8 - 2
src/PixiEditor/Views/Nodes/Properties/DoublePropertyView.axaml

@@ -43,10 +43,16 @@
                                    Min="{Binding Min}" Max="{Binding Max}"
                                    Min="{Binding Min}" Max="{Binding Max}"
                                    Value="{Binding DoubleValue, Mode=TwoWay}" />
                                    Value="{Binding DoubleValue, Mode=TwoWay}" />
 
 
-                <Slider Value="{Binding DoubleValue, Mode=TwoWay}"
-                        Margin="5, 0"
+                <Slider
+                        Margin="5, 0" Name="slider"
                         Classes.colorSlider="{Binding SliderSettings.IsColorSlider}"
                         Classes.colorSlider="{Binding SliderSettings.IsColorSlider}"
                         Minimum="{Binding Min}" Maximum="{Binding Max}">
                         Minimum="{Binding Min}" Maximum="{Binding Max}">
+                    <Interaction.Behaviors>
+                        <behaviours:SliderBindingBehavior
+                            CanBind="{Binding NumberPickerMode,
+                                            Converter={converters:EnumBooleanConverter}, ConverterParameter=Slider}"
+                            ValueBinding="{Binding DoubleValue, Mode=TwoWay}"/>
+                    </Interaction.Behaviors>
                     <Slider.Styles>
                     <Slider.Styles>
                         <Style Selector="Slider.colorSlider Border#TrackBackground">
                         <Style Selector="Slider.colorSlider Border#TrackBackground">
                             <Setter Property="Background" Value="{Binding SliderSettings.BackgroundBrush}" />
                             <Setter Property="Background" Value="{Binding SliderSettings.BackgroundBrush}" />

+ 10 - 0
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -499,6 +499,7 @@ public class VectorPathOverlay : Overlay
 
 
     private void SelectAnchor(AnchorHandle handle, bool append = false)
     private void SelectAnchor(AnchorHandle handle, bool append = false)
     {
     {
+        lastSelectedIndices.Clear();
         if (append)
         if (append)
         {
         {
             handle.IsSelected = !handle.IsSelected;
             handle.IsSelected = !handle.IsSelected;
@@ -888,6 +889,15 @@ public class VectorPathOverlay : Overlay
         }
         }
 
 
         anchorHandles.Clear();
         anchorHandles.Clear();
+        foreach (var handle in controlPointHandles)
+        {
+            handle.OnPress -= OnControlPointPress;
+            handle.OnDrag -= OnControlPointDrag;
+            handle.OnRelease -= OnHandleRelease;
+            Handles.Remove(handle);
+        }
+
+        controlPointHandles.Clear();
     }
     }
 
 
     private VecD TryFindAnySnap(VecD delta, VectorPath path, out string? axisX, out string? axisY, out VecD? snapPoint)
     private VecD TryFindAnySnap(VecD delta, VectorPath path, out string? axisX, out string? axisY, out VecD? snapPoint)