Sfoglia il codice sorgente

Merge pull request #704 from PixiEditor/fixes/20.12.2024

Fixes/20-23.12.2024
Krzysztof Krysiński 7 mesi fa
parent
commit
ea77265787
62 ha cambiato i file con 589 aggiunte e 326 eliminazioni
  1. 12 17
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  2. 1 0
      src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj
  3. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs
  4. 0 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  5. 2 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  6. 13 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  7. 5 8
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  8. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  9. 0 11
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  10. 9 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  11. 18 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  12. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/DeleteKeyFrame_Change.cs
  13. 30 36
      src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs
  14. 10 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  15. 19 0
      src/PixiEditor.sln
  16. 2 2
      src/PixiEditor/Helpers/SurfaceHelpers.cs
  17. 1 1
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  18. 11 4
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  19. 41 17
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  20. 59 11
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  21. 2 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs
  22. 6 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs
  23. 3 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  24. 8 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  25. 15 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs
  26. 15 12
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs
  27. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  28. 10 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  29. 1 5
      src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs
  30. 3 3
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  31. 2 1
      src/PixiEditor/Models/Handlers/ITransformHandler.cs
  32. 1 0
      src/PixiEditor/Models/IO/Exporter.cs
  33. 1 1
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  34. 1 1
      src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  35. 11 6
      src/PixiEditor/Models/Serialization/Factories/SurfaceSerializationFactory.cs
  36. 2 2
      src/PixiEditor/Models/Serialization/SerializationConfig.cs
  37. 5 0
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  38. 1 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  39. 6 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  40. 44 26
      src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs
  41. 10 46
      src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs
  42. 1 1
      src/PixiEditor/ViewModels/Document/TransformOverlays/UndoStack.cs
  43. 15 3
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  44. 15 6
      src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs
  45. 3 2
      src/PixiEditor/ViewModels/Tools/ShapeTool.cs
  46. 24 2
      src/PixiEditor/ViewModels/Tools/ToolViewModel.cs
  47. 28 8
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  48. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs
  49. 15 3
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  50. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  51. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  52. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  53. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RotateViewportToolViewModel.cs
  54. 16 11
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  55. 16 11
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  56. 5 5
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  57. 16 13
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  58. 1 1
      src/PixiEditor/Views/Animations/Timeline.cs
  59. 12 0
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  60. 12 0
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  61. 16 3
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  62. 1 1
      src/PixiParser

+ 12 - 17
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -7,6 +7,7 @@ using FFMpegCore.Pipes;
 using PixiEditor.AnimationRenderer.Core;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
+using PixiEditor.OperatingSystem;
 
 namespace PixiEditor.AnimationRenderer.FFmpeg;
 
@@ -16,16 +17,10 @@ public class FFMpegRenderer : IAnimationRenderer
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
 
-    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken, Action<double>? progressCallback = null)
+    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath, CancellationToken cancellationToken,
+        Action<double>? progressCallback = null)
     {
-        string path = "ThirdParty/{0}/ffmpeg";
-#if WINDOWS
-        path = string.Format(path, "Windows");
-#elif MACOS
-        path = string.Format(path, "MacOS");
-#elif LINUX
-        path = string.Format(path, "Linux");
-#endif
+        string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
         GlobalFFOptions.Configure(new FFOptions()
         {
@@ -35,26 +30,26 @@ public class FFMpegRenderer : IAnimationRenderer
         try
         {
             List<ImgFrame> frames = new();
-            
+
             foreach (var frame in rawFrames)
             {
                 frames.Add(new ImgFrame(frame));
             }
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
-            
+
             string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
-            
+
             if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
             {
                 Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
             }
-            
+
             if (RequiresPaletteGeneration())
             {
                 GeneratePalette(streamPipeSource, paletteTempPath);
             }
-            
+
             streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
             var args = FFMpegArguments
@@ -67,15 +62,15 @@ public class FFMpegRenderer : IAnimationRenderer
             TimeSpan totalTimeSpan = TimeSpan.FromSeconds(frames.Count / (float)FrameRate);
             var result = await outputArgs.CancellableThrough(cancellationToken)
                 .NotifyOnProgress(progressCallback, totalTimeSpan).ProcessAsynchronously();
-            
+
             if (RequiresPaletteGeneration())
             {
                 File.Delete(paletteTempPath);
                 Directory.Delete(Path.GetDirectoryName(paletteTempPath));
             }
-            
+
             DisposeStream(frames);
-            
+
             return result;
         }
         catch (Exception e)

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

@@ -10,6 +10,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj" />
+      <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
     </ItemGroup>
 
     <ItemGroup>

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeGraph.cs

@@ -81,6 +81,8 @@ public class NodeGraph : IReadOnlyNodeGraph, IDisposable
             
             if (node is Node typedNode)
             {
+                if(typedNode.IsDisposed) continue;
+                
                 typedNode.ExecuteInternal(context);
             }
             else

+ 0 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs

@@ -56,11 +56,6 @@ public class MergeNode : RenderNode
             int saved = target.Canvas.SaveLayer();
             Bottom.Value.Paint(context, target);
 
-            if (paint == null)
-            {
-                paint = new Paint();
-            }
-            
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             

+ 2 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -88,15 +88,9 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         return CalculateHash();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new EllipseVectorData(Center, Radius)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
+       
     }
 
     public override VectorPath ToPath()

+ 13 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs

@@ -9,10 +9,10 @@ using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnlyLineData
+public class LineVectorData : ShapeVectorData, IReadOnlyLineData
 {
-    public VecD Start { get; set; } = startPos; // Relative to the document top left
-    public VecD End { get; set; } = pos; // Relative to the document top left
+    public VecD Start { get; set; }
+    public VecD End { get; set; }
 
     public VecD TransformedStart
     {
@@ -53,6 +53,15 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
     public override ShapeCorners TransformationCorners => new ShapeCorners(GeometryAABB)
         .WithMatrix(TransformationMatrix);
 
+
+    public LineVectorData(VecD startPos, VecD pos)
+    {
+        Start = startPos;
+        End = pos;
+        
+        Fill = false;
+    }
+
     public override void RasterizeGeometry(Canvas canvas)
     {
         Rasterize(canvas, false);
@@ -101,18 +110,10 @@ public class LineVectorData(VecD startPos, VecD pos) : ShapeVectorData, IReadOnl
         return GetCacheHash();
     }
 
-    public override object Clone()
-    {
-        return new LineVectorData(Start, End)
-        {
-            StrokeColor = StrokeColor, StrokeWidth = StrokeWidth, TransformationMatrix = TransformationMatrix
-        };
-    }
-
     public override VectorPath ToPath()
     {
         // TODO: Apply transformation matrix
-        
+
         VectorPath path = new VectorPath();
         path.MoveTo((VecF)Start);
         path.LineTo((VecF)End);

+ 5 - 8
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -10,7 +10,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
-    public VectorPath Path { get; }
+    public VectorPath Path { get; set; }
     public override RectD GeometryAABB => Path.TightBounds;
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
@@ -88,15 +88,12 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
         return Path.GetHashCode();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new PathVectorData(new VectorPath(Path))
+        if (copy is PathVectorData pathData)
         {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
+            pathData.Path = new VectorPath(Path);
+        }
     }
 
     public override VectorPath ToPath()

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -71,12 +71,12 @@ public class PointsVectorData : ShapeVectorData
         return Points.GetHashCode();
     }
 
-    public override object Clone()
+    protected override void AdjustCopy(ShapeVectorData copy)
     {
-        return new PointsVectorData(Points)
+        if (copy is PointsVectorData pointsVectorData)
         {
-            StrokeColor = StrokeColor, FillColor = FillColor, StrokeWidth = StrokeWidth
-        };
+            pointsVectorData.Points = new List<VecD>(Points);
+        }
     }
 
     public override VectorPath ToPath()

+ 0 - 11
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -100,17 +100,6 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         return CalculateHash();
     }
 
-    public override object Clone()
-    {
-        return new RectangleVectorData(Center, Size)
-        {
-            StrokeColor = StrokeColor,
-            FillColor = FillColor,
-            StrokeWidth = StrokeWidth,
-            TransformationMatrix = TransformationMatrix
-        };
-    }
-
     public override VectorPath ToPath()
     {
         VectorPath path = new VectorPath();

+ 9 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -37,7 +37,15 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     public abstract bool IsValid();
     public abstract int GetCacheHash();
     public abstract int CalculateHash();
-    public abstract object Clone();
+
+    public object Clone()
+    {
+        ShapeVectorData copy = (ShapeVectorData)MemberwiseClone();
+        AdjustCopy(copy);
+        return copy;
+    }
+
+    protected virtual void AdjustCopy(ShapeVectorData copy) { }
 
     public override int GetHashCode()
     {

+ 18 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

@@ -27,6 +27,18 @@ internal class CreateRasterKeyFrame_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
+        var targetLayer = target.FindMember(_targetLayerGuid);
+        
+        if (targetLayer is null)
+        {
+            return false;
+        }
+        
+        if(_frame == -1 && targetLayer.KeyFrames.All(x => x.KeyFrameGuid != createdKeyFrameId))
+        {
+            return false;
+        }
+        
         return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);
     }
 
@@ -51,6 +63,11 @@ internal class CreateRasterKeyFrame_Change : Change
 
         if (existingData is null)
         {
+            if (_frame == -1)
+            {
+                ignoreInUndo = true;
+                return new None();
+            }
             targetNode.AddFrame(createdKeyFrameId,
                 new KeyFrameData(createdKeyFrameId, _frame, 1, ImageLayerNode.ImageLayerKey) { Data = img, });
         }
@@ -81,6 +98,7 @@ internal class CreateRasterKeyFrame_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.AnimationData.RemoveKeyFrame(createdKeyFrameId);
+        target.FindMemberOrThrow<ImageLayerNode>(_targetLayerGuid).RemoveKeyFrame(createdKeyFrameId);
         return new DeleteKeyFrame_ChangeInfo(createdKeyFrameId);
     }
 }

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

@@ -42,6 +42,7 @@ internal class DeleteKeyFrame_Change : Change
         out bool ignoreInUndo)
     {
         target.AnimationData.RemoveKeyFrame(_keyFrameId);
+        target.FindNode<Node>(clonedKeyFrame.NodeId).RemoveKeyFrame(_keyFrameId);
         ignoreInUndo = false;
         return new DeleteKeyFrame_ChangeInfo(_keyFrameId);
     }

+ 30 - 36
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -22,7 +22,7 @@ internal class CombineStructureMembersOnto_Change : Change
 
     private Guid targetLayerGuid;
     private Dictionary<int, CommittedChunkStorage> originalChunks = new();
-    
+
     private Dictionary<int, VectorPath> originalPaths = new();
 
 
@@ -160,7 +160,7 @@ internal class CombineStructureMembersOnto_Change : Change
         VectorPath? targetPath = targetData?.ToPath();
 
         var reversed = toCombine.Reverse().ToHashSet();
-        
+
         foreach (var guid in reversed)
         {
             if (target.FindMember(guid) is not VectorLayerNode vectorNode)
@@ -175,10 +175,10 @@ internal class CombineStructureMembersOnto_Change : Change
             {
                 targetData = vectorNode.ShapeData;
                 targetPath = path;
-                
-                if(originalPaths.ContainsKey(frame))
+
+                if (originalPaths.ContainsKey(frame))
                     originalPaths[frame].Dispose();
-                
+
                 originalPaths[frame] = new VectorPath(path);
             }
             else
@@ -188,14 +188,27 @@ internal class CombineStructureMembersOnto_Change : Change
             }
         }
 
-        var pathData = new PathVectorData(targetPath)
+        var clone = targetData.Clone();
+        PathVectorData data;
+        if (clone is not PathVectorData vectorData)
         {
-            StrokeWidth = targetData.StrokeWidth,
-            StrokeColor = targetData.StrokeColor,
-            FillColor = targetData.FillColor
-        };
+            ShapeVectorData shape = clone as ShapeVectorData;
+            data = new PathVectorData(targetPath)
+            {
+                StrokeColor = shape.StrokeColor,
+                FillColor = shape.FillColor,
+                StrokeWidth = shape.StrokeWidth,
+                Fill = shape.Fill,
+                TransformationMatrix = shape.TransformationMatrix,
+            };
+        }
+        else
+        {
+            data = vectorData;
+            data.Path = targetPath;
+        }
 
-        vectorLayer.ShapeData = pathData;
+        vectorLayer.ShapeData = data;
 
         return new AffectedArea(new HashSet<VecI>());
     }
@@ -212,26 +225,7 @@ internal class CombineStructureMembersOnto_Change : Change
         AffectedArea affArea = new();
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
-            if (frame == 0)
-            {
-                renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full);
-            }
-            else
-            {
-                HashSet<Guid> layersToRender = new();
-                foreach (var layer in layersToCombine)
-                {
-                    if (target.FindMember(layer) is LayerNode node)
-                    {
-                        if (node.KeyFrames.Any(x => x.IsInFrame(frame)))
-                        {
-                            layersToRender.Add(layer);
-                        }
-                    }
-                }
-
-                renderer.RenderLayers(tempTexture.DrawingSurface, layersToRender, frame, ChunkResolution.Full);
-            }
+            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full);
 
             toDrawOnImage.EnqueueDrawTexture(VecI.Zero, tempTexture);
 
@@ -328,7 +322,7 @@ internal class CombineStructureMembersOnto_Change : Change
         {
             return VectorRevert(vectorLayerNode, frame);
         }
-        
+
         throw new InvalidOperationException("Layer type not supported");
     }
 
@@ -347,7 +341,7 @@ internal class CombineStructureMembersOnto_Change : Change
         toDrawOnImage.CommitChanges();
         return new LayerImageArea_ChangeInfo(targetLayerGuid, affectedArea);
     }
-    
+
     private IChangeInfo VectorRevert(VectorLayerNode targetLayer, int frame)
     {
         if (!originalPaths.TryGetValue(frame, out var path))
@@ -363,14 +357,14 @@ internal class CombineStructureMembersOnto_Change : Change
         {
             originalChunk.Value.Dispose();
         }
-        
+
         originalChunks.Clear();
-        
+
         foreach (var originalPath in originalPaths)
         {
             originalPath.Value.Dispose();
         }
-        
+
         originalPaths.Clear();
     }
 }

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs

@@ -96,4 +96,14 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
 
         return new VectorShape_ChangeInfo(node.Id, affected);
     }
+
+    public override bool IsMergeableWith(Change other)
+    {
+        if (other is SetShapeGeometry_UpdateableChange change)
+        {
+            return change.TargetId == TargetId;
+        }
+
+        return false;
+    }
 }

+ 19 - 0
src/PixiEditor.sln

@@ -198,6 +198,8 @@ Global
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x64.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevSteam|x64.Build.0 = Release|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.ActiveCfg = Debug|x64
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.Build.0 = Debug|x64
 		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x64.ActiveCfg = DevRelease|x64
@@ -240,6 +242,8 @@ Global
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Steam|x64.Build.0 = Steam|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevSteam|x64.ActiveCfg = DevRelease|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevSteam|x64.Build.0 = DevRelease|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -254,6 +258,8 @@ Global
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x64.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevSteam|x64.Build.0 = Release|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -268,6 +274,8 @@ Global
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x64.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevSteam|x64.Build.0 = Release|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.Build.0 = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -282,6 +290,8 @@ Global
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Steam|x64.Build.0 = Release|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevSteam|x64.Build.0 = Release|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.Build.0 = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -296,6 +306,8 @@ Global
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Steam|x64.Build.0 = Release|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevSteam|x64.Build.0 = Release|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.Build.0 = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -309,6 +321,7 @@ Global
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|x64.Build.0 = Debug|Any CPU
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -336,6 +349,8 @@ Global
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Steam|x64.Build.0 = Release|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevSteam|x64.ActiveCfg = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevSteam|x64.Build.0 = Release|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.Build.0 = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -568,6 +583,8 @@ Global
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Release|x64.Build.0 = Release|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevRelease|x64.ActiveCfg = DevRelease|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevRelease|x64.Build.0 = DevRelease|x64
+		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevSteam|x64.ActiveCfg = DevSteam|x64
+		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevSteam|x64.Build.0 = DevSteam|x64
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -860,6 +877,8 @@ Global
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|ARM64.Build.0 = Release|arm64
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|x64.ActiveCfg = Release|x64
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.Steam|x64.Build.0 = Release|x64
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|x64.ActiveCfg = Release|x64
+		{CD863C88-72E3-40F4-9AAE-5696BBB4460C}.DevSteam|x64.Build.0 = Release|x64
 		{8064811C-6A34-456E-A3F2-D77395009A29}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{8064811C-6A34-456E-A3F2-D77395009A29}.Debug|x64.Build.0 = Debug|Any CPU
 		{8064811C-6A34-456E-A3F2-D77395009A29}.Debug|ARM64.ActiveCfg = Debug|Any CPU

+ 2 - 2
src/PixiEditor/Helpers/SurfaceHelpers.cs

@@ -36,11 +36,11 @@ public static class SurfaceHelpers
         return result;
     }
 
-    public static unsafe byte[] ToByteArray(this Surface surface, ColorType colorType = ColorType.Bgra8888, AlphaType alphaType = AlphaType.Premul)
+    public static unsafe byte[] ToByteArray(this Surface surface, ColorType colorType = ColorType.Bgra8888, AlphaType alphaType = AlphaType.Premul, ColorSpace colorSpace = null)
     {
         int width = surface.Size.X;
         int height = surface.Size.Y;
-        var imageInfo = new ImageInfo(width, height, colorType, alphaType, ColorSpace.CreateSrgb());
+        var imageInfo = new ImageInfo(width, height, colorType, alphaType, colorSpace == null ? surface.ImageInfo.ColorSpace : colorSpace);
 
         byte[] buffer = new byte[width * height * imageInfo.BytesPerPixel];
         fixed (void* pointer = buffer)

+ 1 - 1
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -104,7 +104,7 @@ internal class ClassicDesktopEntry
 
     private IPlatform GetActivePlatform()
     {
-#if STEAM
+#if STEAM || DEV_STEAM
         return new PixiEditor.Platform.Steam.SteamPlatform();
 #elif MSIX || MSIX_DEBUG
         return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();

+ 11 - 4
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -446,7 +446,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         IMidChangeUndoableExecutor executor =
             Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
-        if (executor is { CanRedo: true })
+        if (executor is { CanRedo: true }) 
         {
             executor.OnMidChangeRedo();
             return;
@@ -455,7 +455,11 @@ internal class DocumentOperationsModule : IDocumentOperations
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
-        Internals.ChangeController.TryStopActiveExecutor();
+        if (!Internals.ChangeController.IsChangeOfTypeActive<IMidChangeUndoableExecutor>())
+        {
+            Internals.ChangeController.TryStopActiveExecutor();
+        }
+
         Internals.ActionAccumulator.AddActions(new Redo_Action());
     }
 
@@ -807,12 +811,15 @@ internal class DocumentOperationsModule : IDocumentOperations
         Internals.ActionAccumulator.AddFinishedActions(new RasterizeMember_Action(memberId));
     }
 
-    public void InvokeCustomAction(Action action)
+    public void InvokeCustomAction(Action action, bool stopActiveExecutor = true)
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
-        Internals.ChangeController.TryStopActiveExecutor();
+        if (stopActiveExecutor)
+        {
+            Internals.ChangeController.TryStopActiveExecutor();
+        }
 
         IAction targetAction = new InvokeAction_PassthroughAction(action);
 

+ 41 - 17
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -1,8 +1,6 @@
 using ChunkyImageLib.DataHolders;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.ChangeableDocument.Actions;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using Drawie.Backend.Core.ColorsImpl;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Toolbars;
@@ -10,7 +8,8 @@ using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+using PixiEditor.ViewModels.Document.TransformOverlays;
+using PixiEditor.Views.Overlays.TransformOverlay;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -35,10 +34,13 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     protected IFillableShapeToolbar toolbar;
     private IColorsHandler? colorsVM;
     private bool ignoreNextColorChange = false;
+    
+    protected abstract bool UseGlobalUndo { get; }
+    protected abstract bool ShowApplyButton { get; }
 
-    public override bool CanUndo => document.TransformHandler.HasUndo;
-    public override bool CanRedo => document.TransformHandler.HasRedo;
-
+    public override bool CanUndo => !UseGlobalUndo && document.TransformHandler.HasUndo;
+    public override bool CanRedo => !UseGlobalUndo && document.TransformHandler.HasRedo;
+    
     public override ExecutionState Start()
     {
         if (base.Start() == ExecutionState.Error)
@@ -55,7 +57,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             return ExecutionState.Error;
         if (!drawOnMask && member is not ILayerHandler)
             return ExecutionState.Error;
-
+        
         if (ActiveMode == ShapeToolMode.Drawing)
         {
             if (toolbar.SyncWithPrimaryColor)
@@ -68,7 +70,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             lastRect = new RectD(startDrawingPos, VecD.Zero);
 
             document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect.Inflate(1)),
-                false);
+                false, UseGlobalUndo ? AddToUndo : null);
             document.TransformHandler.ShowHandles = false;
             document.TransformHandler.IsSizeBoxEnabled = true;
             document.TransformHandler.CanAlignToPixels = AlignToPixels;
@@ -91,6 +93,11 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
             toolbar.ToolSize = shapeData.StrokeWidth;
             toolbar.Fill = shapeData.FillColor != Colors.Transparent;
             initialCorners = shapeData.TransformationCorners;
+            
+            ShapeCorners corners = vectorLayerHandler.TransformationCorners;
+            document.TransformHandler.ShowTransform(
+                TransformMode, false, corners, false, UseGlobalUndo ? AddToUndo : null);
+            document.TransformHandler.CanAlignToPixels = false;
 
             ActiveMode = ShapeToolMode.Transform;
         }
@@ -118,8 +125,8 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         VecI pos2 = (VecI)(((VecD)curPos).ProjectOntoLine(startPos, startPos + new VecD(1, -1)) -
                            new VecD(0.25).Multiply((curPos - startPos).Signs())).Round();
         if ((pos1 - curPos).LengthSquared > (pos2 - curPos).LengthSquared)
-            return (VecI)pos2;
-        return (VecI)pos1;
+            return pos2;
+        return pos1;
     }
 
     public static VecD GetSquaredPosition(VecD startPos, VecD curPos)
@@ -138,13 +145,19 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         if (ActiveMode != ShapeToolMode.Transform)
             return;
 
+        var shapeData = ShapeDataFromCorners(corners);
+        IAction drawAction = TransformMovedAction(shapeData, corners);
+
+        internals!.ActionAccumulator.AddActions(drawAction);
+    }
+
+    private ShapeData ShapeDataFromCorners(ShapeCorners corners)
+    {
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
         ShapeData shapeData = new ShapeData(rect.Center, rect.Size, corners.RectRotation, (float)StrokeWidth,
             StrokeColor,
             FillColor) { AntiAliasing = toolbar.AntiAliasing };
-        IAction drawAction = TransformMovedAction(shapeData, corners);
-
-        internals!.ActionAccumulator.AddActions(drawAction);
+        return shapeData;
     }
 
     public override void OnTransformApplied()
@@ -190,14 +203,16 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     {
         if (ActiveMode != ShapeToolMode.Transform)
             return;
-        document!.TransformHandler.Undo();
+
+        document.TransformHandler.Undo();
     }
 
     public override void OnMidChangeRedo()
     {
         if (ActiveMode != ShapeToolMode.Transform)
             return;
-        document!.TransformHandler.Redo();
+
+        document.TransformHandler.Redo();
     }
 
     protected override void PrecisePositionChangeDrawingMode(VecD pos)
@@ -240,7 +255,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         startDrawingPos = startPos;
 
-        document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false);
+        document!.TransformHandler.ShowTransform(TransformMode, false, new ShapeCorners((RectD)lastRect), false, UseGlobalUndo ? AddToUndo : null);
         document.TransformHandler.CanAlignToPixels = AlignToPixels;
         document!.TransformHandler.Corners = new ShapeCorners((RectD)lastRect);
     }
@@ -318,6 +333,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         if (CanEditShape(layer))
         {
             internals!.ActionAccumulator.AddActions(SettingsChangedAction());
+            // TODO add to undo
         }
     }
 
@@ -354,7 +370,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         if (mode == ShapeToolMode.Transform)
         {
             document.TransformHandler.HideTransform();
-            document!.TransformHandler.ShowTransform(TransformMode, false, initialCorners, true);
+            document!.TransformHandler.ShowTransform(TransformMode, false, initialCorners, ShowApplyButton, UseGlobalUndo ? AddToUndo : null);
             document.TransformHandler.CanAlignToPixels = AlignToPixels;
         }
     }
@@ -369,4 +385,12 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     {
         document!.TransformHandler.HideTransform();
     }
+    
+    private void AddToUndo(ShapeCorners corners)
+    {
+        if (UseGlobalUndo)
+        {
+            internals!.ActionAccumulator.AddFinishedActions(EndDrawAction(), TransformMovedAction(ShapeDataFromCorners(corners), corners), EndDrawAction());
+        }
+    }
 }

+ 59 - 11
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -9,8 +9,10 @@ using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.ViewModels.Document.TransformOverlays;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -20,6 +22,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
     protected Color StrokeColor => toolbar!.StrokeColor.ToColor();
     protected double StrokeWidth => toolViewModel!.ToolSize;
+    protected abstract bool UseGlobalUndo { get; }
+    protected abstract bool ShowApplyButton { get; }
 
     protected bool drawOnMask;
 
@@ -31,8 +35,10 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     private bool ignoreNextColorChange = false;
     private VecD lastStartPos;
 
-    public override bool CanUndo => document.LineToolOverlayHandler.HasUndo;
-    public override bool CanRedo => document.LineToolOverlayHandler.HasRedo;
+    private UndoStack<LineVectorData>? localUndoStack;
+
+    public override bool CanUndo => !UseGlobalUndo && localUndoStack is { UndoCount: > 0 };
+    public override bool CanRedo => !UseGlobalUndo && localUndoStack is { RedoCount: > 0 };
 
     public override ExecutionState Start()
     {
@@ -51,6 +57,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return ExecutionState.Error;
         if (!drawOnMask && member is not ILayerHandler)
             return ExecutionState.Error;
+        
+        localUndoStack = new UndoStack<LineVectorData>();
 
         if (ActiveMode == ShapeToolMode.Drawing)
         {
@@ -61,7 +69,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             }
 
             document.LineToolOverlayHandler.Hide();
-            document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false);
+            document.LineToolOverlayHandler.Show(startDrawingPos, startDrawingPos, false, AddToUndo);
             document.LineToolOverlayHandler.ShowHandles = false;
             document.LineToolOverlayHandler.IsSizeBoxEnabled = true;
 
@@ -85,6 +93,8 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             }
 
             ActiveMode = ShapeToolMode.Transform;
+
+            document.LineToolOverlayHandler.Show(data.Start, data.End, false, AddToUndo);
         }
         else
         {
@@ -97,7 +107,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     protected abstract bool InitShapeData(IReadOnlyLineData? data);
     protected abstract IAction DrawLine(VecD pos);
     protected abstract IAction TransformOverlayMoved(VecD start, VecD end);
-    protected abstract IAction SettingsChange();
+    protected abstract IAction[] SettingsChange();
     protected abstract IAction EndDraw();
 
     protected override void PrecisePositionChangeDrawingMode(VecD pos)
@@ -159,8 +169,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return;
         }
 
+        AddToUndo((lastStartPos, curPos));
         document!.LineToolOverlayHandler.Hide();
-        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, true);
+        document!.LineToolOverlayHandler.Show(lastStartPos, curPos, ShowApplyButton, AddToUndo);
         base.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
 
@@ -200,28 +211,40 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
             return;
 
         document!.LineToolOverlayHandler.Nudge(distance);
+        AddToUndo((document.LineToolOverlayHandler.LineStart, document.LineToolOverlayHandler.LineEnd));
     }
 
     public override void OnSettingsChanged(string name, object value)
     {
-        var colorChangedAction = SettingsChange();
-        internals!.ActionAccumulator.AddActions(colorChangedAction);
+        var colorChangedActions = SettingsChange();
+        if (ActiveMode == ShapeToolMode.Transform)
+        {
+            internals!.ActionAccumulator.AddFinishedActions(colorChangedActions);
+        }
     }
 
     public override void OnMidChangeUndo()
     {
-        if (ActiveMode != ShapeToolMode.Transform)
+        if (ActiveMode != ShapeToolMode.Transform || localUndoStack == null)
             return;
 
-        document!.LineToolOverlayHandler.Undo();
+        var undone = localUndoStack?.Undo();
+        if (undone is not null)
+        {
+            ApplyState(undone);
+        }
     }
 
     public override void OnMidChangeRedo()
     {
-        if (ActiveMode != ShapeToolMode.Transform)
+        if (ActiveMode != ShapeToolMode.Transform || localUndoStack == null)
             return;
 
-        document!.LineToolOverlayHandler.Redo();
+        var redone = localUndoStack?.Redo();
+        if (redone is not null)
+        {
+            ApplyState(redone);
+        }
     }
 
     public override void OnTransformApplied()
@@ -244,4 +267,29 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
     {
         document!.LineToolOverlayHandler.Hide();
     }
+
+    private void AddToUndo((VecD, VecD) newPos)
+    {
+        if (UseGlobalUndo)
+        {
+            internals!.ActionAccumulator.AddFinishedActions(EndDraw(), TransformOverlayMoved(newPos.Item1, newPos.Item2), EndDraw());
+        }
+        else
+        {
+            localUndoStack!.AddState(ConstructLineData(newPos.Item1, newPos.Item2));
+        }
+    }
+
+    protected LineVectorData ConstructLineData(VecD start, VecD end)
+    {
+        return new LineVectorData(start, end) { StrokeWidth = (float)StrokeWidth, StrokeColor = StrokeColor };
+    }
+    
+    private void ApplyState(LineVectorData data)
+    {
+        toolbar!.StrokeColor = data.StrokeColor.ToColor();
+        toolbar!.ToolSize = data.StrokeWidth;
+        
+        document!.LineToolOverlayHandler.Show(data.Start, data.End, ShowApplyButton, AddToUndo);
+    }
 }

+ 2 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterEllipseToolExecutor.cs

@@ -29,6 +29,8 @@ internal class RasterEllipseToolExecutor : DrawableShapeToolExecutor<IRasterElli
 
     public override ExecutorType Type => ExecutorType.ToolLinked;
     protected override DocumentTransformMode TransformMode => DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective;
+    protected override bool UseGlobalUndo => false;
+    protected override bool ShowApplyButton => true;
     protected override void DrawShape(VecD currentPos, double rotationRad, bool firstDraw) => DrawEllipseOrCircle(currentPos, rotationRad, firstDraw);
     protected override IAction SettingsChangedAction()
     {

+ 6 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterLineToolExecutor.cs

@@ -9,6 +9,9 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
 {
+    protected override bool UseGlobalUndo => false;
+    protected override bool ShowApplyButton => true;
+
     protected override bool InitShapeData(IReadOnlyLineData? data)
     {
         return false;
@@ -30,12 +33,12 @@ internal class RasterLineToolExecutor : LineExecutor<ILineToolHandler>
             (float)StrokeWidth, StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
     }
 
-    protected override IAction SettingsChange()
+    protected override IAction[] SettingsChange()
     {
         VecD dir = GetSignedDirection(startDrawingPos, curPos);
         VecD oppositeDir = new VecD(-dir.X, -dir.Y);
-        return new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), (float)StrokeWidth,
-            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
+        return [new DrawRasterLine_Action(memberId, ToPixelPos(startDrawingPos, oppositeDir), ToPixelPos(curPos, dir), (float)StrokeWidth,
+            StrokeColor, StrokeCap.Butt, toolbar.AntiAliasing, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)];
     }
 
     private VecI ToPixelPos(VecD pos, VecD dir)

+ 3 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs

@@ -35,6 +35,9 @@ internal class RasterRectangleToolExecutor : DrawableShapeToolExecutor<IRasterRe
             document!.AnimationHandler.ActiveFrameBindable));
     }
 
+    protected override bool UseGlobalUndo => false;
+    protected override bool ShowApplyButton => true;
+
     protected override void DrawShape(VecD currentPos, double rotationRad, bool first) =>
         DrawRectangle(currentPos, rotationRad, first);
 

+ 8 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -56,6 +56,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     private ExecutionState SelectMembers(List<IStructureMemberHandler> members)
     {
         bool allRaster = true;
+        bool anyRaster = false;
         memberCorners = new();
         foreach (IStructureMemberHandler member in members)
         {
@@ -69,6 +70,11 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             {
                 allRaster = false;
             }
+            
+            if (member is IRasterLayerHandler)
+            {
+                anyRaster = true;
+            }
 
             memberCorners.Add(member.Id, targetCorners);
         }
@@ -111,6 +117,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         lastCorners = masterCorners;
         document.TransformHandler.ShowTransform(mode, true, masterCorners,
             Type == ExecutorType.Regular || tool.KeepOriginalImage);
+        
+        document.TransformHandler.CanAlignToPixels = anyRaster;
 
         internals!.ActionAccumulator.AddActions(
             new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false,

+ 15 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorEllipseToolExecutor.cs

@@ -8,6 +8,7 @@ using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -41,11 +42,14 @@ internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorElli
         IVectorLayerHandler vectorLayer = layer as IVectorLayerHandler;
         if (vectorLayer is null)
             return false;
-        
+
         var shapeData = vectorLayer.GetShapeData(document.AnimationHandler.ActiveFrameTime);
         return shapeData is EllipseVectorData;
     }
 
+    protected override bool UseGlobalUndo => true;
+    protected override bool ShowApplyButton => false;
+
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     {
         RectD rect;
@@ -90,9 +94,10 @@ internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorElli
         {
             firstCenter = corners.RectCenter;
             firstRadius = corners.RectSize / 2f;
-            
-            if(corners.RectRotation != 0)
-                matrix = Matrix3X3.CreateRotation((float)corners.RectRotation, (float)firstCenter.X, (float)firstCenter.Y);
+
+            if (corners.RectRotation != 0)
+                matrix = Matrix3X3.CreateRotation((float)corners.RectRotation, (float)firstCenter.X,
+                    (float)firstCenter.Y);
         }
         else
         {
@@ -119,4 +124,10 @@ internal class VectorEllipseToolExecutor : DrawableShapeToolExecutor<IVectorElli
     {
         return new EndSetShapeGeometry_Action();
     }
+
+    public override bool IsFeatureEnabled(IExecutorFeature feature)
+    {
+        if(feature is IMidChangeUndoableExecutor) return false;
+        return base.IsFeatureEnabled(feature);
+    }
 }

+ 15 - 12
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 
@@ -15,6 +16,9 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override bool AlignToPixels => false;
 
+    protected override bool UseGlobalUndo => true;
+    protected override bool ShowApplyButton => false;
+
     protected override bool InitShapeData(IReadOnlyLineData? data)
     {
         if (data is null)
@@ -28,11 +32,7 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override IAction DrawLine(VecD pos)
     {
-        LineVectorData data = new LineVectorData(startDrawingPos, pos)
-        {
-            StrokeColor = StrokeColor,
-            StrokeWidth = (float)StrokeWidth,
-        };
+        LineVectorData data = ConstructLineData(startDrawingPos, pos);
         
         startPoint = startDrawingPos;
         endPoint = pos;
@@ -42,11 +42,7 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
 
     protected override IAction TransformOverlayMoved(VecD start, VecD end)
     {
-        LineVectorData data = new LineVectorData(start, end)
-        {
-            StrokeColor = StrokeColor,
-            StrokeWidth = (float)StrokeWidth,
-        };
+        var data = ConstructLineData(start, end);
         
         startPoint = start;
         endPoint = end;
@@ -54,13 +50,20 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         return new SetShapeGeometry_Action(memberId, data);
     }
 
-    protected override IAction SettingsChange()
+    protected override IAction[] SettingsChange()
     {
-        return TransformOverlayMoved(startPoint, endPoint);
+        return [TransformOverlayMoved(startPoint, endPoint), new EndSetShapeGeometry_Action()];
     }
 
     protected override IAction EndDraw()
     {
         return new EndSetShapeGeometry_Action();
     }
+
+    public override bool IsFeatureEnabled(IExecutorFeature feature)
+    {
+        if(feature is IMidChangeUndoableExecutor) return false;
+        
+        return base.IsFeatureEnabled(feature);
+    }
 }

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

@@ -179,7 +179,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
         if (document.PathOverlayHandler.IsActive)
         {
-            internals.ActionAccumulator.AddActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData(startingPath)));
+            internals.ActionAccumulator.AddFinishedActions(new SetShapeGeometry_Action(member.Id, ConstructShapeData(startingPath)), new EndSetShapeGeometry_Action());
         }
     }
 

+ 10 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs

@@ -8,6 +8,7 @@ using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
@@ -45,6 +46,9 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         return shapeData is RectangleVectorData;
     }
 
+    protected override bool UseGlobalUndo => true;
+    protected override bool ShowApplyButton => false;
+
     protected override void DrawShape(VecD curPos, double rotationRad, bool firstDraw)
     {
         RectD rect;
@@ -127,4 +131,10 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
     {
         return new EndSetShapeGeometry_Action();
     }
+
+    public override bool IsFeatureEnabled(IExecutorFeature feature)
+    {
+        if (feature is IMidChangeUndoableExecutor) return false;
+        return base.IsFeatureEnabled(feature);
+    }
 }

+ 1 - 5
src/PixiEditor/Models/Handlers/ILineOverlayHandler.cs

@@ -7,11 +7,7 @@ internal interface ILineOverlayHandler
 {
     public void Hide();
     public bool Nudge(VecD distance);
-    public bool Undo();
-    public bool Redo();
-    public void Show(VecD startPos, VecD endPos, bool showApplyButton);
-    public bool HasUndo { get; }
-    public bool HasRedo { get; }
+    public void Show(VecD startPos, VecD endPos, bool showApplyButton, Action<(VecD, VecD)> addToUndo);
     public VecD LineStart { get; set; }
     public VecD LineEnd { get; set; }
     public bool ShowHandles { get; set; }

+ 3 - 3
src/PixiEditor/Models/Handlers/IToolHandler.cs

@@ -58,12 +58,12 @@ internal interface IToolHandler : IHandler
 
     public void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown);
     public void UseTool(VecD pos);
-    public void OnSelected(bool restoring);
+    public void OnToolSelected(bool restoring);
 
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
     public void ApplyToolSetSettings(IToolSetHandler toolset);
-    public void OnDeselecting(bool transient);
-    
+    public void OnToolDeselected(bool transient);
     public void OnPostUndo();
     public void OnPostRedo();
+    public void OnActiveFrameChanged(int newFrame);
 }

+ 2 - 1
src/PixiEditor/Models/Handlers/ITransformHandler.cs

@@ -3,13 +3,14 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.Models.DocumentModels;
 using Drawie.Numerics;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Views.Overlays.TransformOverlay;
 
 namespace PixiEditor.Models.Handlers;
 
 internal interface ITransformHandler : IHandler
 {
     public void KeyModifiersInlet(bool argsIsShiftDown, bool argsIsCtrlDown, bool argsIsAltDown);
-    public void ShowTransform(DocumentTransformMode transformMode, bool coverWholeScreen, ShapeCorners shapeCorners, bool showApplyButton);
+    public void ShowTransform(DocumentTransformMode transformMode, bool coverWholeScreen, ShapeCorners shapeCorners, bool showApplyButton, Action<ShapeCorners>? customAddToUndo = null);
     public void HideTransform();
     public bool Undo();
     public bool Redo();

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

@@ -125,6 +125,7 @@ internal class Exporter
         {
             job?.Finish();
             Console.WriteLine(e);
+            CrashHelper.SendExceptionInfoToWebhook(e);
             return SaveResult.UnknownError;
         }
     }

+ 1 - 1
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -24,7 +24,7 @@ internal class SceneRenderer
 
     public void RenderScene(DrawingSurface target, ChunkResolution resolution)
     {
-        if(Document.Renderer.IsBusy) return;
+        if(Document.Renderer.IsBusy || DocumentViewModel.Busy) return;
         RenderOnionSkin(target, resolution);
         RenderGraph(target, resolution);
     }

+ 1 - 1
src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs

@@ -35,7 +35,7 @@ public class ChunkyImageSerializationFactory : SerializationFactory<byte[], Chun
                 return false;
             }
 
-            original = new ChunkyImage(surface.Size, Config.ProcessingProcessingColorSpace);
+            original = new ChunkyImage(surface.Size, Config.ProcessingColorSpace);
             original.EnqueueDrawImage(VecI.Zero, surface);
             original.CommitChanges();
             return true;

+ 11 - 6
src/PixiEditor/Models/Serialization/Factories/SurfaceSerializationFactory.cs

@@ -11,7 +11,8 @@ public class SurfaceSerializationFactory : SerializationFactory<byte[], Surface>
     public override byte[] Serialize(Surface original)
     {
         var encoder = Config.Encoder;
-        byte[] result = encoder.Encode(original.ToByteArray(), original.Size.X, original.Size.Y);
+        byte[] result = encoder.Encode(original.ToByteArray(), original.Size.X, original.Size.Y,
+            original.ImageInfo.ColorSpace?.IsSrgb ?? true);
 
         return result;
     }
@@ -21,21 +22,25 @@ public class SurfaceSerializationFactory : SerializationFactory<byte[], Surface>
     {
         if (serialized is byte[] imgBytes)
         {
-            original = DecodeSurface(imgBytes, Config.Encoder);
+            original = DecodeSurface(imgBytes, Config.Encoder, Config.ProcessingColorSpace);
             return true;
         }
-        
+
         original = null;
         return false;
     }
 
 
-    public static Surface DecodeSurface(byte[] imgBytes, ImageEncoder encoder)
+    public static Surface DecodeSurface(byte[] imgBytes, ImageEncoder encoder, ColorSpace processingColorSpace)
     {
         byte[] decoded =
             encoder.Decode(imgBytes, out SKImageInfo info);
-        using Image img = Image.FromPixels(info.ToImageInfo(), decoded);
-        Surface surface = new Surface(img.Size);
+        ImageInfo finalInfo = info.ToImageInfo();
+
+        using Image img = Image.FromPixels(finalInfo, decoded);
+        Surface surface = Surface.ForDisplay(finalInfo.Size);
+
+
         surface.DrawingSurface.Canvas.DrawImage(img, 0, 0);
 
         return surface;

+ 2 - 2
src/PixiEditor/Models/Serialization/SerializationConfig.cs

@@ -6,11 +6,11 @@ namespace PixiEditor.Models.Serialization;
 public class SerializationConfig
 {
     public ImageEncoder Encoder { get; set; }
-    public ColorSpace ProcessingProcessingColorSpace { get; set; }
+    public ColorSpace ProcessingColorSpace { get; set; }
     
     public SerializationConfig(ImageEncoder encoder, ColorSpace processingColorSpace)
     {
         Encoder = encoder;
-        ProcessingProcessingColorSpace = processingColorSpace;
+        ProcessingColorSpace = processingColorSpace;
     }
 }

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

@@ -24,6 +24,8 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public IReadOnlyCollection<ICelHandler> AllCels => allCels;
 
+    public event Action<int, int> ActiveFrameChanged;
+    
     private KeyFrameCollection keyFrames = new KeyFrameCollection();
     private List<ICelHandler> allCels = new List<ICelHandler>();
     private bool onionSkinningEnabled;
@@ -187,7 +189,9 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public void SetActiveFrame(int newFrame)
     {
+        int previousFrame = _activeFrameBindable;
         _activeFrameBindable = newFrame;
+        ActiveFrameChanged?.Invoke(previousFrame, newFrame);
         OnPropertyChanged(nameof(ActiveFrameBindable));
     }
     
@@ -386,4 +390,5 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         this.keyFrames = new KeyFrameCollection(layerKeyFrames);
         OnPropertyChanged(nameof(KeyFrames));
     }
+
 }

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

@@ -431,7 +431,7 @@ internal partial class DocumentViewModel
         var shape = layer.Shape;
         var imageSize = layer.ImageSize;
 
-        var imageBytes = config.Encoder.Encode(layer.ImageBgra8888Bytes.ToArray(), imageSize.X, imageSize.Y);
+        var imageBytes = config.Encoder.Encode(layer.ImageBgra8888Bytes.ToArray(), imageSize.X, imageSize.Y, true);
 
         return new ReferenceLayer
         {

+ 6 - 2
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -160,6 +160,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public bool IsChangeFeatureActive<T>() where T : IExecutorFeature =>
         Internals.ChangeController.IsChangeOfTypeActive<T>();
+    
+    public T? TryGetExecutorFeature<T>() where T : IExecutorFeature =>
+        Internals.ChangeController.TryGetExecutorFeature<T>();
 
     public bool PointerDragChangeInProgress =>
         Internals.ChangeController.IsBlockingChangeActive && Internals.ChangeController.LeftMousePressed;
@@ -880,8 +883,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             return [];
 
         int firstFrame = AnimationDataViewModel.FirstFrame;
-        int framesCount = AnimationDataViewModel.FramesCount;
-        int lastFrame = firstFrame + framesCount;
+        int lastFrame = AnimationDataViewModel.LastFrame;
+        
+        int framesCount = lastFrame - firstFrame;
 
         Image[] images = new Image[framesCount];
 

+ 44 - 26
src/PixiEditor/ViewModels/Document/TransformOverlays/DocumentTransformViewModel.cs

@@ -61,6 +61,13 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         get => lockRotation;
         set => SetProperty(ref lockRotation, value);
     }
+    
+    private bool lockShear;
+    public bool LockShear
+    {
+        get => lockShear;
+        set => SetProperty(ref lockShear, value);
+    }
 
     private bool snapToAngles;
 
@@ -101,8 +108,9 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         get => showTransformControls;
         set => SetProperty(ref showTransformControls, value);
     }
-    
+
     private bool canAlignToPixels = true;
+
     public bool CanAlignToPixels
     {
         get => canAlignToPixels;
@@ -147,14 +155,6 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         set => SetProperty(ref isSizeBoxEnabled, value);
     }
 
-    private bool enableSnapping = true;
-
-    public bool EnableSnapping
-    {
-        get => enableSnapping;
-        set => SetProperty(ref enableSnapping, value);
-    }
-
 
     private ExecutionTrigger<ShapeCorners> requestedCornersExecutor;
 
@@ -172,6 +172,14 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         set => SetProperty(ref actionCompletedCommand, value);
     }
 
+    private ICommand? addToUndoCommand = null;
+
+    public ICommand? AddToUndoCommand
+    {
+        get => addToUndoCommand;
+        set => SetProperty(ref addToUndoCommand, value);
+    }
+
     private RelayCommand<MouseOnCanvasEventArgs>? passThroughPointerPressedCommand;
 
     public RelayCommand<MouseOnCanvasEventArgs> PassThroughPointerPressedCommand
@@ -192,6 +200,8 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
         this.document = document;
         ActionCompletedCommand = new RelayCommand(() =>
         {
+            AddToUndoCommand?.Execute(Corners);
+
             if (undoStack is null)
                 return;
 
@@ -230,12 +240,13 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
     public bool Nudge(VecD distance)
     {
-        if (undoStack is null)
-            return false;
-
         InternalState = InternalState with { Origin = InternalState.Origin + distance };
         Corners = Corners.AsTranslated(distance);
-        undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Nudge);
+        
+        AddToUndoCommand?.Execute(Corners);
+
+        undoStack?.AddState((Corners, InternalState), TransformOverlayStateType.Nudge);
+
         return true;
     }
 
@@ -244,25 +255,22 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
     public void HideTransform()
     {
-        if (undoStack is null)
-            return;
         undoStack = null;
-
         TransformActive = false;
         ShowTransformControls = false;
     }
 
     public void ShowTransform(DocumentTransformMode mode, bool coverWholeScreen, ShapeCorners initPos,
-        bool showApplyButton)
+        bool showApplyButton, Action<ShapeCorners>? customAddToUndo = null)
     {
-        if (undoStack is not null || initPos.IsPartiallyDegenerate)
+        if (initPos.IsPartiallyDegenerate)
             return;
-        undoStack = new();
 
         activeTransformMode = mode;
         CornerFreedom = TransformCornerFreedom.Scale;
         SideFreedom = TransformSideFreedom.Stretch;
         LockRotation = mode == DocumentTransformMode.Scale_NoRotate_NoShear_NoPerspective;
+        LockShear = mode is DocumentTransformMode.Scale_Rotate_NoShear_NoPerspective or DocumentTransformMode.Scale_NoRotate_NoShear_NoPerspective;
         CoverWholeScreen = coverWholeScreen;
         TransformActive = true;
         ShowTransformControls = showApplyButton;
@@ -270,9 +278,19 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
         IsSizeBoxEnabled = false;
         ShowHandles = true;
-
+        
         RequestCornersExecutor?.Execute(this, initPos);
-        undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Initial);
+
+        if (customAddToUndo is not null)
+        {
+            AddToUndoCommand = new RelayCommand<ShapeCorners>(customAddToUndo);
+            undoStack = null;
+        }
+        else
+        {
+            undoStack = new TransformOverlayUndoStack<(ShapeCorners, TransformState)>();
+            undoStack.AddState((Corners, InternalState), TransformOverlayStateType.Initial);
+        }
     }
 
     public void KeyModifiersInlet(bool isShiftDown, bool isCtrlDown, bool isAltDown)
@@ -291,11 +309,6 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
             requestedCornerFreedom = TransformCornerFreedom.Free;
             requestedSideFreedom = TransformSideFreedom.Free;
         }
-        /*else if (isAltDown)
-        {
-        TODO: Add shear to the transform overlay
-            requestedSideFreedom = TransformSideFreedom.Shear;
-        }*/
         else
         {
             requestedCornerFreedom = TransformCornerFreedom.Scale;
@@ -304,6 +317,11 @@ internal class DocumentTransformViewModel : ObservableObject, ITransformHandler
 
         ScaleFromCenter = isCtrlDown;
 
+        UpdateFreedom(requestedCornerFreedom, requestedSideFreedom);
+    }
+
+    private void UpdateFreedom(TransformCornerFreedom requestedCornerFreedom, TransformSideFreedom requestedSideFreedom)
+    {
         switch (activeTransformMode)
         {
             case DocumentTransformMode.Scale_Rotate_Shear_Perspective:

+ 10 - 46
src/PixiEditor/ViewModels/Document/TransformOverlays/LineToolOverlayViewModel.cs

@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 namespace PixiEditor.ViewModels.Document.TransformOverlays;
 
@@ -10,8 +11,6 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 {
     public event EventHandler<(VecD, VecD)>? LineMoved;
 
-    private TransformOverlayUndoStack<(VecD, VecD)>? undoStack = null;
-
     private VecD lineStart;
 
     public VecD LineStart
@@ -66,6 +65,13 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
         set => SetProperty(ref actionCompletedCommand, value);
     }
 
+    private ICommand addToUndoCommand = null;
+    public ICommand AddToUndoCommand
+    {
+        get => addToUndoCommand;
+        set => SetProperty(ref addToUndoCommand, value);
+    }
+    
     private bool showApplyButton;
 
     public bool ShowApplyButton
@@ -76,34 +82,21 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 
     public LineToolOverlayViewModel()
     {
-        ActionCompletedCommand =
-            new RelayCommand(() => undoStack?.AddState((LineStart, LineEnd), TransformOverlayStateType.Move));
     }
 
-    public void Show(VecD lineStart, VecD endPos, bool showApplyButton)
+    public void Show(VecD lineStart, VecD endPos, bool showApplyButton, Action<(VecD, VecD)> addToUndo)
     {
-        if (undoStack is not null)
-            return;
-        undoStack = new();
-        
-        undoStack.AddState((lineStart, endPos), TransformOverlayStateType.Initial);
-
         LineStart = lineStart;
         LineEnd = endPos; 
         IsEnabled = true;
         ShowApplyButton = showApplyButton;
         ShowHandles = true;
         IsSizeBoxEnabled = false;
+        AddToUndoCommand = new RelayCommand(() => addToUndo((LineStart, LineEnd)));
     }
 
-    public bool HasUndo => undoStack is not null && undoStack.UndoCount > 0;
-    public bool HasRedo => undoStack is not null && undoStack.RedoCount > 0; 
-
     public void Hide()
     {
-        if (undoStack is null)
-            return;
-        undoStack = null;
         IsEnabled = false;
         ShowApplyButton = false;
         IsSizeBoxEnabled = false;
@@ -111,37 +104,8 @@ internal class LineToolOverlayViewModel : ObservableObject, ILineOverlayHandler
 
     public bool Nudge(VecD distance)
     {
-        if (undoStack is null)
-            return false;
         LineStart = LineStart + distance;
         LineEnd = LineEnd + distance;
-        undoStack.AddState((lineStart, lineEnd), TransformOverlayStateType.Nudge);
-        return true;
-    }
-
-    public bool Undo()
-    {
-        if (undoStack is null)
-            return false;
-
-        var newState = undoStack.Undo();
-        if (newState is null)
-            return false;
-        LineStart = newState.Value.Item1;
-        LineEnd = newState.Value.Item2;
-        return true;
-    }
-
-    public bool Redo()
-    {
-        if (undoStack is null)
-            return false;
-
-        var newState = undoStack.Redo();
-        if (newState is null)
-            return false;
-        LineStart = newState.Value.Item1;
-        LineEnd = newState.Value.Item2;
         return true;
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Document/TransformOverlays/PathOverlayUndoStack.cs → src/PixiEditor/ViewModels/Document/TransformOverlays/UndoStack.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.ViewModels.Document.TransformOverlays;
 
-internal class PathOverlayUndoStack<TState> : IDisposable where TState : class
+internal class UndoStack<TState> : IDisposable where TState : class
 {
     private struct StackItem<TState>
     {

+ 15 - 3
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -228,7 +228,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
 
         if (ActiveTool != null)
         {
-            ActiveTool.OnDeselecting(transient);
+            ActiveTool.OnToolDeselected(transient);
             ActiveTool.Toolbar.SettingChanged -= ToolbarSettingChanged;
         }
 
@@ -261,7 +261,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         LastActionTool?.ModifierKeyChanged(false, false, false);
         //update new tool
         ActiveTool.ModifierKeyChanged(ctrlIsDown, shiftIsDown, altIsDown);
-        ActiveTool.OnSelected(wasTransient);
+        ActiveTool.OnToolSelected(wasTransient);
 
         tool.IsActive = true;
         ActiveTool.IsTransient = transient;
@@ -475,15 +475,22 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         {
             e.OldDocument.PropertyChanged -= DocumentOnPropertyChanged;
             e.OldDocument.LayersChanged -= DocumentOnLayersChanged;
+            e.OldDocument.AnimationDataViewModel.ActiveFrameChanged -= ActiveFrameChanged;
         }
 
         if (e.NewDocument is not null)
         {
             e.NewDocument.PropertyChanged += DocumentOnPropertyChanged;
             e.NewDocument.LayersChanged += DocumentOnLayersChanged;
+            e.NewDocument.AnimationDataViewModel.ActiveFrameChanged += ActiveFrameChanged;
             UpdateEnabledState();
         }
     }
+    
+    private void ActiveFrameChanged(int oldFrame, int newFrame)
+    {
+        UpdateActiveFrame(newFrame);
+    }
 
     private void DocumentOnLayersChanged(object? sender, LayersChangedEventArgs e)
     {
@@ -497,7 +504,12 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
             UpdateEnabledState();
         }
     }
-
+    
+    private void UpdateActiveFrame(int newFrame)
+    {
+        ActiveTool?.OnActiveFrameChanged(newFrame);
+    }
+    
     private void UpdateEnabledState()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;

+ 15 - 6
src/PixiEditor/ViewModels/SubViewModels/UndoViewModel.cs

@@ -24,11 +24,13 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedRedo))
             return;
+        
         doc.Operations.Redo();
-        doc.Operations.InvokeCustomAction(() =>
+        doc.Operations.InvokeCustomAction(
+            () =>
         {
             Owner.ToolsSubViewModel.OnPostRedoInlet();
-        });
+        }, false);
     }
 
     /// <summary>
@@ -42,10 +44,11 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         if (doc is null || (!doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() && !doc.HasSavedUndo))
             return;
         doc.Operations.Undo();
-        doc.Operations.InvokeCustomAction(() =>
+        doc.Operations.InvokeCustomAction(
+            () =>
         {
             Owner.ToolsSubViewModel.OnPostUndoInlet();
-        });
+        }, false);
     }
 
     /// <summary>
@@ -59,7 +62,10 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return false;
-        return doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() || doc.HasSavedUndo;
+        
+        IMidChangeUndoableExecutor executor = doc.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        
+        return executor is { CanUndo: true } || doc.HasSavedUndo;
     }
 
     /// <summary>
@@ -73,6 +79,9 @@ internal class UndoViewModel : SubViewModel<ViewModelMain>
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return false;
-        return doc.IsChangeFeatureActive<IMidChangeUndoableExecutor>() || doc.HasSavedRedo;
+        
+        IMidChangeUndoableExecutor executor = doc.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        
+        return executor is { CanRedo: true } || doc.HasSavedRedo;
     }
 }

+ 3 - 2
src/PixiEditor/ViewModels/Tools/ShapeTool.cs

@@ -15,6 +15,7 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
     public override bool IsErasable => true;
     public bool DrawEven { get; protected set; }
     public bool DrawFromCenter { get; protected set; }
+    
 
     public ShapeTool()
     {
@@ -22,12 +23,12 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
         Toolbar = new FillableShapeToolbar();
     }
 
-    public override void OnDeselecting(bool transient)
+
+    protected override void OnDeselecting(bool transient)
     {
         if (!transient)
         {
             ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
         }
     }
-
 }

+ 24 - 2
src/PixiEditor/ViewModels/Tools/ToolViewModel.cs

@@ -151,16 +151,38 @@ internal abstract class ToolViewModel : ObservableObject, IToolHandler
     public virtual void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown) { }
 
     public virtual void UseTool(VecD pos) { }
-    public virtual void OnSelected(bool restoring) { }
+    
+    protected virtual void OnSelected(bool restoring) { }
+
+    public void OnToolSelected(bool restoring)
+    {
+        if (!restoring)
+        {
+            IsActive = true;
+        }
+
+        OnSelected(restoring);
+    }
 
     protected virtual void OnSelectedLayersChanged(IStructureMemberHandler[] layers) { }
 
-    public virtual void OnDeselecting(bool transient)
+    public void OnToolDeselected(bool transient)
+    {
+        if (!transient)
+        {
+            IsActive = false;
+        }
+
+        OnDeselecting(transient);
+    }
+
+    protected virtual void OnDeselecting(bool transient)
     {
     }
     
     public virtual void OnPostUndo() { }
     public virtual void OnPostRedo() { }
+    public virtual void OnActiveFrameChanged(int newFrame) { }
 
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings)
     {

+ 28 - 8
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -54,7 +54,6 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 
     public override void UseTool(VecD pos)
     {
-        //ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseShiftLayerTool();
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
     }
 
@@ -77,7 +76,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         }
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (TransformingSelectedArea)
         {
@@ -87,7 +86,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
     }
 
-    public override void OnDeselecting(bool transient)
+    protected override void OnDeselecting(bool transient)
     {
         var vm = ViewModelMain.Current;
         if (!transient)
@@ -97,17 +96,38 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
         }
     }
 
-    protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
+    public override void OnPostUndo()
     {
-        if (ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel.TransformActive)
+        if (IsActive)
         {
-            return;
+            OnSelected(false);
         }
+    }
 
-        OnDeselecting(false);
-        OnSelected(false);
+    public override void OnPostRedo()
+    {
+        if (IsActive)
+        {
+            OnSelected(false);
+        }
+    }
+
+    protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
+    {
+        UpdateSelection();
     }
     
+    public override void OnActiveFrameChanged(int newFrame)
+    {
+        UpdateSelection();
+    }
+
+    private void UpdateSelection()
+    {
+        OnDeselecting(false);
+        OnToolSelected(false);
+    }
+
     public void KeepOriginalChanged()
     {
         var activeDocument = ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument;

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/MoveViewportToolViewModel.cs

@@ -25,7 +25,7 @@ internal class MoveViewportToolViewModel : ToolViewModel
         Cursor = new Cursor(StandardCursorType.SizeAll);
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         ActionDisplay = new LocalizedString("MOVE_VIEWPORT_ACTION_DISPLAY");
     }

+ 15 - 3
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -76,11 +76,23 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 return;
             }
             
-            var oldSetting = (SizeSettingViewModel)oldToolbar.Settings[0];
-            actualToolSize = (int)oldSetting.Value;
+            var oldSetting = oldToolbar.Settings.FirstOrDefault(x => x.Name == nameof(oldToolbar.ToolSize));
+            if (oldSetting is null)
+            {
+                return;
+            }
+            
+            if(oldSetting.Value is int intValue)
+            {
+                actualToolSize = intValue;
+            }
+            else if(oldSetting.Value is double doubleValue)
+            {
+                actualToolSize = (int)doubleValue;
+            }
         }
 
-        public override void OnDeselecting(bool transient)
+        protected override void OnDeselecting(bool transient)
         {
             if (!PixelPerfectEnabled)
             {

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs

@@ -49,7 +49,7 @@ internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterEllipseTool();
     }
     
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if(restoring) return;
         

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs

@@ -56,7 +56,7 @@ internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterLineTool();
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs

@@ -51,7 +51,7 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseRasterRectangleTool();
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/RotateViewportToolViewModel.cs

@@ -23,7 +23,7 @@ internal class RotateViewportToolViewModel : ToolViewModel
     {
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         ActionDisplay = new LocalizedString("ROTATE_VIEWPORT_ACTION_DISPLAY");
     }

+ 16 - 11
src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs

@@ -35,6 +35,7 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
 
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
+    
 
     public override void UseTool(VecD pos)
     {
@@ -57,23 +58,27 @@ internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
         }
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 
-        var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
-        var layer = document.SelectedStructureMember;
-        if (layer is IVectorLayerHandler vectorLayer &&
-            vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyEllipseData)
-        {
-            ShapeCorners corners = vectorLayer.TransformationCorners;
-            document.TransformViewModel.ShowTransform(
-                DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
+    }
 
-            document.TransformViewModel.CanAlignToPixels = false;
+    public override void OnPostUndo()
+    {
+        if (IsActive)
+        {
+            OnSelected(false);
         }
+    }
 
-        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorEllipseTool();
+    public override void OnPostRedo()
+    {
+        if (IsActive)
+        {
+            OnSelected(false);
+        }
     }
 
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)

+ 16 - 11
src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs

@@ -64,28 +64,33 @@ internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorLineTool();
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 
         var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
-        var layer = document.SelectedStructureMember;
-        if (layer is IVectorLayerHandler vectorLayer)
+        document.Tools.UseVectorLineTool();
+    }
+
+    public override void OnPostUndo()
+    {
+        if (IsActive)
         {
-            if (vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyLineData
-                lineVectorData)
-            {
-                document.LineToolOverlayViewModel.Show(lineVectorData.TransformedStart, lineVectorData.TransformedEnd,
-                    false);
-            }
+            OnToolSelected(false);
         }
+    }
 
-        document.Tools.UseVectorLineTool();
+    public override void OnPostRedo()
+    {
+        if (IsActive)
+        {
+            OnToolSelected(false);
+        }
     }
 
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     {
         OnDeselecting(false);
-        OnSelected(false);
+        OnToolSelected(false);
     }
 }

+ 5 - 5
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -111,7 +111,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         }
     }
     
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 
@@ -119,7 +119,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         isActivated = true;
     }
     
-    public override void OnDeselecting(bool transient)
+    protected override void OnDeselecting(bool transient)
     {
         if (!transient)
         {
@@ -132,7 +132,7 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     {
         if (isActivated)
         {
-            OnSelected(false);
+            OnToolSelected(false);
         }
     }
 
@@ -140,14 +140,14 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     {
         if (isActivated)
         {
-            OnSelected(false);
+            OnToolSelected(false);
         }
     }
 
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     {
         OnDeselecting(false);
-        OnSelected(false);
+        OnToolSelected(false);
     }
 }
 

+ 16 - 13
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -55,29 +55,32 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
         ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
     }
 
-    public override void OnSelected(bool restoring)
+    protected override void OnSelected(bool restoring)
     {
         if (restoring) return;
 
-        var document = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument;
-        var layer = document.SelectedStructureMember;
-        if (layer is IVectorLayerHandler vectorLayer &&
-            vectorLayer.GetShapeData(document.AnimationDataViewModel.ActiveFrameTime) is IReadOnlyRectangleData)
+        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
+    }
+
+    public override void OnPostUndo()
+    {
+        if (IsActive)
         {
-            ShapeCorners corners = vectorLayer.TransformationCorners;
-            var transformVm = ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument.TransformViewModel;
-            transformVm.ShowTransform(
-                DocumentTransformMode.Scale_Rotate_Shear_NoPerspective, false, corners, false);
-            
-            transformVm.CanAlignToPixels = false;
+            OnToolSelected(false);
         }
+    }
 
-        ViewModelMain.Current?.DocumentManagerSubViewModel.ActiveDocument?.Tools.UseVectorRectangleTool();
+    public override void OnPostRedo()
+    {
+        if (IsActive)
+        {
+            OnToolSelected(false);
+        }
     }
 
     protected override void OnSelectedLayersChanged(IStructureMemberHandler[] layers)
     {
         OnDeselecting(false);
-        OnSelected(false);
+        OnToolSelected(false);
     }
 }

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

@@ -166,7 +166,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         set { SetValue(FpsProperty, value); }
     }
 
-    public int EndFrame => KeyFrames?.FrameCount > 0 ? KeyFrames.FrameCount : DefaultEndFrame;
+    public int EndFrame => KeyFrames?.FrameCount > 0 ? KeyFrames.FrameCount - 1 : DefaultEndFrame;
 
     public ICommand DraggedKeyFrameCommand { get; }
     public ICommand ReleasedKeyFrameCommand { get; }

+ 12 - 0
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -237,6 +237,11 @@ internal class ViewportOverlays
             Source = Viewport, Path = "Document.LineToolOverlayViewModel.IsSizeBoxEnabled", Mode = BindingMode.TwoWay
         };
         
+        Binding addToUndoCommandBinding = new()
+        {
+            Source = Viewport, Path = "Document.LineToolOverlayViewModel.AddToUndoCommand", Mode = BindingMode.OneWay
+        };
+        
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         lineToolOverlay.Bind(LineToolOverlay.SnappingControllerProperty, snappingBinding);
         lineToolOverlay.Bind(LineToolOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -247,6 +252,7 @@ internal class ViewportOverlays
         lineToolOverlay.Bind(LineToolOverlay.ShowHandlesProperty, showHandlesBinding);
         lineToolOverlay.Bind(LineToolOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
         lineToolOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
+        lineToolOverlay.Bind(LineToolOverlay.AddToUndoCommandProperty, addToUndoCommandBinding);
     }
 
     private void BindTransformOverlay()
@@ -336,6 +342,11 @@ internal class ViewportOverlays
         {
             Source = Viewport, Path = "Document.TransformViewModel.CanAlignToPixels", Mode = BindingMode.OneWay
         };
+        
+        Binding lockShearBinding = new()
+        {
+            Source = Viewport, Path = "Document.TransformViewModel.LockShear", Mode = BindingMode.OneWay
+        };
 
         transformOverlay.Bind(Visual.IsVisibleProperty, isVisibleBinding);
         transformOverlay.Bind(TransformOverlay.ActionCompletedProperty, actionCompletedBinding);
@@ -354,6 +365,7 @@ internal class ViewportOverlays
         transformOverlay.Bind(TransformOverlay.IsSizeBoxEnabledProperty, isSizeBoxEnabledBinding);
         transformOverlay.Bind(TransformOverlay.ScaleFromCenterProperty, scaleFromCenterBinding);
         transformOverlay.Bind(TransformOverlay.CanAlignToPixelsProperty, canAlignToPixelsBinding);
+        transformOverlay.Bind(TransformOverlay.LockShearProperty, lockShearBinding);
     }
     
     private void BindVectorPathOverlay()

+ 12 - 0
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -75,6 +75,15 @@ internal class LineToolOverlay : Overlay
         set => SetValue(IsSizeBoxEnabledProperty, value);
     }
 
+    public static readonly StyledProperty<ICommand> AddToUndoCommandProperty = AvaloniaProperty.Register<LineToolOverlay, ICommand>(
+        nameof(AddToUndoCommand));
+
+    public ICommand AddToUndoCommand
+    {
+        get => GetValue(AddToUndoCommandProperty);
+        set => SetValue(AddToUndoCommandProperty, value);
+    }
+
     static LineToolOverlay()
     {
         LineStartProperty.Changed.Subscribe(RenderAffectingPropertyChanged);
@@ -131,6 +140,7 @@ internal class LineToolOverlay : Overlay
         moveHandle.StrokePaint = blackPaint;
         moveHandle.OnPress += OnHandlePress;
         moveHandle.OnDrag += MoveHandleOnDrag;
+        moveHandle.OnRelease += OnHandleRelease;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         moveHandle.OnHover += (handle, _) => Refresh();
         moveHandle.OnRelease += OnHandleRelease;
@@ -162,6 +172,8 @@ internal class LineToolOverlay : Overlay
 
         isDraggingHandle = false;
         IsSizeBoxEnabled = false;
+        
+        AddToUndoCommand.Execute((LineStart, LineEnd));
     }
 
     protected override void ZoomChanged(double newZoom)

+ 16 - 3
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -171,6 +171,15 @@ internal class TransformOverlay : Overlay
         set => SetValue(CanAlignToPixelsProperty, value);
     }
 
+    public static readonly StyledProperty<bool> LockShearProperty = AvaloniaProperty.Register<TransformOverlay, bool>(
+        nameof(LockShear));
+
+    public bool LockShear
+    {
+        get => GetValue(LockShearProperty);
+        set => SetValue(LockShearProperty, value);
+    }
+    
     static TransformOverlay()
     {
         AffectsRender<TransformOverlay>(CornersProperty, ZoomScaleProperty, SideFreedomProperty, CornerFreedomProperty,
@@ -179,8 +188,6 @@ internal class TransformOverlay : Overlay
         RequestCornersExecutorProperty.Changed.Subscribe(OnCornersExecutorChanged);
     }
 
-    private const int anchorSizeMultiplierForRotation = 15;
-
     private bool isMoving = false;
     private VecD mousePosOnStartMove = new();
     private VecD originOnStartMove = new();
@@ -689,6 +696,12 @@ internal class TransformOverlay : Overlay
 
     private bool CanShear(VecD mousePos, out Anchor side)
     {
+        if(LockShear)
+        {
+            side = default;
+            return false;
+        }
+        
         double distance = 20 / ZoomScale;
         var sides = new[] { Anchor.Top, Anchor.Bottom, Anchor.Left, Anchor.Right };
 
@@ -702,7 +715,7 @@ internal class TransformOverlay : Overlay
         side = sides.FirstOrDefault(side => VecD.Distance(TransformHelper.GetAnchorPosition(Corners, side), mousePos)
                                             < distance);
 
-        return side != default;
+        return side != default && !Corners.IsPointInside(mousePos);
     }
 
     private void StopMoving()

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit 4176e1eb211e5e6f8932671e82b2813c737a1a56
+Subproject commit 345e15022ad031e97907bdeba8a91cc4c7fd2a8d