Explorar o código

Merge branch 'master' into linux
sync

flabbet hai 5 meses
pai
achega
736e68bccb
Modificáronse 100 ficheiros con 2764 adicións e 470 borrados
  1. 2 2
      README.md
  2. 1 1
      src/PixiDocks
  3. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyShapeVectorData.cs
  4. 13 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyTextData.cs
  5. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MergeNode.cs
  6. 32 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  7. 17 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  8. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  9. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  10. 21 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  11. 231 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs
  12. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  13. 2 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  14. 71 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextNode.cs
  15. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextOnPathNode.cs
  16. 13 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  17. 7 4
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs
  18. 153 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  19. 68 51
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs
  20. 18 11
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  21. 87 7
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  22. 11 1
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  23. 9 2
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  24. 21 14
      src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs
  25. 25 5
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  26. 12 3
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs
  27. 99 13
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  28. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs
  29. 46 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/SetLowDpiRendering_Change.cs
  30. 73 0
      src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs
  31. 11 10
      src/PixiEditor.ChangeableDocument/Changes/Vectors/SetShapeGeometry_UpdateableChange.cs
  32. 1 0
      src/PixiEditor.Extensions/UI/Overlays/OverlayPointerArgs.cs
  33. 4 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  34. 6 1
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  35. 51 0
      src/PixiEditor.SVG/Elements/SvgText.cs
  36. 8 0
      src/PixiEditor.SVG/Enums/SvgFontStyle.cs
  37. 9 0
      src/PixiEditor.SVG/Enums/SvgFontWeight.cs
  38. 1 0
      src/PixiEditor.SVG/Features/IFillable.cs
  39. 8 0
      src/PixiEditor.SVG/Features/IOpacity.cs
  40. 154 32
      src/PixiEditor.SVG/StyleContext.cs
  41. 16 13
      src/PixiEditor.SVG/SvgDocument.cs
  42. 24 23
      src/PixiEditor.SVG/SvgElement.cs
  43. 14 12
      src/PixiEditor.SVG/SvgParser.cs
  44. 20 3
      src/PixiEditor.SVG/SvgProperty.cs
  45. 35 15
      src/PixiEditor.SVG/Units/SvgNumericUnit.cs
  46. 142 0
      src/PixiEditor.SVG/Units/SvgNumericUnits.cs
  47. 73 0
      src/PixiEditor.SVG/Units/SvgStyleUnit.cs
  48. 98 4
      src/PixiEditor.SVG/Units/SvgTransformUnit.cs
  49. 8 2
      src/PixiEditor.UI.Common/Accents/Base.axaml
  50. BIN=BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  51. 49 29
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  52. 16 1
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  53. 0 1
      src/PixiEditor/App.axaml
  54. 26 3
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  55. 24 3
      src/PixiEditor/Data/Localization/Languages/en.json
  56. 18 0
      src/PixiEditor/Helpers/Converters/FontFamilyNameToAvaloniaFontFamily.cs
  57. 19 9
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  58. 4 2
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  59. 4 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  60. 2 2
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  61. 8 0
      src/PixiEditor/Helpers/ThemeResources.cs
  62. 1 1
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  63. 18 3
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  64. 44 0
      src/PixiEditor/Models/Controllers/FontLibrary.cs
  65. 3 3
      src/PixiEditor/Models/Controllers/InputDevice/MouseInputFilter.cs
  66. 10 6
      src/PixiEditor/Models/Controllers/InputDevice/MouseOnCanvasEventArgs.cs
  67. 3 3
      src/PixiEditor/Models/Controllers/ShortcutController.cs
  68. 68 51
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  69. 34 2
      src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs
  70. 2 0
      src/PixiEditor/Models/DocumentModels/Public/DocumentEventsModule.cs
  71. 17 6
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  72. 14 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  73. 6 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs
  74. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  75. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/IQuickToolSwitchable.cs
  76. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITextOverlayEvents.cs
  77. 9 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformDraggedEvent.cs
  78. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformStoppedEvent.cs
  79. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformableExecutor.cs
  80. 4 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs
  81. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/MagicWandToolExecutor.cs
  82. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PasteImageExecutor.cs
  83. 0 74
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs
  84. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/SimpleShapeToolExecutor.cs
  85. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformReferenceLayerExecutor.cs
  86. 180 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  87. 10 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs
  88. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs
  89. 268 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  90. 2 0
      src/PixiEditor/Models/Files/IoFileType.cs
  91. 18 0
      src/PixiEditor/Models/Files/OtfFileType.cs
  92. 2 0
      src/PixiEditor/Models/Files/SvgFileType.cs
  93. 18 0
      src/PixiEditor/Models/Files/TtfFileType.cs
  94. 6 2
      src/PixiEditor/Models/Handlers/IDocument.cs
  95. 1 0
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  96. 1 0
      src/PixiEditor/Models/Handlers/ILayerHandler.cs
  97. 16 0
      src/PixiEditor/Models/Handlers/ITextOverlayHandler.cs
  98. 6 3
      src/PixiEditor/Models/Handlers/IToolHandler.cs
  99. 3 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  100. 15 0
      src/PixiEditor/Models/Handlers/Toolbars/ITextToolbar.cs

+ 2 - 2
README.md

@@ -12,9 +12,9 @@
 
 
 ### Check out our website [pixieditor.net](https://pixieditor.net) and [PixiEditor Forum](https://forum.pixieditor.net/)
 ### Check out our website [pixieditor.net](https://pixieditor.net) and [PixiEditor Forum](https://forum.pixieditor.net/)
 
 
-# Contributions temporarily freezed!
+# Feature Contributions temporarily freezed!
 
 
-PixiEditor is undergoing massive changes, master branch is unstable. We will not accept any contributions at the moment, until version 2.0 comes out.
+PixiEditor is undergoing massive changes, master branch is unstable. We will not accept any feature contributions at the moment, until version 2.0 comes out. Feel free to fix bugs though. But before you do, let us know on [Discord](https://discord.gg/qSRMYmq), since we already might've fixed them.
 
 
 ## About PixiEditor
 ## About PixiEditor
 
 

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit b83ba013241e6d6b6d280eea8836e49c5c6b9f81
+Subproject commit 47107d7dc284e04ed92e4c470a6ed2f972e5d9cd

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

@@ -1,5 +1,6 @@
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -14,4 +15,5 @@ public interface IReadOnlyShapeVectorData
     public RectD GeometryAABB { get; }
     public RectD GeometryAABB { get; }
     public RectD TransformedAABB { get; }
     public RectD TransformedAABB { get; }
     public ShapeCorners TransformationCorners { get; }
     public ShapeCorners TransformationCorners { get; }
+    public VectorPath ToPath();
 }
 }

+ 13 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyTextData.cs

@@ -0,0 +1,13 @@
+using Drawie.Backend.Core.Text;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+public interface IReadOnlyTextData
+{
+    public string Text { get; }
+    public VecD Position { get; }
+    public Font ConstructFont();
+    public double Spacing { get; }
+    public double MaxWidth { get; }
+}

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

@@ -54,12 +54,12 @@ public class MergeNode : RenderNode
         if (Bottom.Value != null && Top.Value != null)
         if (Bottom.Value != null && Top.Value != null)
         {
         {
             int saved = target.Canvas.SaveLayer();
             int saved = target.Canvas.SaveLayer();
-            Bottom.Value.Paint(context, target);
+            Bottom.Value?.Paint(context, target);
 
 
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             paint.BlendMode = RenderContext.GetDrawingBlendMode(BlendMode.Value);
             target.Canvas.SaveLayer(paint);
             target.Canvas.SaveLayer(paint);
             
             
-            Top.Value.Paint(context, target);
+            Top.Value?.Paint(context, target);
             target.Canvas.RestoreToCount(saved);
             target.Canvas.RestoreToCount(saved);
             return;
             return;
         }
         }

+ 32 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -44,7 +44,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
 
 
     protected internal bool IsDisposed => _isDisposed;
     protected internal bool IsDisposed => _isDisposed;
     private bool _isDisposed;
     private bool _isDisposed;
-    
+
     public void Execute(RenderContext context)
     public void Execute(RenderContext context)
     {
     {
         ExecuteInternal(context);
         ExecuteInternal(context);
@@ -112,6 +112,36 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
         }
     }
     }
 
 
+    public void TraverseBackwards(Func<IReadOnlyNode, IReadOnlyNode?, IInputProperty, bool> action)
+    {
+        var visited = new HashSet<IReadOnlyNode>();
+        var queueNodes = new Queue<(IReadOnlyNode, IReadOnlyNode, IInputProperty)>();
+        queueNodes.Enqueue((this, null, null));
+
+        while (queueNodes.Count > 0)
+        {
+            var node = queueNodes.Dequeue();
+
+            if (!visited.Add((node.Item1)))
+            {
+                continue;
+            }
+
+            if (!action(node.Item1, node.Item2, node.Item3))
+            {
+                return;
+            }
+
+            foreach (var inputProperty in node.Item1.InputProperties)
+            {
+                if (inputProperty.Connection != null)
+                {
+                    queueNodes.Enqueue((inputProperty.Connection.Node, node.Item1, inputProperty));
+                }
+            }
+        }
+    }
+
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
     public void TraverseBackwards(Func<IReadOnlyNode, bool> action)
     {
     {
         var visited = new HashSet<IReadOnlyNode>();
         var visited = new HashSet<IReadOnlyNode>();
@@ -461,7 +491,7 @@ public abstract class Node : IReadOnlyNode, IDisposable
     {
     {
         return new None();
         return new None();
     }
     }
-    
+
     private void InvokeConnectionsChanged()
     private void InvokeConnectionsChanged()
     {
     {
         ConnectionsChanged?.Invoke();
         ConnectionsChanged?.Invoke();

+ 17 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 
@@ -67,6 +68,22 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
         return textureCache.RequestTexture(id, size, processingCs, clear);
         return textureCache.RequestTexture(id, size, processingCs, clear);
     }
     }
 
 
+    public override void SerializeAdditionalData(Dictionary<string, object> additionalData)
+    {
+        base.SerializeAdditionalData(additionalData);
+        additionalData["AllowHighDpiRendering"] = AllowHighDpiRendering;
+    }
+
+    internal override OneOf<None, IChangeInfo, List<IChangeInfo>> DeserializeAdditionalData(IReadOnlyDocument target, IReadOnlyDictionary<string, object> data)
+    {
+        base.DeserializeAdditionalData(target, data);
+
+        if(data.TryGetValue("AllowHighDpiRendering", out var value))
+            AllowHighDpiRendering = (bool)value;
+
+        return new None();
+    }
+
     public override void Dispose()
     public override void Dispose()
     {
     {
         base.Dispose();
         base.Dispose();

+ 10 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
 {
     public VectorPath Path { get; set; }
     public VectorPath Path { get; set; }
-    public override RectD GeometryAABB => Path.TightBounds;
+    public override RectD GeometryAABB => Path?.TightBounds ?? RectD.Empty;
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
 
     public override ShapeCorners TransformationCorners =>
     public override ShapeCorners TransformationCorners =>
@@ -24,6 +24,10 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public PathVectorData(VectorPath path)
     public PathVectorData(VectorPath path)
     {
     {
         Path = path;
         Path = path;
+        if (path == null)
+        {
+            Path = new VectorPath();
+        }
     }
     }
 
 
     public override void RasterizeGeometry(Canvas canvas)
     public override void RasterizeGeometry(Canvas canvas)
@@ -38,6 +42,11 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
 
     private void Rasterize(Canvas canvas, bool applyTransform)
     private void Rasterize(Canvas canvas, bool applyTransform)
     {
     {
+        if(Path == null)
+        {
+            return;
+        }
+
         int num = 0;
         int num = 0;
         if (applyTransform)
         if (applyTransform)
         {
         {

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

@@ -3,6 +3,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using Drawie.Numerics.Helpers;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 
@@ -47,8 +48,7 @@ public class PointsVectorData : ShapeVectorData
             canvas.SetMatrix(final);
             canvas.SetMatrix(final);
         }
         }
 
 
-        canvas.DrawPoints(PointMode.Points, Points.Select(p => new VecF((float)p.X, (float)p.Y)).ToArray(),
-            paint);
+        canvas.DrawPoints(PointMode.Points, Points.ToVecFArray(), paint);
 
 
         if (applyTransform)
         if (applyTransform)
         {
         {

+ 21 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -11,18 +11,31 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 
 public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVectorData
 public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVectorData
 {
 {
-    public Matrix3X3 TransformationMatrix { get; set; } = Matrix3X3.Identity; 
-    
+    private float strokeWidth = 0;
+
+    public Matrix3X3 TransformationMatrix { get; set; } = Matrix3X3.Identity;
+
     public Color StrokeColor { get; set; } = Colors.White;
     public Color StrokeColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
-    public float StrokeWidth { get; set; } = 1;
+
+    public float StrokeWidth
+    {
+        get => strokeWidth;
+        set
+        {
+            strokeWidth = value;
+            OnStrokeWidthChanged();
+        }
+    }
+    
     public bool Fill { get; set; } = true;
     public bool Fill { get; set; } = true;
-    public abstract RectD GeometryAABB { get; } 
+
+    public abstract RectD GeometryAABB { get; }
     public abstract RectD VisualAABB { get; }
     public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
     public RectD TransformedVisualAABB => new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix).AABBBounds;
-    public abstract ShapeCorners TransformationCorners { get; } 
-    
+    public abstract ShapeCorners TransformationCorners { get; }
+
     protected void ApplyTransformTo(Canvas canvas)
     protected void ApplyTransformTo(Canvas canvas)
     {
     {
         Matrix3X3 canvasMatrix = canvas.TotalMatrix;
         Matrix3X3 canvasMatrix = canvas.TotalMatrix;
@@ -46,6 +59,8 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     }
     }
 
 
     protected virtual void AdjustCopy(ShapeVectorData copy) { }
     protected virtual void AdjustCopy(ShapeVectorData copy) { }
+    
+    protected virtual void OnStrokeWidthChanged() { }
 
 
     public override int GetHashCode()
     public override int GetHashCode()
     {
     {

+ 231 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs

@@ -0,0 +1,231 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+
+public class TextVectorData : ShapeVectorData, IReadOnlyTextData
+{
+    private string text;
+    private Font font = Font.CreateDefault();
+    private double? spacing = null;
+    private double strokeWidth = 1;
+    private VectorPath? path;
+
+    public string Text
+    {
+        get => text;
+        set
+        {
+            text = value;
+            richText = new RichText(value) { Spacing = Spacing, MaxWidth = MaxWidth, StrokeWidth = StrokeWidth };
+
+            lastBounds = richText.MeasureBounds(Font);
+        }
+    }
+
+    public VecD Position { get; set; }
+
+
+    public double MaxWidth { get; set; } = double.MaxValue;
+
+    public Font Font
+    {
+        get => font;
+        set
+        {
+            if (value != null)
+            {
+                value.Changed -= FontChanged;
+            }
+
+            font = value;
+            if (value != null)
+            {
+                value.Changed += FontChanged;
+            }
+
+            lastBounds = richText.MeasureBounds(value);
+        }
+    }
+
+    private void FontChanged()
+    {
+        if (richText == null)
+        {
+            return;
+        }
+
+        lastBounds = richText.MeasureBounds(Font);
+    }
+
+    public Font ConstructFont()
+    {
+        Font newFont = Font.FromFontFamily(Font.Family);
+        newFont.Size = Font.Size;
+        newFont.Edging = Font.Edging;
+        newFont.SubPixel = Font.SubPixel;
+        newFont.Bold = Font.Bold;
+        newFont.Italic = Font.Italic;
+
+        return newFont;
+    }
+
+    double IReadOnlyTextData.Spacing => Spacing ?? Font.Size;
+
+    public double? Spacing
+    {
+        get => spacing;
+        set
+        {
+            spacing = value;
+            richText.Spacing = value;
+            lastBounds = richText.MeasureBounds(Font);
+        }
+    }
+    
+    public bool AntiAlias { get; set; } = true;
+
+    protected override void OnStrokeWidthChanged()
+    {
+        if(richText == null)
+        {
+            return;
+        }
+
+        richText.StrokeWidth = StrokeWidth;
+        lastBounds = richText.MeasureBounds(Font);
+    }
+
+    public override RectD GeometryAABB
+    {
+        get
+        {
+            return lastBounds.Offset(Position);
+        }
+    }
+
+    public override ShapeCorners TransformationCorners =>
+        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
+
+    public override RectD VisualAABB => GeometryAABB;
+
+    public VectorPath? Path
+    {
+        get => path;
+        set
+        {
+            path = value;
+            // TODO: properly calculate bounds
+            //lastBounds = richText.MeasureBounds(Font);
+        }
+    }
+
+    public FontFamilyName? MissingFontFamily { get; set; }
+    public string MissingFontText { get; set; }
+
+    private RichText richText;
+    private RectD lastBounds;
+    private double _spacing;
+
+    public TextVectorData()
+    {
+
+    }
+
+    public TextVectorData(string text)
+    {
+        Text = text;
+    }
+
+
+    public override VectorPath ToPath()
+    {
+        var path = richText.ToPath(Font);
+        path.Offset(Position);
+
+        return path;
+    }
+
+    public override void RasterizeGeometry(Canvas canvas)
+    {
+        Rasterize(canvas, false);
+    }
+
+    public override void RasterizeTransformed(Canvas canvas)
+    {
+        Rasterize(canvas, true);
+    }
+
+    private void Rasterize(Canvas canvas, bool applyTransform)
+    {
+        int num = 0;
+        if (applyTransform)
+        {
+            num = canvas.Save();
+            ApplyTransformTo(canvas);
+        }
+
+        using Paint paint = new Paint() { IsAntiAliased = AntiAlias };
+
+        richText.Fill = Fill;
+        richText.FillColor = FillColor;
+        richText.StrokeColor = StrokeColor;
+        richText.StrokeWidth = StrokeWidth;
+        richText.Spacing = Spacing;
+
+        if (MissingFontFamily != null)
+        {
+            paint.Color = Fill ? FillColor : StrokeColor;
+            canvas.DrawText($"{MissingFontText}: " + MissingFontFamily.Value.Name, Position, Font, paint);
+        }
+        else
+        {
+            PaintText(canvas, paint);
+        }
+
+        if (applyTransform)
+        {
+            canvas.RestoreToCount(num);
+        }
+    }
+
+    private void PaintText(Canvas canvas, Paint paint)
+    {
+        richText.Paint(canvas, Position, Font, paint, Path);
+    }
+
+    public override bool IsValid()
+    {
+        return !string.IsNullOrEmpty(Text);
+    }
+
+    public override int GetCacheHash()
+    {
+        return HashCode.Combine(Text, Position, Font, StrokeColor, FillColor, StrokeWidth, TransformationMatrix);
+    }
+
+    protected override void AdjustCopy(ShapeVectorData copy)
+    {
+        if (copy is TextVectorData textData)
+        {
+            textData.Font = Font.FromFontFamily(Font.Family);
+            textData.Font.Size = Font.Size;
+            textData.Font.Edging = Font.Edging;
+            textData.Font.SubPixel = Font.SubPixel;
+            textData.Font.Bold = Font.Bold;
+            textData.Font.Italic = Font.Italic;
+
+            textData.lastBounds = lastBounds;
+        }
+    }
+
+    public override int CalculateHash()
+    {
+        return GetCacheHash();
+    }
+}

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -12,11 +12,12 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 public class RasterizeShapeNode : RenderNode
 public class RasterizeShapeNode : RenderNode
 {
 {
     public InputProperty<ShapeVectorData> Data { get; }
     public InputProperty<ShapeVectorData> Data { get; }
-
+    public InputProperty<bool> HighDpiRendering { get; }
 
 
     public RasterizeShapeNode()
     public RasterizeShapeNode()
     {
     {
         Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
         Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
+        HighDpiRendering = CreateInput<bool>("High DPI Rendering", "HIGH_DPI_RENDERING", true);
     }
     }
 
 
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
@@ -25,11 +26,14 @@ public class RasterizeShapeNode : RenderNode
 
 
         if (shape == null || !shape.IsValid())
         if (shape == null || !shape.IsValid())
             return;
             return;
-        
+
+        AllowHighDpiRendering = HighDpiRendering.Value;
+
         shape.RasterizeTransformed(surface.Canvas);
         shape.RasterizeTransformed(surface.Canvas);
     }
     }
 
 
     public override Node CreateCopy() => new RasterizeShapeNode();
     public override Node CreateCopy() => new RasterizeShapeNode();
+
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
     {
         return Data?.Value?.TransformedAABB;
         return Data?.Value?.TransformedAABB;

+ 2 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -26,6 +26,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
         var data = Input.Value;
         var data = Input.Value;
 
 
         var distance = MinDistance.Value;
         var distance = MinDistance.Value;
+        var minDistanceSquared = distance * distance;
 
 
         if (distance == 0 || data == null || data.Points == null)
         if (distance == 0 || data == null || data.Points == null)
         {
         {
@@ -34,9 +35,6 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 
 
         var availablePoints = data.Points.Distinct().ToList();
         var availablePoints = data.Points.Distinct().ToList();
         List<VecD> newPoints = new List<VecD>();
         List<VecD> newPoints = new List<VecD>();
-        
-        var minDistance = MinDistance.Value;
-        var documentSize = context.DocumentSize;
 
 
         var random = new Random(Seed.Value);
         var random = new Random(Seed.Value);
         while (availablePoints.Count > 1)
         while (availablePoints.Count > 1)
@@ -55,7 +53,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
             continue;
             continue;
 
 
             bool InRange(VecD other) =>
             bool InRange(VecD other) =>
-                (other - point).Length <= minDistance;
+                (other - point).LengthSquared <= minDistanceSquared;
         }
         }
 
 
         if (availablePoints.Count == 1)
         if (availablePoints.Count == 1)

+ 71 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextNode.cs

@@ -0,0 +1,71 @@
+using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("Text")]
+public class TextNode : ShapeNode<TextVectorData>
+{
+    public InputProperty<string> Text { get; }
+    public InputProperty<VecD> TextPosition { get; }
+    public InputProperty<FontFamilyName> FontFamily { get; }
+    public InputProperty<double> FontSize { get; }
+
+    private string lastText = "";
+    private VecD lastPosition = new VecD();
+    private FontFamilyName lastFontFamily = new FontFamilyName();
+    private double lastFontSize = 12d;
+    private VectorPath? lastPath;
+
+    private TextVectorData? cachedData;
+    public TextNode()
+    {
+        Text = CreateInput("Text", "TEXT_LABEL", "");
+        TextPosition = CreateInput("Position", "POSITION", new VecD());
+        FontFamily = CreateInput("FontFamily", "FONT_LABEL", new FontFamilyName());
+        FontSize = CreateInput("FontSize", "FONT_SIZE_LABEL", 12d);
+    }
+    
+    protected override TextVectorData? GetShapeData(RenderContext context)
+    {
+        string text = Text.Value;
+        VecD position = TextPosition.Value;
+        FontFamilyName fontFamily = FontFamily.Value;
+        double fontSize = FontSize.Value;
+
+        if (text == lastText && position == lastPosition && fontFamily.Equals(lastFontFamily) && fontSize == lastFontSize)
+        {
+            return cachedData;
+        }
+        
+        lastText = text;
+        lastPosition = position;
+        lastFontFamily = fontFamily;
+        lastFontSize = fontSize;
+
+        Font font = Font.FromFontFamily(fontFamily);
+        if(font == null)
+        {
+            font = Font.CreateDefault();
+        }
+        
+        font.Size = fontSize;
+        
+        cachedData = new TextVectorData()
+        {
+            Text = text,
+            Position = position,
+            Font = font,
+        };
+        
+        return cachedData;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TextNode();
+    }
+}

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

@@ -0,0 +1,51 @@
+using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("TextOnPath")]
+public class TextOnPathNode : Node
+{
+    public InputProperty<TextVectorData> TextData { get; }
+    public InputProperty<ShapeVectorData> PathData { get; }
+
+    public OutputProperty<TextVectorData> Output { get; }
+
+    private VectorPath lastPath;
+
+    public TextOnPathNode()
+    {
+        TextData = CreateInput<TextVectorData>("Text", "TEXT_LABEL", null);
+        PathData = CreateInput<ShapeVectorData>("Path", "SHAPE_LABEL", null);
+
+        Output = CreateOutput<TextVectorData>("Output", "TEXT_LABEL", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var textData = TextData.Value;
+        var pathData = PathData.Value;
+
+        if (textData == null || pathData == null || !textData.IsValid() || !pathData.IsValid())
+        {
+            Output.Value = null;
+            return;
+        }
+
+        var cloned = (TextVectorData)textData.Clone();
+
+        lastPath?.Dispose();
+        lastPath = pathData.ToPath();
+        lastPath.Transform(pathData.TransformationMatrix);
+
+        cloned.Path = lastPath;
+
+        Output.Value = cloned;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TextOnPathNode();
+    }
+}

+ 13 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -17,6 +17,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 [NodeInfo("VectorLayer")]
 [NodeInfo("VectorLayer")]
 public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable
 public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable
 {
 {
+    public OutputProperty<ShapeVectorData> Shape { get; }
     public Matrix3X3 TransformationMatrix
     public Matrix3X3 TransformationMatrix
     {
     {
         get => ShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
         get => ShapeData?.TransformationMatrix ?? Matrix3X3.Identity;
@@ -31,7 +32,11 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         }
         }
     }
     }
 
 
-    public ShapeVectorData? ShapeData { get; set; }
+    public ShapeVectorData? ShapeData
+    {
+        get => Shape.Value;
+        set => Shape.Value = value;
+    }
     IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
     IReadOnlyShapeVectorData IReadOnlyVectorNode.ShapeData => ShapeData;
 
 
 
 
@@ -43,6 +48,7 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
     public VectorLayerNode()
     public VectorLayerNode()
     {
     {
         AllowHighDpiRendering = true;
         AllowHighDpiRendering = true;
+        Shape = CreateOutput<ShapeVectorData>("Shape", "SHAPE", null);
     }
     }
     
     
     protected override VecI GetTargetSize(RenderContext ctx)
     protected override VecI GetTargetSize(RenderContext ctx)
@@ -167,6 +173,11 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
 
 
     public override Node CreateCopy()
     public override Node CreateCopy()
     {
     {
-        return new VectorLayerNode() { ShapeData = (ShapeVectorData?)ShapeData?.Clone(), ClipToPreviousMember = this.ClipToPreviousMember };
+        return new VectorLayerNode()
+        {
+            ShapeData = (ShapeVectorData?)ShapeData?.Clone(),
+            ClipToPreviousMember = this.ClipToPreviousMember,
+            AllowHighDpiRendering = this.AllowHighDpiRendering
+        };
     }
     }
 }
 }

+ 7 - 4
src/PixiEditor.ChangeableDocument/Changes/Drawing/ClearSelectedArea_Change.cs

@@ -1,25 +1,28 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 internal class ClearSelectedArea_Change : Change
 internal class ClearSelectedArea_Change : Change
 {
 {
+    private VectorPath clearArea;
     private readonly Guid memberGuid;
     private readonly Guid memberGuid;
     private readonly bool drawOnMask;
     private readonly bool drawOnMask;
     private CommittedChunkStorage? savedChunks;
     private CommittedChunkStorage? savedChunks;
     private int frame;
     private int frame;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
-    public ClearSelectedArea_Change(Guid memberGuid, bool drawOnMask, int frame)
+    public ClearSelectedArea_Change(Guid memberGuid, VectorPath clearArea, bool drawOnMask, int frame)
     {
     {
         this.memberGuid = memberGuid;
         this.memberGuid = memberGuid;
         this.drawOnMask = drawOnMask;
         this.drawOnMask = drawOnMask;
+        this.clearArea = clearArea;
         this.frame = frame;
         this.frame = frame;
     }
     }
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
-        return !target.Selection.SelectionPath.IsEmpty && DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
+        return clearArea is { IsEmpty: false } && DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
@@ -29,10 +32,10 @@ internal class ClearSelectedArea_Change : Change
 
 
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
 
 
-        RectD bounds = target.Selection.SelectionPath.Bounds;
+        RectD bounds = clearArea.Bounds;
         RectI intBounds = (RectI)bounds.Intersect(new RectD(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
         RectI intBounds = (RectI)bounds.Intersect(new RectD(0, 0, target.Size.X, target.Size.Y)).RoundOutwards();
 
 
-        image.EnqueueClearPath(target.Selection.SelectionPath, intBounds);
+        image.EnqueueClearPath(clearArea, intBounds);
         var affArea = image.FindAffectedArea();
         var affArea = image.FindAffectedArea();
         savedChunks = new(image, affArea.Chunks);
         savedChunks = new(image, affArea.Chunks);
         image.CommitChanges();
         image.CommitChanges();

+ 153 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs

@@ -0,0 +1,153 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
+internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChange
+{
+    private List<Guid> layerGuids;
+    private VecD delta;
+    private Dictionary<Guid, ShapeVectorData> originalShapes;
+
+    private int frame;
+
+    [GenerateUpdateableChangeActions]
+    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
+    {
+        this.delta = delta;
+        this.layerGuids = layerGuids;
+        this.frame = frame;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (layerGuids.Count == 0)
+        {
+            return false;
+        }
+
+        layerGuids = target.ExtractLayers(layerGuids);
+
+        foreach (var layer in layerGuids)
+        {
+            if (!target.HasMember(layer)) return false;
+        }
+
+        originalShapes = new Dictionary<Guid, ShapeVectorData>();
+
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is VectorLayerNode transformableObject)
+            {
+                originalShapes[layerGuid] = transformableObject.ShapeData;
+                transformableObject.ShapeData = null;
+            }
+        }
+
+        return true;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(VecD delta)
+    {
+        this.delta = delta;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is ImageLayerNode)
+            {
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
+            }
+            else if (layer is VectorLayerNode vectorLayer)
+            {
+                StrokeJoin join = StrokeJoin.Miter;
+                StrokeCap cap = StrokeCap.Butt;
+                
+                (vectorLayer.ShapeData as PathVectorData)?.Path.Dispose();
+
+                var originalShape = originalShapes[layerGuid];
+
+                var path = originalShape.ToPath();
+
+                if (originalShape is PathVectorData shape)
+                {
+                    join = shape.StrokeLineJoin;
+                    cap = shape.StrokeLineCap;
+                }
+
+                VecD mappedDelta = originalShape.TransformationMatrix.Invert().MapVector((float)delta.X, (float)delta.Y);
+                
+                var finalMatrix = Matrix3X3.CreateTranslation((float)mappedDelta.X, (float)mappedDelta.Y);
+
+                path.AddPath(path, finalMatrix, AddPathMode.Append);
+
+                var newShapeData = new PathVectorData(path)
+                {
+                    StrokeWidth = originalShape.StrokeWidth,
+                    StrokeColor = originalShape.StrokeColor,
+                    FillColor = originalShape.FillColor,
+                    Fill = originalShape.Fill,
+                    TransformationMatrix = originalShape.TransformationMatrix,
+                    StrokeLineJoin = join,
+                    StrokeLineCap = cap
+                };
+                
+                vectorLayer.ShapeData = newShapeData;
+                changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
+            }
+        }
+
+        return changes;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = true;
+        return RevertPreview(target);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return RevertPreview(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RevertPreview(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+        foreach (var layerGuid in layerGuids)
+        {
+            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+
+            if (layer is ImageLayerNode imgLayer)
+            {
+                var image = imgLayer.GetLayerImageAtFrame(frame);
+                var affected = image.FindAffectedArea();
+                image.CancelChanges();
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
+            }
+            else if (layer is VectorLayerNode transformableObject)
+            {
+                (transformableObject.ShapeData as PathVectorData)?.Path.Dispose();
+                transformableObject.ShapeData = originalShapes[layerGuid];
+            }
+        }
+
+        return changes;
+    }
+}

+ 68 - 51
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayer_UpdateableChange.cs

@@ -2,25 +2,25 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
-internal class ShiftLayer_UpdateableChange : UpdateableChange
+internal class ShiftLayer_UpdateableChange : Change
 {
 {
     private List<Guid> layerGuids;
     private List<Guid> layerGuids;
-    private bool keepOriginal;
-    private VecI delta;
+    private VecD delta;
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
     private Dictionary<Guid, CommittedChunkStorage?> originalLayerChunks = new();
+    private Dictionary<Guid, Matrix3X3> originalTransformations = new();
 
 
-    private List<IChangeInfo> _tempChanges = new();
     private int frame;
     private int frame;
 
 
-    [GenerateUpdateableChangeActions]
-    public ShiftLayer_UpdateableChange(List<Guid> layerGuids, VecI delta, bool keepOriginal, int frame)
+    [GenerateMakeChangeAction]
+    public ShiftLayer_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
     {
     {
         this.delta = delta;
         this.delta = delta;
         this.layerGuids = layerGuids;
         this.layerGuids = layerGuids;
-        this.keepOriginal = keepOriginal;
         this.frame = frame;
         this.frame = frame;
     }
     }
 
 
@@ -41,59 +41,38 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         return true;
         return true;
     }
     }
 
 
-    [UpdateChangeMethod]
-    public void Update(VecI delta, bool keepOriginal)
-    {
-        this.delta = delta;
-        this.keepOriginal = keepOriginal;
-    }
-
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage?>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage?>();
+        originalTransformations = new Dictionary<Guid, Matrix3X3>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
         foreach (var layerGuid in layerGuids)
         foreach (var layerGuid in layerGuids)
         {
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
 
-            // TODO: This now does't crash, but ignores other layer types. Think how to handle this.
-            if (layer is not ImageLayerNode)
+            if (layer is ImageLayerNode)
             {
             {
-                continue;
-            }
-
-            var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-
-            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
-
-            originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
-            image.CommitChanges();
-        }
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, (VecI)delta, frame);
+                var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
 
 
-        ignoreInUndo = delta.TaxicabLength == 0;
-        return changes;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        _tempChanges.Clear();
-
-        foreach (var layerGuid in layerGuids)
-        {
-            var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
 
 
-            if (layer is not ImageLayerNode)
+                originalLayerChunks[layerGuid] = new(image, image.FindAffectedArea().Chunks);
+                image.CommitChanges();
+            }
+            else if (layer is ITransformableObject transformableObject)
             {
             {
-                continue;
+                originalTransformations[layerGuid] = transformableObject.TransformationMatrix;
+                AffectedArea affected = AffectedAreaFromBounds(target, layerGuid, frame);
+                transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                Matrix3X3.CreateTranslation((float)delta.X, (float)delta.Y));
+                changes.Add(new VectorShape_ChangeInfo(layerGuid, affected));
             }
             }
-
-            var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, keepOriginal, delta, frame);
-            _tempChanges.Add(new LayerImageArea_ChangeInfo(layerGuid, chunks));
         }
         }
 
 
-        return _tempChanges;
+        ignoreInUndo = delta.TaxicabLength == 0;
+        return changes;
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
@@ -103,15 +82,17 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
         {
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
 
-            if (layer is not ImageLayerNode)
+            if (layer is ImageLayerNode)
             {
             {
-                continue;
+                var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
+                CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
+                var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
+                changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
+            }
+            else if (layer is ITransformableObject transformableObject)
+            {
+                transformableObject.TransformationMatrix = originalTransformations[layerGuid];
             }
             }
-
-            var image = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
-            CommittedChunkStorage? originalChunks = originalLayerChunks[layerGuid];
-            var affected = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(image, ref originalChunks);
-            changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
         }
         }
 
 
         return changes;
         return changes;
@@ -124,4 +105,40 @@ internal class ShiftLayer_UpdateableChange : UpdateableChange
             value?.Dispose();
             value?.Dispose();
         }
         }
     }
     }
+    
+    internal static AffectedArea AffectedAreaFromBounds(Document target, Guid layerGuid, int frame)
+    {
+        HashSet<VecI> chunks = new HashSet<VecI>();
+        
+        var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
+        if (layer is not VectorLayerNode vectorLayer)
+        {
+            return new AffectedArea();
+        }
+
+        RectD? bounds = vectorLayer.GetTightBounds(frame);
+        if (bounds is null)
+        {
+            return new AffectedArea();
+        }
+        
+        RectD boundsValue = bounds.Value;
+
+        int chunkSize = ChunkyImage.FullChunkSize;
+        
+        VecI start = new VecI((int)boundsValue.X / chunkSize, (int)boundsValue.Y / chunkSize);
+        VecI end = new VecI((int)(boundsValue.X + boundsValue.Width) / chunkSize, (int)(boundsValue.Y + boundsValue.Height) / chunkSize);
+        
+        HashSet<VecI> affectedChunks = new HashSet<VecI>();
+        
+        for (int x = start.X; x <= end.X; x++)
+        {
+            for (int y = start.Y; y <= end.Y; y++)
+            {
+                affectedChunks.Add(new VecI(x, y));
+            }
+        }
+        
+        return new AffectedArea(affectedChunks);
+    }
 }
 }

+ 18 - 11
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -9,6 +9,7 @@ using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Vector;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 
@@ -118,21 +119,22 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             }
             else if (layer is ITransformableObject transformable)
             else if (layer is ITransformableObject transformable)
             {
             {
-                SetTransformableMember(layer, member, transformable, tightBounds);
+                SetTransformableMember(layer.Id, member, transformable, tightBounds);
             }
             }
         }
         }
 
 
         return true;
         return true;
     }
     }
 
 
-    private void SetTransformableMember(StructureNode layer, MemberTransformationData member,
+    private void SetTransformableMember(Guid transformableId,
+        MemberTransformationData member,
         ITransformableObject transformable, RectD tightBounds)
         ITransformableObject transformable, RectD tightBounds)
     {
     {
         member.OriginalBounds = tightBounds; 
         member.OriginalBounds = tightBounds; 
         VecD posRelativeToMaster = member.OriginalBounds.Value.TopLeft - masterCorners.TopLeft;
         VecD posRelativeToMaster = member.OriginalBounds.Value.TopLeft - masterCorners.TopLeft;
 
 
         member.OriginalPos = (VecI)posRelativeToMaster;
         member.OriginalPos = (VecI)posRelativeToMaster;
-        member.AddTransformableObject(transformable, transformable.TransformationMatrix);
+        member.AddTransformableObject(transformableId, transformable.TransformationMatrix);
     }
     }
 
 
     private void SetImageMember(Document target, MemberTransformationData member, RectD originalTightBounds,
     private void SetImageMember(Document target, MemberTransformationData member, RectD originalTightBounds,
@@ -255,7 +257,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             }
             else if (member.IsTransformable)
             else if (member.IsTransformable)
             {
             {
-                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.LocalMatrix;
 
 
                 AffectedArea area = GetTranslationAffectedArea();
                 AffectedArea area = GetTranslationAffectedArea();
                 infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
                 infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
@@ -290,7 +293,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             }
             else if (member.IsTransformable)
             else if (member.IsTransformable)
             {
             {
-                member.TransformableObject.TransformationMatrix = member.LocalMatrix;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.LocalMatrix;
 
 
                 AffectedArea translationAffectedArea = GetTranslationAffectedArea();
                 AffectedArea translationAffectedArea = GetTranslationAffectedArea();
                 var tmp = new AffectedArea(translationAffectedArea);
                 var tmp = new AffectedArea(translationAffectedArea);
@@ -331,7 +335,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             }
             }
             else if (member.IsTransformable)
             else if (member.IsTransformable)
             {
             {
-                member.TransformableObject.TransformationMatrix = member.OriginalMatrix!.Value;
+                var transformable = target.FindMemberOrThrow(member.MemberId) as ITransformableObject;
+                transformable.TransformationMatrix = member.OriginalMatrix!.Value;
 
 
                 //TODO this is probably wrong
                 //TODO this is probably wrong
                 AffectedArea area = GetTranslationAffectedArea();
                 AffectedArea area = GetTranslationAffectedArea();
@@ -409,8 +414,8 @@ class MemberTransformationData : IDisposable
     public Guid MemberId { get; }
     public Guid MemberId { get; }
     public ShapeCorners MemberCorners { get; init; }
     public ShapeCorners MemberCorners { get; init; }
 
 
-    public ITransformableObject? TransformableObject { get; private set; }
-    public Matrix3X3? OriginalMatrix { get; private set; }
+    public Guid TransformableObjectId { get; private set; }
+    public Matrix3X3? OriginalMatrix { get; set; }
 
 
     public CommittedChunkStorage? SavedChunks { get; set; }
     public CommittedChunkStorage? SavedChunks { get; set; }
     public VectorPath? OriginalPath { get; set; }
     public VectorPath? OriginalPath { get; set; }
@@ -418,18 +423,19 @@ class MemberTransformationData : IDisposable
     public RectD? OriginalBounds { get; set; }
     public RectD? OriginalBounds { get; set; }
     public VecD? OriginalPos { get; set; }
     public VecD? OriginalPos { get; set; }
     public bool IsImage => Image != null;
     public bool IsImage => Image != null;
-    public bool IsTransformable => TransformableObject != null;
+    public bool IsTransformable => TransformableObjectId != default;
     public RectI? RoundedOriginalBounds => (RectI)OriginalBounds?.RoundOutwards();
     public RectI? RoundedOriginalBounds => (RectI)OriginalBounds?.RoundOutwards();
     public Matrix3X3 LocalMatrix { get; set; }
     public Matrix3X3 LocalMatrix { get; set; }
+    public ShapeVectorData? OriginalShapeData { get; set; }
 
 
     public MemberTransformationData(Guid memberId)
     public MemberTransformationData(Guid memberId)
     {
     {
         MemberId = memberId;
         MemberId = memberId;
     }
     }
 
 
-    public void AddTransformableObject(ITransformableObject transformableObject, Matrix3X3 originalMatrix)
+    public void AddTransformableObject(Guid transformableObject, Matrix3X3 originalMatrix)
     {
     {
-        TransformableObject = transformableObject;
+        TransformableObjectId = transformableObject;
         OriginalMatrix = new Matrix3X3?(originalMatrix);
         OriginalMatrix = new Matrix3X3?(originalMatrix);
     }
     }
 
 
@@ -446,5 +452,6 @@ class MemberTransformationData : IDisposable
         OriginalPath?.Dispose();
         OriginalPath?.Dispose();
         OriginalPath = null;
         OriginalPath = null;
         SavedChunks?.Dispose();
         SavedChunks?.Dispose();
+        OriginalShapeData = null;
     }
     }
 }
 }

+ 87 - 7
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -9,6 +9,7 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Shaders.Generation;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Context;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
 namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
@@ -29,7 +30,7 @@ public static class NodeOperations
             INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
             INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
             allFactories.Add(factory.NodeType, factory);
             allFactories.Add(factory.NodeType, factory);
         }
         }
-        
+
         nodeMap = new Dictionary<string, Type>();
         nodeMap = new Dictionary<string, Type>();
         var nodeTypes = typeof(Node).Assembly.GetTypes().Where(x =>
         var nodeTypes = typeof(Node).Assembly.GetTypes().Where(x =>
                 x.IsSubclassOf(typeof(Node)) && x is { IsAbstract: false, IsInterface: false })
                 x.IsSubclassOf(typeof(Node)) && x is { IsAbstract: false, IsInterface: false })
@@ -55,7 +56,7 @@ public static class NodeOperations
     {
     {
         return nodeMap.TryGetValue(nodeUniqueName, out nodeType);
         return nodeMap.TryGetValue(nodeUniqueName, out nodeType);
     }
     }
-    
+
     public static Node CreateNode(Type nodeType, IReadOnlyDocument target, params object[] optionalParameters)
     public static Node CreateNode(Type nodeType, IReadOnlyDocument target, params object[] optionalParameters)
     {
     {
         Node node = null;
         Node node = null;
@@ -71,12 +72,12 @@ public static class NodeOperations
         return node;
         return node;
     }
     }
 
 
-    public static List<ConnectProperty_ChangeInfo> AppendMember(
+    public static List<IChangeInfo> AppendMember(
         InputProperty<Painter?> parentInput,
         InputProperty<Painter?> parentInput,
         OutputProperty<Painter> toAddOutput,
         OutputProperty<Painter> toAddOutput,
         InputProperty<Painter> toAddInput, Guid memberId)
         InputProperty<Painter> toAddInput, Guid memberId)
     {
     {
-        List<ConnectProperty_ChangeInfo> changes = new();
+        List<IChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
         IOutputProperty? previouslyConnected = null;
         if (parentInput.Connection != null)
         if (parentInput.Connection != null)
         {
         {
@@ -94,7 +95,7 @@ public static class NodeOperations
 
 
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
         changes.Add(new ConnectProperty_ChangeInfo(memberId, parentInput.Node.Id,
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
             toAddOutput.InternalPropertyName, parentInput.InternalPropertyName));
-        
+
         return changes;
         return changes;
     }
     }
 
 
@@ -113,6 +114,7 @@ public static class NodeOperations
             foreach (var input in connections)
             foreach (var input in connections)
             {
             {
                 output.ConnectTo(input);
                 output.ConnectTo(input);
+
                 changes.Add(new ConnectProperty_ChangeInfo(output.Node.Id, input.Node.Id,
                 changes.Add(new ConnectProperty_ChangeInfo(output.Node.Id, input.Node.Id,
                     output.InternalPropertyName, input.InternalPropertyName));
                     output.InternalPropertyName, input.InternalPropertyName));
             }
             }
@@ -133,6 +135,84 @@ public static class NodeOperations
         return changes;
         return changes;
     }
     }
 
 
+    public static List<IChangeInfo> AdjustPositionsAfterAppend(Node member, Node appendedTo, Node? previouslyConnected,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        previouslyConnected?.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                toMove.Position = (previousNode?.Position ?? member.Position) - new VecD(250, 0);
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+            }
+
+            return true;
+        });
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> AdjustPositionsBeforeAppend(Node member, Node appendedTo,
+        out Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+        Dictionary<Guid, VecD> originalPositionDict = new();
+
+        member.TraverseBackwards((aNode, previousNode, _) =>
+        {
+            if (aNode is Node toMove)
+            {
+                originalPositionDict.Add(toMove.Id, toMove.Position);
+                var y = toMove.Position.Y;
+                VecD pos = member.Position + new VecD(250, 0);
+                if (previousNode != null)
+                {
+                    pos = previousNode.Position - new VecD(250, 0);
+                }
+
+                toMove.Position = pos;
+                toMove.Position = new VecD(toMove.Position.X, y);
+                changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
+                
+                if(aNode == appendedTo) return false;
+            }
+
+            return true;
+        });
+
+        member.Position = new VecD(appendedTo.Position.X - 250, appendedTo.Position.Y);
+        changes.Add(new NodePosition_ChangeInfo(member.Id, member.Position));
+
+        originalPositions = originalPositionDict;
+        return changes;
+    }
+
+    public static List<IChangeInfo> RevertPositions(Dictionary<Guid, VecD> positions, IReadOnlyDocument target)
+    {
+        List<IChangeInfo> changes = new();
+        foreach (var (guid, position) in positions)
+        {
+            var node = target.FindNode(guid) as Node;
+            if (node == null) continue;
+
+            node.Position = position;
+            changes.Add(new NodePosition_ChangeInfo(guid, position));
+        }
+
+        return changes;
+    }
+
     public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     public static ConnectionsData CreateConnectionsData(IReadOnlyNode node)
     {
     {
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
         var originalOutputConnections = new Dictionary<PropertyConnection, List<PropertyConnection>>();
@@ -152,7 +232,7 @@ public static class NodeOperations
                 (new PropertyConnection(x.Node.Id, x.InternalPropertyName),
                 (new PropertyConnection(x.Node.Id, x.InternalPropertyName),
                     new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
                     new PropertyConnection(x.Connection?.Node.Id, x.Connection?.InternalPropertyName)))
             .ToList();
             .ToList();
-        
+
         return new ConnectionsData(originalOutputConnections, originalInputConnections);
         return new ConnectionsData(originalOutputConnections, originalInputConnections);
     }
     }
 
 
@@ -246,7 +326,7 @@ public static class NodeOperations
                     value = expressionVariable.GetConstant();
                     value = expressionVariable.GetConstant();
                 }
                 }
             }
             }
-            
+
             changes.Add(new PropertyValueUpdated_ChangeInfo(copy.Id, input.InternalPropertyName, value));
             changes.Add(new PropertyValueUpdated_ChangeInfo(copy.Id, input.InternalPropertyName, value));
         }
         }
 
 

+ 11 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -111,7 +112,16 @@ internal sealed class FlipImage_Change : Change
                         new LayerImageArea_ChangeInfo(member.Id, image.FindAffectedArea()));
                         new LayerImageArea_ChangeInfo(member.Id, image.FindAffectedArea()));
                     image.CommitChanges();
                     image.CommitChanges();
                 }
                 }
-                // TODO: Add support for non-raster layers
+                else if (member is ITransformableObject transformableObject)
+                {
+                    RectD? tightBounds = member.GetTightBounds(frame);
+                    if(tightBounds == null) return;
+                    transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                        Matrix3X3.CreateScale(
+                            flipType == FlipType.Horizontal ? -1 : 1,
+                            flipType == FlipType.Vertical ? -1 : 1, 
+                            (float)tightBounds.Value.Center.X, (float)tightBounds.Value.Center.Y));
+                }
 
 
                 if (member.EmbeddedMask is not null)
                 if (member.EmbeddedMask is not null)
                 {
                 {

+ 9 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -172,8 +173,14 @@ internal sealed class RotateImage_Change : Change
                         });
                         });
                     }
                     }
                 }
                 }
-
-                // TODO: Add support for different Layer types
+                else if (member is ITransformableObject transformableObject)
+                {
+                    RectD? tightBounds = member.GetTightBounds(frame.Value);
+                    transformableObject.TransformationMatrix = transformableObject.TransformationMatrix.PostConcat(
+                        Matrix3X3.CreateRotation(
+                            RotationAngleToRadians(rotation),
+                            (float?)tightBounds?.Center.X ?? 0, (float?)tightBounds?.Center.Y ?? 0));
+                }
 
 
                 if (member.EmbeddedMask is null)
                 if (member.EmbeddedMask is null)
                     return;
                     return;

+ 21 - 14
src/PixiEditor.ChangeableDocument/Changes/Structure/CreateStructureMember_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -13,8 +14,9 @@ internal class CreateStructureMember_Change : Change
 
 
     private Guid parentGuid;
     private Guid parentGuid;
     private Type structureMemberOfType;
     private Type structureMemberOfType;
-    
+
     private ConnectionsData? connectionsData;
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
     public CreateStructureMember_Change(Guid parent, Guid newGuid,
@@ -27,25 +29,28 @@ internal class CreateStructureMember_Change : Change
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
-        if(structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface || !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
+        if (structureMemberOfType == null || structureMemberOfType.IsAbstract || structureMemberOfType.IsInterface ||
+            !structureMemberOfType.IsAssignableTo(typeof(StructureNode)))
             return false;
             return false;
-        
+
         return target.TryFindNode<Node>(parentGuid, out _);
         return target.TryFindNode<Node>(parentGuid, out _);
     }
     }
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document document, bool firstApply,
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
-        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document); 
+        StructureNode member = (StructureNode)NodeOperations.CreateNode(structureMemberOfType, document);
         member.Id = newMemberGuid;
         member.Id = newMemberGuid;
 
 
         document.TryFindNode<Node>(parentGuid, out var parentNode);
         document.TryFindNode<Node>(parentGuid, out var parentNode);
 
 
         List<IChangeInfo> changes = new() { CreateChangeInfo(member) };
         List<IChangeInfo> changes = new() { CreateChangeInfo(member) };
-        
-        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x => 
+
+        InputProperty<Painter> targetInput = parentNode.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
-        
+
+        var previouslyConnected = targetInput.Connection;
+
         if (member is FolderNode folder)
         if (member is FolderNode folder)
         {
         {
             document.NodeGraph.AddNode(member);
             document.NodeGraph.AddNode(member);
@@ -54,11 +59,12 @@ internal class CreateStructureMember_Change : Change
         else
         else
         {
         {
             document.NodeGraph.AddNode(member);
             document.NodeGraph.AddNode(member);
-            List<ConnectProperty_ChangeInfo> connectPropertyChangeInfo =
+            var connectPropertyChangeInfo =
                 NodeOperations.AppendMember(targetInput, member.Output, member.Background, member.Id);
                 NodeOperations.AppendMember(targetInput, member.Output, member.Background, member.Id);
             changes.AddRange(connectPropertyChangeInfo);
             changes.AddRange(connectPropertyChangeInfo);
         }
         }
-
+        
+        changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(member, targetInput.Node, previouslyConnected?.Node as Node, out originalPositions));
 
 
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
@@ -79,7 +85,7 @@ internal class CreateStructureMember_Change : Change
     {
     {
         var container = document.FindNodeOrThrow<Node>(parentGuid);
         var container = document.FindNodeOrThrow<Node>(parentGuid);
 
 
-       InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x => 
+        InputProperty<Painter> backgroundInput = container.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
             x.ValueType == typeof(Painter)) as InputProperty<Painter>;
 
 
         StructureNode child = document.FindMemberOrThrow(newMemberGuid);
         StructureNode child = document.FindMemberOrThrow(newMemberGuid);
@@ -98,15 +104,16 @@ internal class CreateStructureMember_Change : Change
                 backgroundInput.InternalPropertyName);
                 backgroundInput.InternalPropertyName);
             changes.Add(change);
             changes.Add(change);
         }
         }
+        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, document));
 
 
         return changes;
         return changes;
     }
     }
 
 
-    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder, List<IChangeInfo> changes)
+    private static void AppendFolder(InputProperty<Painter> backgroundInput, FolderNode folder,
+        List<IChangeInfo> changes)
     {
     {
         var appened = NodeOperations.AppendMember(backgroundInput, folder.Output, folder.Background, folder.Id);
         var appened = NodeOperations.AppendMember(backgroundInput, folder.Output, folder.Background, folder.Id);
         changes.AddRange(appened);
         changes.AddRange(appened);
     }
     }
-
-    
 }
 }

+ 25 - 5
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,4 +1,7 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -13,14 +16,18 @@ internal class DuplicateFolder_Change : Change
     private Guid[] contentGuids;
     private Guid[] contentGuids;
     private Guid[] contentDuplicateGuids;
     private Guid[] contentDuplicateGuids;
 
 
+    private Guid[]? childGuidsToUse;
+
     private ConnectionsData? connectionsData;
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+    private Dictionary<Guid, VecD> originalPositions;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
-    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid)
+    public DuplicateFolder_Change(Guid folderGuid, Guid newGuid, ImmutableList<Guid>? childGuids)
     {
     {
         this.folderGuid = folderGuid;
         this.folderGuid = folderGuid;
         duplicateGuid = newGuid;
         duplicateGuid = newGuid;
+        childGuidsToUse = childGuids?.ToArray();
     }
     }
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
@@ -58,11 +65,14 @@ internal class DuplicateFolder_Change : Change
 
 
         target.NodeGraph.AddNode(clone);
         target.NodeGraph.AddNode(clone);
         
         
+        var previousConnection = targetInput.Connection;
+
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node, previousConnection?.Node as Node, out originalPositions));
 
 
         DuplicateContent(target, clone, existingLayer, operations);
         DuplicateContent(target, clone, existingLayer, operations);
-        
+
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
         return operations;
         return operations;
@@ -87,7 +97,7 @@ internal class DuplicateFolder_Change : Change
                 Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
                 Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
                 changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
                 changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
                 changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
                 changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
-                
+
                 target.NodeGraph.RemoveNode(contentNode);
                 target.NodeGraph.RemoveNode(contentNode);
                 contentNode.Dispose();
                 contentNode.Dispose();
             }
             }
@@ -100,6 +110,8 @@ internal class DuplicateFolder_Change : Change
                 NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
                 NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
         }
 
 
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         return changes;
         return changes;
     }
     }
 
 
@@ -109,6 +121,7 @@ internal class DuplicateFolder_Change : Change
         Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
         Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
 
 
         nodeMap[existingLayer.Id] = clone.Id;
         nodeMap[existingLayer.Id] = clone.Id;
+        int counter = 0;
         List<Guid> contentGuidList = new();
         List<Guid> contentGuidList = new();
 
 
         existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
         existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
@@ -117,6 +130,13 @@ internal class DuplicateFolder_Change : Change
                 return false;
                 return false;
 
 
             Node? node = targetNode.Clone();
             Node? node = targetNode.Clone();
+            
+            if(node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+            {
+                node.Id = childGuidsToUse[counter];
+                counter++;
+            }
+            
             nodeMap[x.Id] = node.Id;
             nodeMap[x.Id] = node.Id;
             contentGuidList.Add(node.Id);
             contentGuidList.Add(node.Id);
 
 
@@ -133,7 +153,7 @@ internal class DuplicateFolder_Change : Change
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
             operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
         }
-        
+
         contentDuplicateGuids = contentGuidList.ToArray();
         contentDuplicateGuids = contentGuidList.ToArray();
     }
     }
 }
 }

+ 12 - 3
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -12,18 +13,19 @@ internal class DuplicateLayer_Change : Change
     private Guid duplicateGuid;
     private Guid duplicateGuid;
     
     
     private ConnectionsData? connectionsData;
     private ConnectionsData? connectionsData;
+    private Dictionary<Guid, VecD> originalPositions;
 
 
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
-    public DuplicateLayer_Change(Guid layerGuid)
+    public DuplicateLayer_Change(Guid layerGuid, Guid newGuid)
     {
     {
         this.layerGuid = layerGuid;
         this.layerGuid = layerGuid;
+        this.duplicateGuid = newGuid;
     }
     }
 
 
     public override bool InitializeAndValidate(Document target)
     public override bool InitializeAndValidate(Document target)
     {
     {
         if (!target.TryFindMember<LayerNode>(layerGuid, out LayerNode? layer))
         if (!target.TryFindMember<LayerNode>(layerGuid, out LayerNode? layer))
             return false;
             return false;
-        duplicateGuid = Guid.NewGuid();
         
         
         connectionsData = NodeOperations.CreateConnectionsData(layer);
         connectionsData = NodeOperations.CreateConnectionsData(layer);
         
         
@@ -42,6 +44,8 @@ internal class DuplicateLayer_Change : Change
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
         InputProperty<Painter?> targetInput = parent.InputProperties.FirstOrDefault(x =>
             x.ValueType == typeof(Painter) &&
             x.ValueType == typeof(Painter) &&
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+        
+        var previousConnection = targetInput.Connection;
 
 
         List<IChangeInfo> operations = new();
         List<IChangeInfo> operations = new();
 
 
@@ -51,6 +55,9 @@ internal class DuplicateLayer_Change : Change
         
         
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
 
 
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
         ignoreInUndo = false;
         ignoreInUndo = false;
 
 
         return operations;
         return operations;
@@ -74,6 +81,8 @@ internal class DuplicateLayer_Change : Change
             changes.AddRange(NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
             changes.AddRange(NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
         }
         }
         
         
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+        
         return changes;
         return changes;
     }
     }
 }
 }

+ 99 - 13
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -1,6 +1,8 @@
-using PixiEditor.ChangeableDocument.Changeables.Graph;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 
 
@@ -14,8 +16,9 @@ internal class MoveStructureMember_Change : Change
 
 
     private Guid originalFolderGuid;
     private Guid originalFolderGuid;
 
 
-    private ConnectionsData originalConnections; 
-    
+    private ConnectionsData originalConnections;
+    private Dictionary<Guid, VecD> originalPositions;
+
     private bool putInsideFolder;
     private bool putInsideFolder;
 
 
 
 
@@ -34,20 +37,22 @@ internal class MoveStructureMember_Change : Change
         if (member is null || targetFolder is null)
         if (member is null || targetFolder is null)
             return false;
             return false;
 
 
-        originalConnections = NodeOperations.CreateConnectionsData(member); 
-          
+        originalConnections = NodeOperations.CreateConnectionsData(member);
+
         return true;
         return true;
     }
     }
 
 
-    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid, bool putInsideFolder)
+    private static List<IChangeInfo> Move(Document document, Guid sourceNodeGuid, Guid targetNodeGuid,
+        bool putInsideFolder, out Dictionary<Guid, VecD> originalPositions)
     {
     {
         var sourceNode = document.FindMember(sourceNodeGuid);
         var sourceNode = document.FindMember(sourceNodeGuid);
         var targetNode = document.FindNode(targetNodeGuid);
         var targetNode = document.FindNode(targetNodeGuid);
+        originalPositions = null;
         if (sourceNode is null || targetNode is not IRenderInput backgroundInput)
         if (sourceNode is null || targetNode is not IRenderInput backgroundInput)
             return [];
             return [];
 
 
         List<IChangeInfo> changes = new();
         List<IChangeInfo> changes = new();
-        
+
         Guid oldBackgroundId = sourceNode.Background.Node.Id;
         Guid oldBackgroundId = sourceNode.Background.Node.Id;
 
 
         InputProperty<Painter?> inputProperty = backgroundInput.Background;
         InputProperty<Painter?> inputProperty = backgroundInput.Background;
@@ -58,12 +63,43 @@ internal class MoveStructureMember_Change : Change
         }
         }
 
 
         MoveStructureMember_ChangeInfo changeInfo = new(sourceNodeGuid, oldBackgroundId, targetNodeGuid);
         MoveStructureMember_ChangeInfo changeInfo = new(sourceNodeGuid, oldBackgroundId, targetNodeGuid);
+
+        var previouslyConnected = inputProperty.Connection;
+
+        bool isMovingBelow = false;
         
         
+        inputProperty.Node.TraverseForwards(x =>
+        {
+            if (x.Id == sourceNodeGuid)
+            {
+                isMovingBelow = true;
+                return false;
+            }
+            
+            return true;
+        });
+
+        if (isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsBeforeAppend(sourceNode, inputProperty.Node, out originalPositions));
+        }
+
         changes.AddRange(NodeOperations.DetachStructureNode(sourceNode));
         changes.AddRange(NodeOperations.DetachStructureNode(sourceNode));
         changes.AddRange(NodeOperations.AppendMember(inputProperty, sourceNode.Output,
         changes.AddRange(NodeOperations.AppendMember(inputProperty, sourceNode.Output,
             sourceNode.Background,
             sourceNode.Background,
             sourceNode.Id));
             sourceNode.Id));
-        
+
+        if (!isMovingBelow)
+        {
+            changes.AddRange(NodeOperations.AdjustPositionsAfterAppend(sourceNode, inputProperty.Node,
+                previouslyConnected?.Node as Node, out originalPositions));
+        }
+
+        if (targetNode is FolderNode)
+        {
+            changes.AddRange(AdjustPutIntoFolderPositions(targetNode, originalPositions));
+        }
+
         changes.Add(changeInfo);
         changes.Add(changeInfo);
 
 
         return changes;
         return changes;
@@ -72,7 +108,7 @@ internal class MoveStructureMember_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
-        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder);
+        var changes = Move(target, memberGuid, targetNodeGuid, putInsideFolder, out originalPositions);
         ignoreInUndo = false;
         ignoreInUndo = false;
         return changes;
         return changes;
     }
     }
@@ -82,14 +118,64 @@ internal class MoveStructureMember_Change : Change
         StructureNode member = target.FindMember(memberGuid);
         StructureNode member = target.FindMember(memberGuid);
 
 
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        
+
         MoveStructureMember_ChangeInfo changeInfo = new(memberGuid, targetNodeGuid, originalFolderGuid);
         MoveStructureMember_ChangeInfo changeInfo = new(memberGuid, targetNodeGuid, originalFolderGuid);
-        
+
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.AddRange(NodeOperations.DetachStructureNode(member));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, member, target.NodeGraph));
         changes.AddRange(NodeOperations.ConnectStructureNodeProperties(originalConnections, member, target.NodeGraph));
-        
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         changes.Add(changeInfo);
         changes.Add(changeInfo);
-        
+
+        return changes;
+    }
+    
+    private static List<IChangeInfo> AdjustPutIntoFolderPositions(Node targetNode, Dictionary<Guid, VecD> originalPositions)
+    {
+        List<IChangeInfo> changes = new();
+
+        if (targetNode is FolderNode folder)
+        {
+            folder.Content.Connection.Node.TraverseBackwards(contentNode =>
+            {
+                if (contentNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, folder.Position.Y + 250);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+            
+            folder.Background.Connection?.Node.TraverseBackwards(bgNode =>
+            {
+                if (bgNode is Node node)
+                {
+                    if (!originalPositions.ContainsKey(node.Id))
+                    {
+                        originalPositions[node.Id] = node.Position;
+                    }
+
+                    double pos = folder.Position.Y;
+
+                    if (folder.Content.Connection != null)
+                    {
+                        pos -= 250;
+                    }
+                    
+                    node.Position = new VecD(node.Position.X, pos);
+                    changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
+                }
+                
+                return true;
+            });
+        }
+
         return changes;
         return changes;
     }
     }
 }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -46,6 +46,7 @@ internal class RasterizeMember_Change : Change
         
         
         ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         imageLayer.MemberName = node.DisplayName;
         imageLayer.MemberName = node.DisplayName;
+        imageLayer.Position = node.Position;
 
 
         target.NodeGraph.AddNode(imageLayer);
         target.NodeGraph.AddNode(imageLayer);
         
         

+ 46 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/SetLowDpiRendering_Change.cs

@@ -0,0 +1,46 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class SetLowDpiRendering_Change : Change
+{
+    public readonly Guid memberId;
+    public bool value;
+    
+    private bool originalValue;
+    
+    [GenerateMakeChangeAction]
+    public SetLowDpiRendering_Change(Guid memberId, bool value)
+    {
+        this.memberId = memberId;
+        this.value = value;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindNode(memberId, out RenderNode node);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        RenderNode node = target.FindNodeOrThrow<RenderNode>(memberId);
+        
+        bool toSet = !value;
+        
+        originalValue = node.AllowHighDpiRendering;
+        node.AllowHighDpiRendering = toSet;
+        
+        ignoreInUndo = originalValue == toSet;
+
+        return new None();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        RenderNode node = target.FindNodeOrThrow<RenderNode>(memberId);
+        
+        node.AllowHighDpiRendering = originalValue;
+
+        return new None();
+    }
+}

+ 73 - 0
src/PixiEditor.ChangeableDocument/Changes/Vectors/ConvertToCurve_Change.cs

@@ -0,0 +1,73 @@
+using ChunkyImageLib.Operations;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.ChangeInfos.Vectors;
+
+namespace PixiEditor.ChangeableDocument.Changes.Vectors;
+
+internal class ConvertToCurve_Change : Change
+{
+    public readonly Guid memberId;
+
+    private ShapeVectorData originalData;
+    private bool originalHighDpiRendering;
+
+    [GenerateMakeChangeAction]
+    public ConvertToCurve_Change(Guid memberId)
+    {
+        this.memberId = memberId;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.TryFindNode(memberId, out VectorLayerNode? node))
+        {
+            return node.ShapeData != null && node.ShapeData is not PathVectorData;
+        }
+
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        originalData = node.ShapeData;
+
+        node.ShapeData = new PathVectorData(originalData.ToPath())
+        {
+            Fill = originalData.Fill,
+            FillColor = originalData.FillColor,
+            StrokeColor = originalData.StrokeColor,
+            StrokeWidth = originalData.StrokeWidth,
+            TransformationMatrix = originalData.TransformationMatrix
+        };
+
+        originalHighDpiRendering = node.AllowHighDpiRendering;
+        node.AllowHighDpiRendering = true;
+
+        ignoreInUndo = false;
+
+        var aabb = node.ShapeData.TransformedVisualAABB;
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(memberId, affected);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        VectorLayerNode node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        node.ShapeData = originalData;
+
+        node.AllowHighDpiRendering = originalHighDpiRendering;
+
+        var aabb = node.ShapeData.TransformedVisualAABB;
+        var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
+            (RectI)aabb, ChunkyImage.FullChunkSize));
+
+        return new VectorShape_ChangeInfo(memberId, affected);
+    }
+}

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

@@ -12,7 +12,7 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     public ShapeVectorData Data { get; set; }
     public ShapeVectorData Data { get; set; }
 
 
     private ShapeVectorData? originalData;
     private ShapeVectorData? originalData;
-    
+
     private AffectedArea lastAffectedArea;
     private AffectedArea lastAffectedArea;
 
 
     [GenerateUpdateableChangeActions]
     [GenerateUpdateableChangeActions]
@@ -42,8 +42,9 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
     {
         var node = target.FindNode<VectorLayerNode>(TargetId);
         var node = target.FindNode<VectorLayerNode>(TargetId);
-        node.ShapeData = Data;
 
 
+        node.ShapeData = Data;
+        
         RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
         RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
 
 
@@ -51,14 +52,14 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
             (RectI)aabb, ChunkyImage.FullChunkSize));
             (RectI)aabb, ChunkyImage.FullChunkSize));
 
 
         var tmp = new AffectedArea(affected);
         var tmp = new AffectedArea(affected);
-        
+
         if (lastAffectedArea.Chunks != null)
         if (lastAffectedArea.Chunks != null)
         {
         {
             affected.UnionWith(lastAffectedArea);
             affected.UnionWith(lastAffectedArea);
         }
         }
-        
+
         lastAffectedArea = tmp;
         lastAffectedArea = tmp;
-        
+
         return new VectorShape_ChangeInfo(node.Id, affected);
         return new VectorShape_ChangeInfo(node.Id, affected);
     }
     }
 
 
@@ -68,7 +69,7 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
         ignoreInUndo = false;
         ignoreInUndo = false;
         var node = target.FindNode<VectorLayerNode>(TargetId);
         var node = target.FindNode<VectorLayerNode>(TargetId);
         node.ShapeData = Data;
         node.ShapeData = Data;
-        
+
         RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
         RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
         aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
 
 
@@ -84,12 +85,12 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
         node.ShapeData = originalData;
         node.ShapeData = originalData;
 
 
         AffectedArea affected = new AffectedArea();
         AffectedArea affected = new AffectedArea();
-        
+
         if (node.ShapeData != null)
         if (node.ShapeData != null)
-        { 
+        {
             RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
             RectD aabb = node.ShapeData.TransformedAABB.RoundOutwards();
             aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
             aabb = aabb with { Size = new VecD(Math.Max(1, aabb.Size.X), Math.Max(1, aabb.Size.Y)) };
-         
+
             affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
                 (RectI)aabb, ChunkyImage.FullChunkSize));
                 (RectI)aabb, ChunkyImage.FullChunkSize));
         }
         }
@@ -101,7 +102,7 @@ internal class SetShapeGeometry_UpdateableChange : InterruptableUpdateableChange
     {
     {
         if (other is SetShapeGeometry_UpdateableChange change)
         if (other is SetShapeGeometry_UpdateableChange change)
         {
         {
-            return change.TargetId == TargetId;
+            return change.TargetId == TargetId && change.Data is not TextVectorData; // text should not be merged into one change
         }
         }
 
 
         return false;
         return false;

+ 1 - 0
src/PixiEditor.Extensions/UI/Overlays/OverlayPointerArgs.cs

@@ -11,4 +11,5 @@ public class OverlayPointerArgs
     public MouseButton InitialPressMouseButton { get; set; }
     public MouseButton InitialPressMouseButton { get; set; }
     public IOverlayPointer Pointer { get; set; }
     public IOverlayPointer Pointer { get; set; }
     public bool Handled { get; set; }
     public bool Handled { get; set; }
+    public int ClickCount { get; set; }
 }
 }

+ 4 - 1
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -5,19 +5,22 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
 
 
-public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IElementContainer
+public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IOpacity, IElementContainer
 {
 {
     public List<SvgElement> Children { get; } = new();
     public List<SvgElement> Children { get; } = new();
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
 
 
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
         List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);
     }
     }
+
 }
 }

+ 6 - 1
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -5,10 +5,11 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG.Elements;
 namespace PixiEditor.SVG.Elements;
 
 
-public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
+public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable, IOpacity
 {
 {
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     
     
@@ -16,16 +17,20 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
     
     
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
 
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+
     public override void ParseData(XmlReader reader)
     public override void ParseData(XmlReader reader)
     {
     {
         List<SvgProperty> properties = GetProperties().ToList();
         List<SvgProperty> properties = GetProperties().ToList();
         
         
         properties.Add(Transform);
         properties.Add(Transform);
         properties.Add(Fill);
         properties.Add(Fill);
+        properties.Add(FillOpacity);
         properties.Add(Stroke);
         properties.Add(Stroke);
         properties.Add(StrokeWidth);
         properties.Add(StrokeWidth);
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineJoin);
         properties.Add(StrokeLineJoin);
+        properties.Add(Opacity);
 
 
         do
         do
         {
         {

+ 51 - 0
src/PixiEditor.SVG/Elements/SvgText.cs

@@ -0,0 +1,51 @@
+using System.Xml;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgText() : SvgPrimitive("text")
+{
+    public SvgProperty<SvgStringUnit> Text { get; } = new("");
+    public SvgProperty<SvgNumericUnit> X { get; } = new("x");
+    public SvgProperty<SvgNumericUnit> Y { get; } = new("y");
+    public SvgProperty<SvgNumericUnit> FontSize { get; } = new("font-size");
+    public SvgProperty<SvgStringUnit> FontFamily { get; } = new("font-family");
+    public SvgProperty<SvgEnumUnit<SvgFontWeight>> FontWeight { get; } = new("font-weight");
+    public SvgProperty<SvgEnumUnit<SvgFontStyle>> FontStyle { get; } = new("font-style");
+
+    public override void ParseData(XmlReader reader)
+    {
+        base.ParseData(reader);
+        Text.Unit = new SvgStringUnit(ParseContent(reader));
+    }
+
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return X;
+        yield return Y;
+        yield return FontSize;
+        yield return FontFamily;
+        yield return FontWeight;
+        yield return FontStyle;
+    }
+
+    private string ParseContent(XmlReader reader)
+    {
+        string content = string.Empty;
+        if (reader.NodeType == XmlNodeType.None) return content;
+        while (reader.Read())
+        {
+            if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
+            {
+                content = reader.Value;
+            }
+            else if (reader is { NodeType: XmlNodeType.EndElement, Name: "text" })
+            {
+                break;
+            }
+        }
+
+        return content;
+    }
+}

+ 8 - 0
src/PixiEditor.SVG/Enums/SvgFontStyle.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgFontStyle
+{
+    Normal,
+    Italic,
+    Oblique,
+}

+ 9 - 0
src/PixiEditor.SVG/Enums/SvgFontWeight.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgFontWeight
+{
+    Lighter = 100,
+    Normal = 400,
+    Bold = 700,
+    Bolder = 900,
+}

+ 1 - 0
src/PixiEditor.SVG/Features/IFillable.cs

@@ -6,4 +6,5 @@ namespace PixiEditor.SVG.Features;
 public interface IFillable
 public interface IFillable
 {
 {
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; }
 }
 }

+ 8 - 0
src/PixiEditor.SVG/Features/IOpacity.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IOpacity
+{
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
+}

+ 154 - 32
src/PixiEditor.SVG/StyleContext.cs

@@ -1,4 +1,6 @@
-using PixiEditor.SVG.Enums;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 using PixiEditor.SVG.Units;
 
 
@@ -9,69 +11,102 @@ public struct StyleContext
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
+    public SvgProperty<SvgStyleUnit> InlineStyle { get; set; }
+    public VecD ViewboxOrigin { get; set; }
 
 
     public StyleContext()
     public StyleContext()
     {
     {
         StrokeWidth = new("stroke-width");
         StrokeWidth = new("stroke-width");
         Stroke = new("stroke");
         Stroke = new("stroke");
         Fill = new("fill");
         Fill = new("fill");
+        FillOpacity = new("fill-opacity");
+        Fill.Unit = new SvgColorUnit?(new SvgColorUnit("black"));
         Transform = new("transform");
         Transform = new("transform");
         StrokeLineCap = new("stroke-linecap");
         StrokeLineCap = new("stroke-linecap");
         StrokeLineJoin = new("stroke-linejoin");
         StrokeLineJoin = new("stroke-linejoin");
+        Opacity = new("opacity");
+        InlineStyle = new("style");
     }
     }
-    
+
     public StyleContext(SvgDocument document)
     public StyleContext(SvgDocument document)
     {
     {
-        StrokeWidth = document.StrokeWidth;
-        Stroke = document.Stroke;
-        Fill = document.Fill;
-        Transform = document.Transform;
-        StrokeLineCap = document.StrokeLineCap;
-        StrokeLineJoin = document.StrokeLineJoin;
+        StrokeWidth = FallbackToCssStyle(document.StrokeWidth, document.Style);
+        Stroke = FallbackToCssStyle(document.Stroke, document.Style);
+        Fill = FallbackToCssStyle(document.Fill, document.Style, new SvgColorUnit("black"));
+        FillOpacity = FallbackToCssStyle(document.FillOpacity, document.Style);
+        Transform = FallbackToCssStyle(document.Transform, document.Style, new SvgTransformUnit(Matrix3X3.Identity));
+        StrokeLineCap = FallbackToCssStyle(document.StrokeLineCap, document.Style);
+        StrokeLineJoin = FallbackToCssStyle(document.StrokeLineJoin, document.Style);
+        Opacity = FallbackToCssStyle(document.Opacity, document.Style);
+        ViewboxOrigin = new VecD(
+            document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.X : 0,
+            document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.Y : 0);
+        InlineStyle = document.Style;
     }
     }
 
 
     public StyleContext WithElement(SvgElement element)
     public StyleContext WithElement(SvgElement element)
     {
     {
         StyleContext styleContext = Copy();
         StyleContext styleContext = Copy();
 
 
-        if (element is ITransformable { Transform.Unit: not null } transformableElement)
+        styleContext.InlineStyle = MergeInlineStyle(element.Style, InlineStyle);
+
+        if (element is ITransformable transformableElement)
         {
         {
-            styleContext.Transform.Unit = transformableElement.Transform.Unit;
+            if (styleContext.Transform.Unit == null)
+            {
+                styleContext.Transform.Unit =
+                    FallbackToCssStyle(transformableElement.Transform, styleContext.Transform, styleContext.InlineStyle)
+                        .Unit;
+            }
+            else
+            {
+                styleContext.Transform.Unit = new SvgTransformUnit(
+                    styleContext.Transform.Unit.Value.MatrixValue.Concat(
+                        FallbackToCssStyle(transformableElement.Transform, styleContext.InlineStyle).Unit
+                            ?.MatrixValue ??
+                        Matrix3X3.Identity));
+            }
         }
         }
 
 
-        if (element is IFillable { Fill.Unit: not null } fillableElement)
+        if (element is IFillable fillableElement)
         {
         {
-            styleContext.Fill.Unit = fillableElement.Fill.Unit;
+            styleContext.Fill.Unit = FallbackToCssStyle(fillableElement.Fill, styleContext.Fill,
+                styleContext.InlineStyle, new SvgColorUnit("black")).Unit;
+            styleContext.FillOpacity.Unit =
+                FallbackToCssStyle(fillableElement.FillOpacity, styleContext.FillOpacity, styleContext.InlineStyle)
+                    .Unit;
         }
         }
 
 
         if (element is IStrokable strokableElement)
         if (element is IStrokable strokableElement)
         {
         {
-            if (strokableElement.Stroke.Unit != null)
-            {
-                styleContext.Stroke.Unit = strokableElement.Stroke.Unit;
-            }
+            styleContext.Stroke.Unit =
+                FallbackToCssStyle(strokableElement.Stroke, styleContext.Stroke, styleContext.InlineStyle).Unit;
 
 
-            if (strokableElement.StrokeWidth.Unit != null)
-            {
-                styleContext.StrokeWidth.Unit = strokableElement.StrokeWidth.Unit;
-            }
-            
-            if (strokableElement.StrokeLineCap.Unit != null)
-            {
-                styleContext.StrokeLineCap.Unit = strokableElement.StrokeLineCap.Unit;
-            }
-            
-            if (strokableElement.StrokeLineJoin.Unit != null)
-            {
-                styleContext.StrokeLineJoin.Unit = strokableElement.StrokeLineJoin.Unit;
-            }
+            styleContext.StrokeWidth.Unit =
+                FallbackToCssStyle(strokableElement.StrokeWidth, styleContext.StrokeWidth, styleContext.InlineStyle)
+                    .Unit;
+
+            styleContext.StrokeLineCap.Unit =
+                FallbackToCssStyle(strokableElement.StrokeLineCap, styleContext.StrokeLineCap, styleContext.InlineStyle)
+                    .Unit;
+
+            styleContext.StrokeLineJoin.Unit =
+                FallbackToCssStyle(strokableElement.StrokeLineJoin, styleContext.StrokeLineJoin,
+                    styleContext.InlineStyle).Unit;
         }
         }
 
 
+        if (element is IOpacity opacityElement)
+        {
+            styleContext.Opacity.Unit =
+                FallbackToCssStyle(opacityElement.Opacity, styleContext.Opacity, styleContext.InlineStyle).Unit;
+        }
+
+
         return styleContext;
         return styleContext;
     }
     }
 
 
@@ -93,11 +128,98 @@ public struct StyleContext
             styleContext.Fill.Unit = Fill.Unit;
             styleContext.Fill.Unit = Fill.Unit;
         }
         }
 
 
+        if (FillOpacity.Unit != null)
+        {
+            styleContext.FillOpacity.Unit = FillOpacity.Unit;
+        }
+
         if (Transform.Unit != null)
         if (Transform.Unit != null)
         {
         {
             styleContext.Transform.Unit = Transform.Unit;
             styleContext.Transform.Unit = Transform.Unit;
         }
         }
 
 
+        if (StrokeLineCap.Unit != null)
+        {
+            styleContext.StrokeLineCap.Unit = StrokeLineCap.Unit;
+        }
+
+        if (StrokeLineJoin.Unit != null)
+        {
+            styleContext.StrokeLineJoin.Unit = StrokeLineJoin.Unit;
+        }
+
+        if (Opacity.Unit != null)
+        {
+            styleContext.Opacity.Unit = Opacity.Unit;
+        }
+
+        styleContext.ViewboxOrigin = ViewboxOrigin;
+
+        if (InlineStyle.Unit != null)
+        {
+            styleContext.InlineStyle.Unit = InlineStyle.Unit;
+        }
+
         return styleContext;
         return styleContext;
     }
     }
+
+
+    private SvgProperty<TUnit>? FallbackToCssStyle<TUnit>(
+        SvgProperty<TUnit> property,
+        SvgProperty<SvgStyleUnit> inlineStyle, TUnit? fallback = null) where TUnit : struct, ISvgUnit
+    {
+        if (property.Unit != null)
+        {
+            return property;
+        }
+
+        SvgStyleUnit? style = inlineStyle.Unit;
+        return style?.TryGetStyleFor<SvgProperty<TUnit>, TUnit>(property.SvgName)
+               ?? (fallback.HasValue
+                   ? new SvgProperty<TUnit>(property.SvgName) { Unit = fallback.Value }
+                   : new SvgProperty<TUnit>(property.SvgName));
+    }
+
+    private SvgProperty<TUnit>? FallbackToCssStyle<TUnit>(
+        SvgProperty<TUnit> property,
+        SvgProperty<TUnit> parentStyleProperty,
+        SvgProperty<SvgStyleUnit> inlineStyle, TUnit? fallback = null) where TUnit : struct, ISvgUnit
+    {
+        if (property.Unit != null)
+        {
+            return property;
+        }
+
+        SvgStyleUnit? style = inlineStyle.Unit;
+        var styleProp = style?.TryGetStyleFor<SvgProperty<TUnit>, TUnit>(property.SvgName);
+        if (styleProp != null) return styleProp;
+        if(parentStyleProperty.Unit != null)
+        {
+            return parentStyleProperty;
+        }
+
+        return (fallback.HasValue
+            ? new SvgProperty<TUnit>(property.SvgName) { Unit = fallback.Value }
+            : new SvgProperty<TUnit>(property.SvgName));
+    }
+
+    private SvgProperty<SvgStyleUnit> MergeInlineStyle(SvgProperty<SvgStyleUnit> elementStyle,
+        SvgProperty<SvgStyleUnit> parentStyle)
+    {
+        SvgStyleUnit? elementStyleUnit = elementStyle.Unit;
+        SvgStyleUnit? parentStyleUnit = parentStyle.Unit;
+
+        if (elementStyleUnit == null)
+        {
+            return parentStyle;
+        }
+
+        if (parentStyleUnit == null)
+        {
+            return elementStyle;
+        }
+
+        SvgStyleUnit style = parentStyleUnit.Value.MergeWith(elementStyleUnit.Value);
+        return new SvgProperty<SvgStyleUnit>("style") { Unit = style };
+    }
 }
 }

+ 16 - 13
src/PixiEditor.SVG/SvgDocument.cs

@@ -7,27 +7,28 @@ using PixiEditor.SVG.Units;
 
 
 namespace PixiEditor.SVG;
 namespace PixiEditor.SVG;
 
 
-public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable
+public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable, IOpacity
 {
 {
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string Version { get; set; } = "1.1";
     public string Version { get; set; } = "1.1";
-    
+
     public SvgProperty<SvgRectUnit> ViewBox { get; } = new("viewBox");
     public SvgProperty<SvgRectUnit> ViewBox { get; } = new("viewBox");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
-    
+
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
-    
+
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public List<SvgElement> Children { get; } = new();
     public List<SvgElement> Children { get; } = new();
 
 
     public SvgDocument() : base("svg")
     public SvgDocument() : base("svg")
     {
     {
-        
     }
     }
-    
+
     public SvgDocument(RectD viewBox) : base("svg")
     public SvgDocument(RectD viewBox) : base("svg")
     {
     {
         ViewBox.Unit = new SvgRectUnit(viewBox);
         ViewBox.Unit = new SvgRectUnit(viewBox);
@@ -38,14 +39,16 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         List<SvgProperty> properties = new()
         List<SvgProperty> properties = new()
         {
         {
             Fill,
             Fill,
+            FillOpacity,
             Stroke,
             Stroke,
             StrokeWidth,
             StrokeWidth,
             Transform,
             Transform,
             ViewBox,
             ViewBox,
             StrokeLineCap,
             StrokeLineCap,
-            StrokeLineJoin
+            StrokeLineJoin,
+            Opacity
         };
         };
-        
+
         ParseAttributes(properties, reader);
         ParseAttributes(properties, reader);
     }
     }
 
 
@@ -102,11 +105,11 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
 
 
     private void AppendProperties(XElement? root)
     private void AppendProperties(XElement? root)
     {
     {
-        if(ViewBox.Unit != null)
+        if (ViewBox.Unit != null)
         {
         {
             root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml()));
             root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml()));
         }
         }
-        
+
         if (Fill.Unit != null)
         if (Fill.Unit != null)
         {
         {
             root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml()));
             root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml()));
@@ -121,17 +124,17 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         {
         {
             root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml()));
             root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml()));
         }
         }
-        
+
         if (Transform.Unit != null)
         if (Transform.Unit != null)
         {
         {
             root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
             root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
         }
         }
-        
+
         if (StrokeLineCap.Unit != null)
         if (StrokeLineCap.Unit != null)
         {
         {
             root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml()));
             root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml()));
         }
         }
-        
+
         if (StrokeLineJoin.Unit != null)
         if (StrokeLineJoin.Unit != null)
         {
         {
             root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml()));
             root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml()));

+ 24 - 23
src/PixiEditor.SVG/SvgElement.cs

@@ -13,6 +13,7 @@ public class SvgElement(string tagName)
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public string TagName { get; } = tagName;
     public string TagName { get; } = tagName;
 
 
+    public SvgProperty<SvgStyleUnit> Style { get; } = new("style");
 
 
     public XElement ToXml(XNamespace nameSpace)
     public XElement ToXml(XNamespace nameSpace)
     {
     {
@@ -25,14 +26,21 @@ public class SvgElement(string tagName)
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
                 if (prop?.Unit != null)
                 if (prop?.Unit != null)
                 {
                 {
-                    if (!string.IsNullOrEmpty(prop.NamespaceName))
+                    if (string.IsNullOrEmpty(prop.SvgName))
                     {
                     {
-                        XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
-                        element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                        element.Value = prop.Unit.ToXml();
                     }
                     }
                     else
                     else
                     {
                     {
-                        element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                        if (!string.IsNullOrEmpty(prop.NamespaceName))
+                        {
+                            XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
+                            element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                        }
+                        else
+                        {
+                            element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -57,6 +65,16 @@ public class SvgElement(string tagName)
 
 
     protected void ParseAttributes(List<SvgProperty> properties, XmlReader reader)
     protected void ParseAttributes(List<SvgProperty> properties, XmlReader reader)
     {
     {
+        if (!properties.Contains(Id))
+        {
+            properties.Insert(0, Id);
+        }
+
+        if (!properties.Contains(Style))
+        {
+            properties.Insert(0, Style);
+        }
+
         do
         do
         {
         {
             SvgProperty matchingProperty = properties.FirstOrDefault(x =>
             SvgProperty matchingProperty = properties.FirstOrDefault(x =>
@@ -76,31 +94,14 @@ public class SvgElement(string tagName)
         }
         }
         else
         else
         {
         {
-            property.Unit ??= CreateDefaultUnit(property);
+            property.Unit ??= property.CreateDefaultUnit();
             property.Unit.ValuesFromXml(reader.Value);
             property.Unit.ValuesFromXml(reader.Value);
         }
         }
     }
     }
 
 
     private void ParseListProperty(SvgList list, XmlReader reader)
     private void ParseListProperty(SvgList list, XmlReader reader)
     {
     {
-        list.Unit ??= CreateDefaultUnit(list);
+        list.Unit ??= list.CreateDefaultUnit();
         list.Unit.ValuesFromXml(reader.Value);
         list.Unit.ValuesFromXml(reader.Value);
     }
     }
-
-    private ISvgUnit CreateDefaultUnit(SvgProperty property)
-    {
-        var genericType = property.GetType().GetGenericArguments();
-        if (genericType.Length == 0)
-        {
-            throw new InvalidOperationException("Property does not have a generic type");
-        }
-
-        ISvgUnit unit = Activator.CreateInstance(genericType[0]) as ISvgUnit;
-        if (unit == null)
-        {
-            throw new InvalidOperationException("Could not create unit");
-        }
-
-        return unit;
-    }
 }
 }

+ 14 - 12
src/PixiEditor.SVG/SvgParser.cs

@@ -1,4 +1,5 @@
-using System.Xml;
+using System.Globalization;
+using System.Xml;
 using System.Xml.Linq;
 using System.Xml.Linq;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.SVG.Elements;
 using PixiEditor.SVG.Elements;
@@ -19,7 +20,8 @@ public class SvgParser
         { "g", typeof(SvgGroup) },
         { "g", typeof(SvgGroup) },
         { "mask", typeof(SvgMask) },
         { "mask", typeof(SvgMask) },
         { "image", typeof(SvgImage) },
         { "image", typeof(SvgImage) },
-        { "svg", typeof(SvgDocument) }
+        { "svg", typeof(SvgDocument) },
+        { "text", typeof(SvgText) }
     };
     };
 
 
     public string Source { get; set; }
     public string Source { get; set; }
@@ -35,7 +37,7 @@ public class SvgParser
         using var reader = document.CreateReader();
         using var reader = document.CreateReader();
 
 
         XmlNodeType node = reader.MoveToContent();
         XmlNodeType node = reader.MoveToContent();
-        if (node != XmlNodeType.Element || reader.Name != "svg")
+        if (node != XmlNodeType.Element || reader.LocalName != "svg")
         {
         {
             return null;
             return null;
         }
         }
@@ -92,7 +94,7 @@ public class SvgParser
 
 
     private SvgElement? ParseElement(XmlReader reader)
     private SvgElement? ParseElement(XmlReader reader)
     {
     {
-        if (wellKnownElements.TryGetValue(reader.Name, out Type elementType))
+        if (wellKnownElements.TryGetValue(reader.LocalName, out Type elementType))
         {
         {
             SvgElement element = (SvgElement)Activator.CreateInstance(elementType);
             SvgElement element = (SvgElement)Activator.CreateInstance(elementType);
             if (reader.MoveToFirstAttribute())
             if (reader.MoveToFirstAttribute())
@@ -129,16 +131,16 @@ public class SvgParser
             string[] parts = viewBox.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
             string[] parts = viewBox.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
             if (parts.Length == 4)
             if (parts.Length == 4)
             {
             {
-                finalX = double.Parse(parts[0]);
-                finalY = double.Parse(parts[1]);
-                finalWidth = double.Parse(parts[2]);
-                finalHeight = double.Parse(parts[3]);
+                finalX = double.Parse(parts[0], CultureInfo.InvariantCulture);
+                finalY = double.Parse(parts[1], CultureInfo.InvariantCulture);
+                finalWidth = double.Parse(parts[2], CultureInfo.InvariantCulture);
+                finalHeight = double.Parse(parts[3], CultureInfo.InvariantCulture);
             }
             }
         }
         }
 
 
         if (x != null)
         if (x != null)
         {
         {
-            if (double.TryParse(x, out double xValue))
+            if (double.TryParse(x, CultureInfo.InvariantCulture, out double xValue))
             {
             {
                 finalX = xValue;
                 finalX = xValue;
             }
             }
@@ -146,7 +148,7 @@ public class SvgParser
 
 
         if (y != null)
         if (y != null)
         {
         {
-            if (double.TryParse(y, out double yValue))
+            if (double.TryParse(y, CultureInfo.InvariantCulture, out double yValue))
             {
             {
                 finalY = yValue;
                 finalY = yValue;
             }
             }
@@ -154,7 +156,7 @@ public class SvgParser
 
 
         if (width != null)
         if (width != null)
         {
         {
-            if (double.TryParse(width, out double widthValue))
+            if (double.TryParse(width, CultureInfo.InvariantCulture, out double widthValue))
             {
             {
                 finalWidth = widthValue;
                 finalWidth = widthValue;
             }
             }
@@ -162,7 +164,7 @@ public class SvgParser
 
 
         if (height != null)
         if (height != null)
         {
         {
-            if (double.TryParse(height, out double heightValue))
+            if (double.TryParse(height, CultureInfo.InvariantCulture, out double heightValue))
             {
             {
                 finalHeight = heightValue;
                 finalHeight = heightValue;
             }
             }

+ 20 - 3
src/PixiEditor.SVG/SvgProperty.cs

@@ -9,7 +9,7 @@ public abstract class SvgProperty
     {
     {
         SvgName = svgName;
         SvgName = svgName;
     }
     }
-    
+
     protected SvgProperty(string svgName, string? namespaceName) : this(svgName)
     protected SvgProperty(string svgName, string? namespaceName) : this(svgName)
     {
     {
         NamespaceName = namespaceName;
         NamespaceName = namespaceName;
@@ -18,6 +18,23 @@ public abstract class SvgProperty
     public string? NamespaceName { get; set; }
     public string? NamespaceName { get; set; }
     public string SvgName { get; set; }
     public string SvgName { get; set; }
     public ISvgUnit? Unit { get; set; }
     public ISvgUnit? Unit { get; set; }
+
+    public ISvgUnit? CreateDefaultUnit()
+    {
+        var genericType = this.GetType().GetGenericArguments();
+        if (genericType.Length == 0)
+        {
+            return null;
+        }
+
+        ISvgUnit unit = Activator.CreateInstance(genericType[0]) as ISvgUnit;
+        if (unit == null)
+        {
+            throw new InvalidOperationException("Could not create unit");
+        }
+
+        return unit;
+    }
 }
 }
 
 
 public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
 public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
@@ -27,11 +44,11 @@ public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
         get => (T?)base.Unit;
         get => (T?)base.Unit;
         set => base.Unit = value;
         set => base.Unit = value;
     }
     }
-    
+
     public SvgProperty(string svgName) : base(svgName)
     public SvgProperty(string svgName) : base(svgName)
     {
     {
     }
     }
-    
+
     public SvgProperty(string svgName, string? namespaceName) : base(svgName, namespaceName)
     public SvgProperty(string svgName, string? namespaceName) : base(svgName, namespaceName)
     {
     {
     }
     }

+ 35 - 15
src/PixiEditor.SVG/Units/SvgNumericUnit.cs

@@ -6,32 +6,51 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
 {
 {
     public string PostFix { get; set; } = postFix;
     public string PostFix { get; set; } = postFix;
     public double Value { get; set; } = value;
     public double Value { get; set; } = value;
+    public double? PixelsValue => ConvertTo(SvgNumericUnits.Px);
+
+    public double? ConvertTo(SvgNumericUnits other)
+    {
+        SvgNumericUnits? numericUnit = SvgNumericUnitsExtensions.TryParseUnit(PostFix);
+
+        if (numericUnit == null || !numericUnit.Value.IsSizeUnit() || !numericUnit.Value.IsAbsoluteUnit())
+        {
+            return null;
+        }
+
+        double? pixelsValue = SvgNumericConverter.ToPixels(Value, numericUnit.Value);
+        if (pixelsValue == null)
+        {
+            return null;
+        }
+
+        return SvgNumericConverter.FromPixels(pixelsValue.Value, other);
+    }
 
 
     public static SvgNumericUnit FromUserUnits(double value)
     public static SvgNumericUnit FromUserUnits(double value)
     {
     {
         return new SvgNumericUnit(value, string.Empty);
         return new SvgNumericUnit(value, string.Empty);
     }
     }
-    
+
     public static SvgNumericUnit FromPixels(double value)
     public static SvgNumericUnit FromPixels(double value)
     {
     {
         return new SvgNumericUnit(value, "px");
         return new SvgNumericUnit(value, "px");
     }
     }
-    
+
     public static SvgNumericUnit FromInches(double value)
     public static SvgNumericUnit FromInches(double value)
     {
     {
         return new SvgNumericUnit(value, "in");
         return new SvgNumericUnit(value, "in");
     }
     }
-    
+
     public static SvgNumericUnit FromCentimeters(double value)
     public static SvgNumericUnit FromCentimeters(double value)
     {
     {
         return new SvgNumericUnit(value, "cm");
         return new SvgNumericUnit(value, "cm");
     }
     }
-    
+
     public static SvgNumericUnit FromMillimeters(double value)
     public static SvgNumericUnit FromMillimeters(double value)
     {
     {
         return new SvgNumericUnit(value, "mm");
         return new SvgNumericUnit(value, "mm");
     }
     }
-    
+
     public static SvgNumericUnit FromPercent(double value)
     public static SvgNumericUnit FromPercent(double value)
     {
     {
         return new SvgNumericUnit(value, "%");
         return new SvgNumericUnit(value, "%");
@@ -46,7 +65,7 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
     public void ValuesFromXml(string readerValue)
     public void ValuesFromXml(string readerValue)
     {
     {
         string? extractedPostFix = ExtractPostFix(readerValue);
         string? extractedPostFix = ExtractPostFix(readerValue);
-        
+
         if (extractedPostFix == null)
         if (extractedPostFix == null)
         {
         {
             if (double.TryParse(readerValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
             if (double.TryParse(readerValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
@@ -65,7 +84,7 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
             }
             }
         }
         }
     }
     }
-    
+
     private string? ExtractPostFix(string readerValue)
     private string? ExtractPostFix(string readerValue)
     {
     {
         if (readerValue.Length == 0)
         if (readerValue.Length == 0)
@@ -74,21 +93,22 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
         }
         }
 
 
         int postFixStartIndex = readerValue.Length;
         int postFixStartIndex = readerValue.Length;
-        
+
+        if (char.IsDigit(readerValue[^1]))
+        {
+            return null;
+        }
+
         for (int i = readerValue.Length - 1; i >= 0; i--)
         for (int i = readerValue.Length - 1; i >= 0; i--)
         {
         {
-            if (!char.IsDigit(readerValue[i]))
+            if (char.IsDigit(readerValue[i]))
             {
             {
                 postFixStartIndex = i + 1;
                 postFixStartIndex = i + 1;
                 break;
                 break;
             }
             }
         }
         }
-        
-        if (postFixStartIndex == readerValue.Length)
-        {
-            return null;
-        }
-        
+
+
         return readerValue.Substring(postFixStartIndex);
         return readerValue.Substring(postFixStartIndex);
     }
     }
 }
 }

+ 142 - 0
src/PixiEditor.SVG/Units/SvgNumericUnits.cs

@@ -0,0 +1,142 @@
+namespace PixiEditor.SVG.Units;
+
+public enum SvgNumericUnits
+{
+    Px,
+    In,
+    Cm,
+    Mm,
+    Pt,
+    Pc,
+    Em,
+    Ex,
+    Ch,
+    Rem,
+    Vw,
+    Vh,
+    Vmin,
+    Vmax,
+    Percent,
+    Deg,
+    Rad,
+    Grad,
+    Turn,
+    S,
+    Ms,
+    Min,
+    H,
+    Mmss,
+    Hhmmss,
+}
+
+public static class SvgNumericConverter
+{
+    public static double? ToPixels(double value, SvgNumericUnits unit)
+    {
+        if (!unit.IsAbsoluteUnit() && !unit.IsSizeUnit()) return null;
+
+        return unit switch
+        {
+            SvgNumericUnits.Px => value,
+            SvgNumericUnits.In => value * 96,
+            SvgNumericUnits.Cm => value * 37.795,
+            SvgNumericUnits.Mm => value * 3.7795,
+            SvgNumericUnits.Pt => value * 1.3333,
+            SvgNumericUnits.Pc => value * 16,
+            _ => null,
+        };
+    }
+
+    public static double? FromPixels(double pixelsValue, SvgNumericUnits other)
+    {
+        if (!other.IsAbsoluteUnit() && !other.IsSizeUnit()) return null;
+
+        return other switch
+        {
+            SvgNumericUnits.Px => pixelsValue,
+            SvgNumericUnits.In => pixelsValue / 96,
+            SvgNumericUnits.Cm => pixelsValue / 37.795,
+            SvgNumericUnits.Mm => pixelsValue / 3.7795,
+            SvgNumericUnits.Pt => pixelsValue / 1.3333,
+            SvgNumericUnits.Pc => pixelsValue / 16,
+            _ => null,
+        };
+    }
+}
+
+public static class SvgNumericUnitsExtensions
+{
+    public static bool IsSizeUnit(this SvgNumericUnits unit)
+    {
+        return unit switch
+        {
+            SvgNumericUnits.Px => true,
+            SvgNumericUnits.In => true,
+            SvgNumericUnits.Cm => true,
+            SvgNumericUnits.Mm => true,
+            SvgNumericUnits.Pt => true,
+            SvgNumericUnits.Pc => true,
+            SvgNumericUnits.Em => true,
+            SvgNumericUnits.Ex => true,
+            SvgNumericUnits.Ch => true,
+            SvgNumericUnits.Rem => true,
+            SvgNumericUnits.Vw => true,
+            SvgNumericUnits.Vh => true,
+            SvgNumericUnits.Vmin => true,
+            SvgNumericUnits.Vmax => true,
+            SvgNumericUnits.Percent => true,
+            _ => false,
+        };
+    }
+
+    public static bool IsAbsoluteUnit(this SvgNumericUnits unit)
+    {
+        return unit switch
+        {
+            SvgNumericUnits.Px => true,
+            SvgNumericUnits.In => true,
+            SvgNumericUnits.Cm => true,
+            SvgNumericUnits.Mm => true,
+            SvgNumericUnits.Pt => true,
+            SvgNumericUnits.Pc => true,
+            SvgNumericUnits.Rad => true,
+            SvgNumericUnits.Deg => true,
+            SvgNumericUnits.Grad => true,
+            _ => false,
+        };
+    }
+
+    public static SvgNumericUnits? TryParseUnit(string postFix)
+    {
+        if (string.IsNullOrWhiteSpace(postFix)) return SvgNumericUnits.Px;
+        return postFix.ToLower().Trim() switch
+        {
+            "px" => SvgNumericUnits.Px,
+            "in" => SvgNumericUnits.In,
+            "cm" => SvgNumericUnits.Cm,
+            "mm" => SvgNumericUnits.Mm,
+            "pt" => SvgNumericUnits.Pt,
+            "pc" => SvgNumericUnits.Pc,
+            "em" => SvgNumericUnits.Em,
+            "ex" => SvgNumericUnits.Ex,
+            "ch" => SvgNumericUnits.Ch,
+            "rem" => SvgNumericUnits.Rem,
+            "vw" => SvgNumericUnits.Vw,
+            "vh" => SvgNumericUnits.Vh,
+            "vmin" => SvgNumericUnits.Vmin,
+            "vmax" => SvgNumericUnits.Vmax,
+            "%" => SvgNumericUnits.Percent,
+            "deg" => SvgNumericUnits.Deg,
+            "rad" => SvgNumericUnits.Rad,
+            "grad" => SvgNumericUnits.Grad,
+            "turn" => SvgNumericUnits.Turn,
+            "s" => SvgNumericUnits.S,
+            "ms" => SvgNumericUnits.Ms,
+            "min" => SvgNumericUnits.Min,
+            "h" => SvgNumericUnits.H,
+            "mm:ss" => SvgNumericUnits.Mmss,
+            "hh:mm:ss" => SvgNumericUnits.Hhmmss,
+            _ => null,
+        };
+    }
+}

+ 73 - 0
src/PixiEditor.SVG/Units/SvgStyleUnit.cs

@@ -0,0 +1,73 @@
+namespace PixiEditor.SVG.Units;
+
+public struct SvgStyleUnit : ISvgUnit
+{
+    private Dictionary<string, string> inlineDefinedProperties;
+    private string value;
+
+    public SvgStyleUnit(string inlineStyle)
+    {
+        Value = inlineStyle;
+    }
+
+    public string Value
+    {
+        get => value;
+        set
+        {
+            this.value = value;
+            inlineDefinedProperties = new Dictionary<string, string>();
+
+            if (string.IsNullOrEmpty(value))
+            {
+                return;
+            }
+
+            string[] properties = value.Split(';');
+            foreach (string property in properties)
+            {
+                string[] keyValue = property.Split(':');
+                if (keyValue.Length == 2)
+                {
+                    inlineDefinedProperties.Add(keyValue[0].Trim(), keyValue[1].Trim());
+                }
+            }
+        }
+    }
+
+    public string ToXml()
+    {
+        return Value;
+    }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        Value = readerValue;
+    }
+
+    public TProp TryGetStyleFor<TProp, TUnit>(string property) where TProp : SvgProperty<TUnit> where TUnit : struct, ISvgUnit
+    {
+        if (inlineDefinedProperties.TryGetValue(property, out var definedProperty))
+        {
+            TProp prop = (TProp)Activator.CreateInstance(typeof(TProp), property);
+            var unit = (TUnit)prop.CreateDefaultUnit();
+            unit.ValuesFromXml(definedProperty);
+            prop.Unit = unit;
+
+            return prop;
+        }
+
+        return null;
+    }
+
+    public SvgStyleUnit MergeWith(SvgStyleUnit elementStyleUnit)
+    {
+        Dictionary<string, string> props = new(inlineDefinedProperties);
+        foreach (var inlineDefined in elementStyleUnit.inlineDefinedProperties)
+        {
+            props[inlineDefined.Key] = inlineDefined.Value;
+        }
+
+        return new SvgStyleUnit(string.Join(";", props.Select(x => $"{x.Key}:{x.Value}")));
+    }
+}

+ 98 - 4
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -11,12 +11,12 @@ public struct SvgTransformUnit : ISvgUnit
     }
     }
 
 
     public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
     public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
-    
+
     public SvgTransformUnit(Matrix3X3 matrixValue)
     public SvgTransformUnit(Matrix3X3 matrixValue)
     {
     {
         MatrixValue = matrixValue;
         MatrixValue = matrixValue;
     }
     }
-    
+
     public string ToXml()
     public string ToXml()
     {
     {
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
@@ -25,7 +25,7 @@ public struct SvgTransformUnit : ISvgUnit
         string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
         string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
         string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
         string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
         string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
         string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
-        
+
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
     }
     }
 
 
@@ -49,7 +49,101 @@ public struct SvgTransformUnit : ISvgUnit
         }
         }
         else
         else
         {
         {
-            // todo: parse other types of transformation syntax (rotate, translate, scale etc)
+            MatrixValue = TryParseDescriptiveTransform(readerValue);
+        }
+    }
+
+    private Matrix3X3 TryParseDescriptiveTransform(string readerValue)
+    {
+        if (!readerValue.Contains('(') || !readerValue.Contains(')'))
+        {
+            return Matrix3X3.Identity;
+        }
+
+        string[] parts = readerValue.Split(')').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+        Matrix3X3 result = Matrix3X3.Identity;
+        for (int i = 0; i < parts.Length; i++)
+        {
+            string[] part = parts[i].Split('(');
+            if (part.Length != 2)
+            {
+                continue;
+            }
+
+            result = result.Concat(ParsePart(part));
+        }
+
+        return result;
+    }
+
+    private static Matrix3X3 ParsePart(string[] part)
+    {
+        string transformType = part[0].Trim();
+        string[] values = part[1].Split(' ', ')', ',').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+        if (values.Length == 0)
+        {
+            return Matrix3X3.Identity;
+        }
+
+        if (transformType == "translate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float translateX))
+            {
+                float translateY = translateX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out translateY);
+                }
+
+                return Matrix3X3.CreateTranslation(translateX, translateY);
+            }
+        }
+        else if (transformType == "scale")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float scaleX))
+            {
+                float scaleY = scaleX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out scaleY);
+                }
+
+                return Matrix3X3.CreateScale(scaleX, scaleY);
+            }
         }
         }
+        else if (transformType == "rotate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float angle))
+            {
+                float radians = angle * (float)Math.PI / 180;
+
+                if (values.Length > 2)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerX);
+                    float.TryParse(values[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerY);
+                    return Matrix3X3.CreateRotation(radians, centerX, centerY);
+                }
+
+                return Matrix3X3.CreateRotation(radians);
+            }
+        }
+        else if (transformType == "skewX")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewX))
+            {
+                return Matrix3X3.CreateSkew(skewX, 0);
+            }
+        }
+        else if (transformType == "skewY")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewY))
+            {
+                return Matrix3X3.CreateSkew(0, skewY);
+            }
+        }
+
+        return Matrix3X3.Identity;
     }
     }
 }
 }

+ 8 - 2
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -54,12 +54,16 @@
             <Color x:Key="VecDSocketColor">#c984ca</Color>
             <Color x:Key="VecDSocketColor">#c984ca</Color>
             <Color x:Key="VecISocketColor">#c9b4ca</Color>
             <Color x:Key="VecISocketColor">#c9b4ca</Color>
             <Color x:Key="IntSocketColor">#4C64B1</Color>
             <Color x:Key="IntSocketColor">#4C64B1</Color>
+            <Color x:Key="StringSocketColor">#C9E4C6</Color>
             <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
             <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
             <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
             <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
+            <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
             <GradientStops x:Key="ShapeDataSocketGradient">
             <GradientStops x:Key="ShapeDataSocketGradient">
                 <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
                 <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
-                <GradientStop Offset="0.5" Color="{StaticResource EllipseDataSocketColor}"/>
-                <GradientStop Offset="0.5" Color="{StaticResource PointsDataSocketColor}"/>
+                <GradientStop Offset="0.33" Color="{StaticResource EllipseDataSocketColor}"/>
+                <GradientStop Offset="0.33" Color="{StaticResource TextDataSocketColor}"/>
+                <GradientStop Offset="0.66" Color="{StaticResource TextDataSocketColor}"/>
+                <GradientStop Offset="0.66" Color="{StaticResource PointsDataSocketColor}"/>
                 <GradientStop Offset="1" Color="{StaticResource PointsDataSocketColor}"/>
                 <GradientStop Offset="1" Color="{StaticResource PointsDataSocketColor}"/>
             </GradientStops>
             </GradientStops>
             
             
@@ -142,9 +146,11 @@
             <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}"/>
             <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}"/>
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
+            <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}"/>
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
             <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
             <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
             <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
             <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
+            <SolidColorBrush x:Key="TextVectorDataSocketBrush" Color="{StaticResource TextDataSocketColor}"/>
             
             
             <!-- Zones & Frames -->
             <!-- Zones & Frames -->
             <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
             <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>

BIN=BIN
src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf


+ 49 - 29
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -1,10 +1,10 @@
 <Styles xmlns="https://github.com/avaloniaui"
 <Styles xmlns="https://github.com/avaloniaui"
-                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-                    xmlns:system="clr-namespace:System;assembly=System.Runtime">
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime">
     <Styles.Resources>
     <Styles.Resources>
         <ResourceDictionary>
         <ResourceDictionary>
-            <FontFamily x:Key="PixiPerfectIcons">avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf#PixiPerfect</FontFamily>
-            
+            <FontFamily x:Key="PixiPerfectIcons">avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf#pixiperfect</FontFamily>
+
             <system:String x:Key="icon-add-reference">&#xE900;</system:String>
             <system:String x:Key="icon-add-reference">&#xE900;</system:String>
             <system:String x:Key="icon-add-to-mask">&#xE901;</system:String>
             <system:String x:Key="icon-add-to-mask">&#xE901;</system:String>
             <system:String x:Key="icon-alpha-lock">&#xE902;</system:String>
             <system:String x:Key="icon-alpha-lock">&#xE902;</system:String>
@@ -110,12 +110,13 @@
             <system:String x:Key="icon-zoom-out">&#xE962;</system:String>
             <system:String x:Key="icon-zoom-out">&#xE962;</system:String>
             <system:String x:Key="icon-pen">&#xE971;</system:String>
             <system:String x:Key="icon-pen">&#xE971;</system:String>
             <system:String x:Key="icon-link">&#xE96B;</system:String>
             <system:String x:Key="icon-link">&#xE96B;</system:String>
+            <system:String x:Key="icon-upload">&#xE96D;</system:String>
             <system:String x:Key="icon-search">&#xE976;</system:String>
             <system:String x:Key="icon-search">&#xE976;</system:String>
             <system:String x:Key="icon-hard-drive">&#xE96C;</system:String>
             <system:String x:Key="icon-hard-drive">&#xE96C;</system:String>
             <system:String x:Key="icon-copy-add">&#xe921;</system:String>
             <system:String x:Key="icon-copy-add">&#xe921;</system:String>
             <system:String x:Key="icon-check">&#xe96a;</system:String>
             <system:String x:Key="icon-check">&#xe96a;</system:String>
             <system:String x:Key="icon-dot">&#xe963;</system:String>
             <system:String x:Key="icon-dot">&#xe963;</system:String>
-            
+
             <system:String x:Key="icon-paste-reference-layer">&#xe977;</system:String>
             <system:String x:Key="icon-paste-reference-layer">&#xe977;</system:String>
             <system:String x:Key="icon-paste-as-new-layer">&#xe978;</system:String>
             <system:String x:Key="icon-paste-as-new-layer">&#xe978;</system:String>
             <system:String x:Key="icon-star">&#xe97c;</system:String>
             <system:String x:Key="icon-star">&#xe97c;</system:String>
@@ -132,46 +133,65 @@
             <system:String x:Key="icon-swatches">&#xE982;</system:String>
             <system:String x:Key="icon-swatches">&#xE982;</system:String>
             <system:String x:Key="icon-nodes">&#xe984;</system:String>
             <system:String x:Key="icon-nodes">&#xe984;</system:String>
             <system:String x:Key="icon-onion">&#xe985;</system:String>
             <system:String x:Key="icon-onion">&#xe985;</system:String>
-            
+
             <system:String x:Key="icon-lowres-circle">&#xe986;</system:String>
             <system:String x:Key="icon-lowres-circle">&#xe986;</system:String>
             <system:String x:Key="icon-snapping">&#xe987;</system:String>
             <system:String x:Key="icon-snapping">&#xe987;</system:String>
             <system:String x:Key="icon-lowres-square">&#xe988;</system:String>
             <system:String x:Key="icon-lowres-square">&#xe988;</system:String>
             <system:String x:Key="icon-lowres-line">&#xe989;</system:String>
             <system:String x:Key="icon-lowres-line">&#xe989;</system:String>
+
+            <system:String x:Key="icon-align-center">&#xE98C;</system:String>
+            <system:String x:Key="icon-bold">&#xE98D;</system:String>
+            <system:String x:Key="icon-text-antialiased">&#xE98E;</system:String>
+            <system:String x:Key="icon-italic">&#xE98F;</system:String>
+            <system:String x:Key="icon-align-stretch">&#xE990;</system:String>
+            <system:String x:Key="icon-align-left">&#xE991;</system:String>
+            <system:String x:Key="icon-letter-spacing">&#xE992;</system:String>
+            <system:String x:Key="icon-line-height">&#xE993;</system:String>
+            <system:String x:Key="icon-text-pixel">&#xE994;</system:String>
+            <system:String x:Key="icon-align-right">&#xE995;</system:String>
+            <system:String x:Key="icon-strikethrough">&#xE996;</system:String>
+            <system:String x:Key="icon-linked-pipette">&#xE997;</system:String>
+            <system:String x:Key="icon-text-underline">&#xE998;</system:String>
+            <system:String x:Key="icon-text-round">&#xE999;</system:String>
         </ResourceDictionary>
         </ResourceDictionary>
     </Styles.Resources>
     </Styles.Resources>
-    
+
     <Style Selector="TextBlock.pixi-icon">
     <Style Selector="TextBlock.pixi-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}"/>
+        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}" />
     </Style>
     </Style>
-    
+
+    <Style Selector="Label.pixi-icon">
+        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}" />
+    </Style>
+
     <Style Selector="Run.pixi-icon">
     <Style Selector="Run.pixi-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}"/>
+        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}" />
     </Style>
     </Style>
-    
+
     <Style Selector="Button.pixi-icon">
     <Style Selector="Button.pixi-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}"/>
-        <Setter Property="Background" Value="Transparent"/>
-        <Setter Property="FontSize" Value="24"/>
-        <Setter Property="Padding" Value="0"/>
-        <Setter Property="HorizontalContentAlignment" Value="Center"/>
-        <Setter Property="VerticalContentAlignment" Value="Center"/>
+        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}" />
+        <Setter Property="Background" Value="Transparent" />
+        <Setter Property="FontSize" Value="24" />
+        <Setter Property="Padding" Value="0" />
+        <Setter Property="HorizontalContentAlignment" Value="Center" />
+        <Setter Property="VerticalContentAlignment" Value="Center" />
     </Style>
     </Style>
-    
+
     <Style Selector="Button.pixi-icon:pointerover">
     <Style Selector="Button.pixi-icon:pointerover">
-        <Setter Property="Background" Value="Transparent"/>
+        <Setter Property="Background" Value="Transparent" />
     </Style>
     </Style>
-    
+
     <Style Selector="ToggleButton.pixi-icon">
     <Style Selector="ToggleButton.pixi-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}"/>
-        <Setter Property="BorderThickness" Value="0"/>
-        <Setter Property="Background" Value="Transparent"/>
-        <Setter Property="FontSize" Value="24"/>
-        <Setter Property="Padding" Value="0"/>
-        <Setter Property="HorizontalContentAlignment" Value="Center"/>
-        <Setter Property="VerticalContentAlignment" Value="Center"/>
+        <Setter Property="FontFamily" Value="{DynamicResource PixiPerfectIcons}" />
+        <Setter Property="BorderThickness" Value="0" />
+        <Setter Property="Background" Value="Transparent" />
+        <Setter Property="FontSize" Value="24" />
+        <Setter Property="Padding" Value="0" />
+        <Setter Property="HorizontalContentAlignment" Value="Center" />
+        <Setter Property="VerticalContentAlignment" Value="Center" />
     </Style>
     </Style>
-    
+
     <Style Selector="ToggleButton.pixi-icon:checked">
     <Style Selector="ToggleButton.pixi-icon:checked">
-        <Setter Property="Background" Value="{DynamicResource ThemeControlHighBrush}"/>
+        <Setter Property="Background" Value="{DynamicResource ThemeControlHighBrush}" />
     </Style>
     </Style>
 </Styles>
 </Styles>

+ 16 - 1
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -128,7 +128,7 @@ public static class PixiPerfectIcons
     public const string Nodes = "\uE984";
     public const string Nodes = "\uE984";
     public const string Onion = "\uE965";
     public const string Onion = "\uE965";
         
         
-    public const string Reset = "R"; // TODO: Create a reset icon
+    public const string Reset = "\uE98A";
     public const string ToggleLayerVisible = "\u25a1;"; // TODO: Create a toggle layer visible icon
     public const string ToggleLayerVisible = "\u25a1;"; // TODO: Create a toggle layer visible icon
     public const string ToggleMask = "\u25a1;"; // TODO: Create a toggle mask icon
     public const string ToggleMask = "\u25a1;"; // TODO: Create a toggle mask icon
     public const string Pen = "\uE971";
     public const string Pen = "\uE971";
@@ -137,6 +137,21 @@ public static class PixiPerfectIcons
     public const string LowResSquare = "\uE988";
     public const string LowResSquare = "\uE988";
     public const string LowResLine = "\uE989";
     public const string LowResLine = "\uE989";
 
 
+    public const string AlignCenter = "\uE98C";
+    public const string Bold = "\uE98D";
+    public const string TextAntialiased = "\uE98E";
+    public const string Italic = "\uE98F";
+    public const string AlignStretch = "\uE990";
+    public const string AlignLeft = "\uE991";
+    public const string LetterSpacing = "\uE992";
+    public const string LineHeight = "\uE993";
+    public const string TextPixel = "\uE994";
+    public const string AlignRight = "\uE995";
+    public const string Strikethrough = "\uE996";
+    public const string LinkedPipette = "\uE997";
+    public const string TextUnderline = "\uE998";
+    public const string TextRound = "\uE999";
+
     public static Stream GetFontStream()
     public static Stream GetFontStream()
     {
     {
         return AssetLoader.Open(new Uri("avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf"));
         return AssetLoader.Open(new Uri("avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf"));

+ 0 - 1
src/PixiEditor/App.axaml

@@ -21,7 +21,6 @@
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml" />
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml" />
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml" />
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml" />
         <StyleInclude Source="/Styles/Buttons/CaptionButtonsStyle.axaml" />
         <StyleInclude Source="/Styles/Buttons/CaptionButtonsStyle.axaml" />
-        <StyleInclude Source="/Styles/NodeIcons.axaml"/>
     </Application.Styles>
     </Application.Styles>
     <Application.Resources>
     <Application.Resources>
         <ResourceDictionary>
         <ResourceDictionary>

+ 26 - 3
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -19,6 +19,14 @@
       "RasterLine",
       "RasterLine",
       "RasterEllipse",
       "RasterEllipse",
       "RasterRectangle",
       "RasterRectangle",
+      {
+        "ToolName": "Text",
+        "Settings": {
+          "AntiAliasing": false,
+          "ForceLowDpiRendering": true
+        },
+        "Icon": "\ue994"
+      },
       {
       {
         "ToolName": "Eraser",
         "ToolName": "Eraser",
         "Settings": {
         "Settings": {
@@ -42,7 +50,7 @@
           "AntiAliasing": true,
           "AntiAliasing": true,
           "ExposeHardness": true,
           "ExposeHardness": true,
           "ExposeSpacing": true,
           "ExposeSpacing": true,
-          "BrushShapeSetting": "CircleSmooth" 
+          "BrushShapeSetting": "CircleSmooth"
         }
         }
       },
       },
       "Select",
       "Select",
@@ -78,7 +86,15 @@
         "Settings": {
         "Settings": {
           "AntiAliasing": true
           "AntiAliasing": true
         },
         },
-        "Icon": "\uE953" 
+        "Icon": "\uE953"
+      },
+      {
+        "ToolName": "Text",
+        "Settings": {
+          "AntiAliasing": true,
+          "ForceLowDpiRendering": true
+        },
+        "Icon": "\ue98E"
       },
       },
       {
       {
         "ToolName": "Eraser",
         "ToolName": "Eraser",
@@ -102,7 +118,14 @@
       "VectorPath",
       "VectorPath",
       "VectorLine",
       "VectorLine",
       "VectorEllipse",
       "VectorEllipse",
-      "VectorRectangle"
+      "VectorRectangle",
+      {
+        "ToolName": "Text",
+        "Settings": {
+          "AntiAliasing": true,
+          "ForceLowDpiRendering": false
+        }
+      }
     ]
     ]
   }
   }
 ]
 ]

+ 24 - 3
src/PixiEditor/Data/Localization/Languages/en.json

@@ -282,7 +282,7 @@
   "EDITOR_DATA": "Editor data (Local)",
   "EDITOR_DATA": "Editor data (Local)",
   "MOVE_VIEWPORT_TOOLTIP": "Moves viewport. ({0})",
   "MOVE_VIEWPORT_TOOLTIP": "Moves viewport. ({0})",
   "MOVE_VIEWPORT_ACTION_DISPLAY": "Click and move to pan the viewport",
   "MOVE_VIEWPORT_ACTION_DISPLAY": "Click and move to pan the viewport",
-  "MOVE_TOOL_TOOLTIP": "Moves selected pixels ({0}). Hold Ctrl to move all layers.",
+  "MOVE_TOOL_TOOLTIP": "Select and transform layers ({0}).",
   "MOVE_TOOL_ACTION_DISPLAY": "Hold mouse to move selected pixels. Hold Ctrl to move all layers.",
   "MOVE_TOOL_ACTION_DISPLAY": "Hold mouse to move selected pixels. Hold Ctrl to move all layers.",
   "PEN_TOOL_TOOLTIP": "Pen. ({0})",
   "PEN_TOOL_TOOLTIP": "Pen. ({0})",
   "PEN_TOOL_ACTION_DISPLAY": "Click and move to draw.",
   "PEN_TOOL_ACTION_DISPLAY": "Click and move to draw.",
@@ -799,7 +799,7 @@
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "FILL_MODE": "Fill mode",
   "FILL_MODE": "Fill mode",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
-  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using legacy blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using sRGB blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
   "FILL_TYPE_WINDING": "Winding",
   "FILL_TYPE_WINDING": "Winding",
   "FILL_TYPE_EVEN_ODD": "Even Odd",
   "FILL_TYPE_EVEN_ODD": "Even Odd",
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
@@ -834,5 +834,26 @@
   "BLUR_FILTER_NODE": "Gaussian Blur Filter",
   "BLUR_FILTER_NODE": "Gaussian Blur Filter",
   "LENGTH": "Length",
   "LENGTH": "Length",
   "GREATER_THAN_OR_EQUAL": "Greater than or equal",
   "GREATER_THAN_OR_EQUAL": "Greater than or equal",
-  "COLOR_NODE": "Color"
+  "COLOR_NODE": "Color",
+  "CONVERT_TO_CURVE": "Convert to curve",
+  "CONVERT_TO_CURVE_DESCRIPTIVE": "Convert selected vector layer to a curve/path",
+  "FONT_FILES": "Font Files",
+  "UNIT_PT": "pt",
+  "FONT_LABEL": "Family",
+  "FONT_SIZE_LABEL": "Size",
+  "SPACING_LABEL": "Spacing",
+  "TEXT_TOOL": "Text",
+  "MISSING_FONT": "Missing font",
+  "TEXT_LAYER_NAME": "Text",
+  "TEXT_TOOL_TOOLTIP": "Create text ({0}).",
+  "BOLD_TOOLTIP": "Bold",
+  "ITALIC_TOOLTIP": "Italic",
+  "CUSTOM_FONT": "Custom font",
+  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics",
+  "USE_SRGB_PROCESSING": "Use sRGB for processing colors",
+  "USE_SRGB_PROCESSING_DESC": "Convert document using linear sRGB to sRGB for processing colors. This will affect the colors of the document.",
+  "TEXT_NODE": "Text",
+  "TEXT_LABEL": "Text",
+  "TEXT_ON_PATH_NODE": "Text on Path",
+  "HIGH_DPI_RENDERING": "High DPI Rendering"
 }
 }

+ 18 - 0
src/PixiEditor/Helpers/Converters/FontFamilyNameToAvaloniaFontFamily.cs

@@ -0,0 +1,18 @@
+using System.Globalization;
+using Avalonia.Media;
+using Drawie.Backend.Core.Text;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class FontFamilyNameToAvaloniaFontFamily : SingleInstanceConverter<FontFamilyNameToAvaloniaFontFamily>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is FontFamilyName familyName)
+        {
+            return new FontFamily(familyName.Name);
+        }
+
+        return value;
+    }
+}

+ 19 - 9
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -33,8 +33,9 @@ internal class DocumentViewModelBuilder
 
 
     public NodeGraphBuilder Graph { get; set; }
     public NodeGraphBuilder Graph { get; set; }
     public string ImageEncoderUsed { get; set; } = "QOI";
     public string ImageEncoderUsed { get; set; } = "QOI";
-    public bool UsesLegacyColorBlending { get; set; } = false;
+    public bool UsesSrgbColorBlending { get; set; } = false;
     public Version? PixiParserVersionUsed { get; set; }
     public Version? PixiParserVersionUsed { get; set; }
+    public ResourceStorage DocumentResources { get; set; }
 
 
     public DocumentViewModelBuilder WithSize(int width, int height)
     public DocumentViewModelBuilder WithSize(int width, int height)
     {
     {
@@ -126,9 +127,9 @@ internal class DocumentViewModelBuilder
         return this;
         return this;
     }
     }
 
 
-    public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
+    public DocumentViewModelBuilder? WithSrgbColorBlending(bool usesLegacyColorBlending)
     {
     {
-        UsesLegacyColorBlending = usesLegacyColorBlending;
+        UsesSrgbColorBlending = usesLegacyColorBlending;
         return this;
         return this;
     }
     }
 
 
@@ -148,11 +149,12 @@ internal class DocumentViewModelBuilder
 
 
             data?.Add(builder);
             data?.Add(builder);
         }
         }
-        
+
         TryAddMissingKeyFrames(root, data, documentGraph);
         TryAddMissingKeyFrames(root, data, documentGraph);
     }
     }
 
 
-    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data, NodeGraph documentGraph)
+    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data,
+        NodeGraph documentGraph)
     {
     {
         if (data == null)
         if (data == null)
         {
         {
@@ -164,15 +166,15 @@ internal class DocumentViewModelBuilder
             if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
             if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
             {
             {
                 GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
                 GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
-                .WithNodeId(node.Id);
-                
+                    .WithNodeId(node.Id);
+
                 foreach (var keyFrame in node.KeyFrames)
                 foreach (var keyFrame in node.KeyFrames)
                 {
                 {
                     builder.WithChild<KeyFrameBuilder>(x => x
                     builder.WithChild<KeyFrameBuilder>(x => x
                         .WithKeyFrameId(keyFrame.Id)
                         .WithKeyFrameId(keyFrame.Id)
                         .WithNodeId(node.Id));
                         .WithNodeId(node.Id));
-                }   
-                
+                }
+
                 data.Add(builder);
                 data.Add(builder);
             }
             }
         }
         }
@@ -243,6 +245,12 @@ internal class DocumentViewModelBuilder
         PixiParserVersionUsed = version;
         PixiParserVersionUsed = version;
         return this;
         return this;
     }
     }
+
+    public DocumentViewModelBuilder WithResources(ResourceStorage documentResources)
+    {
+        DocumentResources = documentResources;
+        return this;
+    }
 }
 }
 
 
 internal class AnimationDataBuilder
 internal class AnimationDataBuilder
@@ -351,6 +359,7 @@ internal class NodeGraphBuilder
     {
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             .WithKeyFrames(
             [
             [
@@ -372,6 +381,7 @@ internal class NodeGraphBuilder
     {
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
             .WithName(name)
+            .WithPosition(new Vector2 { X = -250, Y = 0 })
             .WithId(AllNodes.Count)
             .WithId(AllNodes.Count)
             .WithKeyFrames(
             .WithKeyFrames(
             [
             [

+ 4 - 2
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core;
 using Drawie.Backend.Core;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.IO;
 using PixiEditor.Parser;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Graph;
 using PixiEditor.Parser.Skia;
 using PixiEditor.Parser.Skia;
@@ -31,14 +32,15 @@ internal static class PixiParserDocumentEx
         return DocumentViewModel.Build(b => b
         return DocumentViewModel.Build(b => b
             .WithPixiParserVersion(document.Version)
             .WithPixiParserVersion(document.Version)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
-            .WithLegacyColorBlending(document.LegacyColorBlending)
+            .WithSrgbColorBlending(document.SrgbColorBlending)
             .WithSize(document.Width, document.Height)
             .WithSize(document.Width, document.Height)
             .WithImageEncoder(document.ImageEncoderUsed)
             .WithImageEncoder(document.ImageEncoderUsed)
             .WithPalette(document.Palette, color => new PaletteColor(color.R, color.G, color.B))
             .WithPalette(document.Palette, color => new PaletteColor(color.R, color.G, color.B))
             .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
             .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
             .WithReferenceLayer(document.ReferenceLayer, BuildReferenceLayer, encoder)
             .WithReferenceLayer(document.ReferenceLayer, BuildReferenceLayer, encoder)
             .WithGraph(document.Graph, BuildGraph)
             .WithGraph(document.Graph, BuildGraph)
-            .WithAnimationData(document.AnimationData, document.Graph));
+            .WithAnimationData(document.AnimationData, document.Graph)
+            .WithResources(document.Resources));
     }
     }
 
 
     private static void BuildGraph(NodeGraph graph, NodeGraphBuilder graphBuilder)
     private static void BuildGraph(NodeGraph graph, NodeGraphBuilder graphBuilder)

+ 4 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -100,6 +100,7 @@ internal static class ServiceCollectionHelpers
             .AddTool<IVectorRectangleToolHandler, VectorRectangleToolViewModel>()
             .AddTool<IVectorRectangleToolHandler, VectorRectangleToolViewModel>()
             .AddTool<IVectorLineToolHandler, VectorLineToolViewModel>()
             .AddTool<IVectorLineToolHandler, VectorLineToolViewModel>()
             .AddTool<IVectorPathToolHandler, VectorPathToolViewModel>()
             .AddTool<IVectorPathToolHandler, VectorPathToolViewModel>()
+            .AddTool<ITextToolHandler, TextToolViewModel>()
             .AddTool<ZoomToolViewModel>()
             .AddTool<ZoomToolViewModel>()
             // File types
             // File types
             .AddSingleton<IoFileType, PixiFileType>()
             .AddSingleton<IoFileType, PixiFileType>()
@@ -109,10 +110,13 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IoFileType, GifFileType>()
             .AddSingleton<IoFileType, GifFileType>()
             .AddSingleton<IoFileType, Mp4FileType>()
             .AddSingleton<IoFileType, Mp4FileType>()
             .AddSingleton<IoFileType, SvgFileType>()
             .AddSingleton<IoFileType, SvgFileType>()
+            .AddSingleton<IoFileType, TtfFileType>()
+            .AddSingleton<IoFileType, OtfFileType>()
             // Serialization Factories
             // Serialization Factories
             .AddAssemblyTypes<SerializationFactory>()
             .AddAssemblyTypes<SerializationFactory>()
             // Custom document builders
             // Custom document builders
             .AddSingleton<IDocumentBuilder, SvgDocumentBuilder>()
             .AddSingleton<IDocumentBuilder, SvgDocumentBuilder>()
+            .AddSingleton<IDocumentBuilder, FontDocumentBuilder>()
             // Palette Parsers
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

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

@@ -70,7 +70,7 @@ internal class SupportedFilesHelper
 
 
     public static List<FilePickerFileType> BuildSaveFilter(FileTypeDialogDataSet.SetKind setKind = FileTypeDialogDataSet.SetKind.Any)
     public static List<FilePickerFileType> BuildSaveFilter(FileTypeDialogDataSet.SetKind setKind = FileTypeDialogDataSet.SetKind.Any)
     {
     {
-        var allSupportedExtensions = GetAllSupportedFileTypes(setKind);
+        var allSupportedExtensions = GetAllSupportedFileTypes(setKind).Where(x => x.CanSave).ToList();
         var filter = allSupportedExtensions.Select(i => i.SaveFilter).ToList();
         var filter = allSupportedExtensions.Select(i => i.SaveFilter).ToList();
 
 
         return filter;
         return filter;
@@ -84,7 +84,7 @@ internal class SupportedFilesHelper
             return null;
             return null;
 
 
         string extension = Path.GetExtension(file.Path.LocalPath);
         string extension = Path.GetExtension(file.Path.LocalPath);
-        return allSupportedExtensions.Single(i => i.Extensions.Contains(extension));
+        return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension));
     }
     }
 
 
     public static List<FilePickerFileType> BuildOpenFilter()
     public static List<FilePickerFileType> BuildOpenFilter()

+ 8 - 0
src/PixiEditor/Helpers/ThemeResources.cs

@@ -24,4 +24,12 @@ public static class ThemeResources
     public static Color BorderMidColor =>
     public static Color BorderMidColor =>
         ResourceLoader.GetResource<SolidColorBrush>("ThemeBorderMidBrush", Application.Current.ActualThemeVariant).Color
         ResourceLoader.GetResource<SolidColorBrush>("ThemeBorderMidBrush", Application.Current.ActualThemeVariant).Color
             .ToColor();
             .ToColor();
+
+    public static Color ThemeControlHighlightColor =>
+        ResourceLoader.GetResource<Avalonia.Media.Color>("ThemeControlHighlightColor", Application.Current.ActualThemeVariant)
+            .ToColor();
+
+    public static Color SelectionFillColor =>
+        ResourceLoader.GetResource<Avalonia.Media.Color>("SelectionFillColor", Application.Current.ActualThemeVariant)
+            .ToColor();
 }
 }

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

@@ -86,7 +86,7 @@ internal class ClassicDesktopEntry
             return;
             return;
         }
         }
 
 
-#if !STEAM
+#if !STEAM && !DEBUG
         if (!HandleNewInstance(dispatcher))
         if (!HandleNewInstance(dispatcher))
         {
         {
             return;
             return;

+ 18 - 3
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -161,6 +161,11 @@ internal static class ClipboardController
 
 
         await Clipboard.SetDataObjectAsync(data);
         await Clipboard.SetDataObjectAsync(data);
     }
     }
+    
+    public static async Task<string> GetTextFromClipboard()
+    {
+        return await Clipboard.GetTextAsync();
+    }
 
 
     private static async Task AddImageToClipboard(Surface actuallySurface, DataObject data)
     private static async Task AddImageToClipboard(Surface actuallySurface, DataObject data)
     {
     {
@@ -257,16 +262,26 @@ internal static class ClipboardController
             pos = dataObjectWithPos.GetVecD(ClipboardDataFormats.PositionFormat);
             pos = dataObjectWithPos.GetVecD(ClipboardDataFormats.PositionFormat);
         }
         }
 
 
+        RectD? tightBounds = null;
         for (var i = 0; i < layerIds.Length; i++)
         for (var i = 0; i < layerIds.Length; i++)
         {
         {
             var layerId = layerIds[i];
             var layerId = layerIds[i];
 
 
             var layer = doc.StructureHelper.Find(layerId);
             var layer = doc.StructureHelper.Find(layerId);
-            if (layer is not { TightBounds: not null } || !layer.TightBounds.Value.Pos.AlmostEquals(pos))
-                return false;
+
+            if(layer == null) return false;
+
+            if(tightBounds == null)
+            {
+                tightBounds = layer.TightBounds;
+            }
+            else if(layer.TightBounds.HasValue)
+            {
+                tightBounds = tightBounds.Value.Union(layer.TightBounds.Value);
+            }
         }
         }
 
 
-        return true;
+        return tightBounds.HasValue && tightBounds.Value.Pos.AlmostEquals(pos);
     }
     }
 
 
     private static Guid[] GetLayerIds(IEnumerable<IDataObject> data)
     private static Guid[] GetLayerIds(IEnumerable<IDataObject> data)

+ 44 - 0
src/PixiEditor/Models/Controllers/FontLibrary.cs

@@ -0,0 +1,44 @@
+using System.Collections.ObjectModel;
+using Avalonia.Media;
+using Drawie.Backend.Core.Text;
+
+namespace PixiEditor.Models.Controllers;
+
+public static class FontLibrary
+{
+    private static List<FontFamilyName> _customFonts = new List<FontFamilyName>();
+    private static List<FontFamilyName> _allFonts = new List<FontFamilyName>();
+
+    public static FontFamilyName DefaultFontFamily { get; } = new FontFamilyName("Arial");
+
+    public static FontFamilyName[] SystemFonts { get; } = FontManager.Current.SystemFonts.Select(x => new FontFamilyName(x.Name)).ToArray();
+    
+    public static IReadOnlyList<FontFamilyName> CustomFonts => _customFonts;
+
+    public static FontFamilyName[] AllFonts
+    {
+        get
+        {
+            if (_allFonts.Count != SystemFonts.Length + CustomFonts.Count)
+            {
+                _allFonts = SystemFonts.Concat(CustomFonts).ToList();
+            }
+
+            return _allFonts.ToArray();
+        }
+    }
+
+    public static event Action<FontFamilyName> FontAdded;
+
+    public static bool TryAddCustomFont(FontFamilyName fontFamily)
+    {
+        if (!CustomFonts.Any(x => x.Name == fontFamily.Name && x.FontUri == fontFamily.FontUri))
+        {
+            _customFonts.Add(fontFamily);
+            FontAdded?.Invoke(fontFamily);
+            return true;
+        }
+        
+        return false;
+    }
+}

+ 3 - 3
src/PixiEditor/Models/Controllers/InputDevice/MouseInputFilter.cs

@@ -51,13 +51,13 @@ internal class MouseInputFilter
 
 
     public void DeactivatedInlet(object? sender, EventArgs e)
     public void DeactivatedInlet(object? sender, EventArgs e)
     {
     {
-        MouseOnCanvasEventArgs argsLeft = new(MouseButton.Left, VecD.Zero, KeyModifiers.None);
+        MouseOnCanvasEventArgs argsLeft = new(MouseButton.Left, VecD.Zero, KeyModifiers.None, 0);
         MouseUpInlet(argsLeft);
         MouseUpInlet(argsLeft);
         
         
-        MouseOnCanvasEventArgs argsMiddle = new(MouseButton.Middle, VecD.Zero, KeyModifiers.None);
+        MouseOnCanvasEventArgs argsMiddle = new(MouseButton.Middle, VecD.Zero, KeyModifiers.None, 0);
         MouseUpInlet(argsMiddle);
         MouseUpInlet(argsMiddle);
         
         
-        MouseOnCanvasEventArgs argsRight = new(MouseButton.Right, VecD.Zero, KeyModifiers.None);
+        MouseOnCanvasEventArgs argsRight = new(MouseButton.Right, VecD.Zero, KeyModifiers.None, 0);
         MouseUpInlet(argsRight);
         MouseUpInlet(argsRight);
     }
     }
 }
 }

+ 10 - 6
src/PixiEditor/Models/Controllers/InputDevice/MouseOnCanvasEventArgs.cs

@@ -3,17 +3,21 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.Models.Controllers.InputDevice;
 namespace PixiEditor.Models.Controllers.InputDevice;
+
 internal class MouseOnCanvasEventArgs : EventArgs
 internal class MouseOnCanvasEventArgs : EventArgs
 {
 {
-    public MouseOnCanvasEventArgs(MouseButton button, VecD positionOnCanvas, KeyModifiers keyModifiers)
+    public MouseButton Button { get; }
+    public VecD PositionOnCanvas { get; }
+    public KeyModifiers KeyModifiers { get; }
+    public bool Handled { get; set; }
+    
+    public int ClickCount { get; set; } = 1;
+
+    public MouseOnCanvasEventArgs(MouseButton button, VecD positionOnCanvas, KeyModifiers keyModifiers, int clickCount)
     {
     {
         Button = button;
         Button = button;
         PositionOnCanvas = positionOnCanvas;
         PositionOnCanvas = positionOnCanvas;
         KeyModifiers = keyModifiers;
         KeyModifiers = keyModifiers;
+        ClickCount = clickCount;
     }
     }
-
-    public MouseButton Button { get; }
-    public VecD PositionOnCanvas { get; }
-    public KeyModifiers KeyModifiers { get; }
-    public bool Handled { get; set; }
 }
 }

+ 3 - 3
src/PixiEditor/Models/Controllers/ShortcutController.cs

@@ -38,14 +38,14 @@ internal class ShortcutController
         _shortcutExecutionBlockers.Clear();
         _shortcutExecutionBlockers.Clear();
     }
     }
 
 
-    public KeyCombination GetToolShortcut<T>()
+    public KeyCombination? GetToolShortcut<T>()
     {
     {
         return GetToolShortcut(typeof(T));
         return GetToolShortcut(typeof(T));
     }
     }
 
 
-    public KeyCombination GetToolShortcut(Type type)
+    public KeyCombination? GetToolShortcut(Type type)
     {
     {
-        return CommandController.Current.Commands.First(x => x is Command.ToolCommand tool && tool.ToolType == type).Shortcut;
+        return CommandController.Current.Commands.FirstOrDefault(x => x is Command.ToolCommand tool && tool.ToolType == type)?.Shortcut;
     }
     }
 
 
     public void KeyPressed(bool isRepeat, Key key, KeyModifiers modifiers)
     public void KeyPressed(bool isRepeat, Key key, KeyModifiers modifiers)

+ 68 - 51
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -95,61 +95,78 @@ internal class ActionAccumulator
         };
         };
         busyTimer.Start();
         busyTimer.Start();
 
 
-        while (queuedActions.Count > 0)
+        try
         {
         {
-            var toExecute = queuedActions;
-            queuedActions = new();
-
-            List<IChangeInfo?> changes;
-            if (AreAllPassthrough(toExecute))
-            {
-                changes = toExecute.Select(a => (IChangeInfo?)a.action).ToList();
-            }
-            else
-            {
-                changes = await internals.Tracker.ProcessActions(toExecute);
-            }
-
-            List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
-            bool undoBoundaryPassed =
-                toExecute.Any(static action => action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
-            bool viewportRefreshRequest =
-                toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
-            bool changeFrameRequest =
-                toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
-            foreach (IChangeInfo info in optimizedChanges)
-            {
-                internals.Updater.ApplyChangeFromChangeInfo(info);
-            }
-
-            if (undoBoundaryPassed)
-                internals.Updater.AfterUndoBoundaryPassed();
-
-            // update the contents of the bitmaps
-            var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime, internals.Tracker,
-                optimizedChanges);
-            if (DrawingBackendApi.Current.IsHardwareAccelerated)
+            while (queuedActions.Count > 0)
             {
             {
-                canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
-                    undoBoundaryPassed || viewportRefreshRequest);
-            }
-            else
-            {
-                await canvasUpdater.UpdateGatheredChunks(affectedAreas,
-                    undoBoundaryPassed || viewportRefreshRequest);
-            }
-
-            previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest, affectedAreas.ImagePreviewAreas.Keys,
-                affectedAreas.MaskPreviewAreas.Keys,
-                affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
-
-            // force refresh viewports for better responsiveness
-            foreach (var (_, value) in internals.State.Viewports)
-            {
-                if (!value.Delayed)
-                    value.InvalidateVisual();
+                var toExecute = queuedActions;
+                queuedActions = new();
+
+                List<IChangeInfo?> changes;
+                if (AreAllPassthrough(toExecute))
+                {
+                    changes = toExecute.Select(a => (IChangeInfo?)a.action).ToList();
+                }
+                else
+                {
+                    changes = await internals.Tracker.ProcessActions(toExecute);
+                }
+
+                List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
+                bool undoBoundaryPassed =
+                    toExecute.Any(static action =>
+                        action.action is ChangeBoundary_Action or Redo_Action or Undo_Action);
+                bool viewportRefreshRequest =
+                    toExecute.Any(static action => action.action is RefreshViewport_PassthroughAction);
+                bool changeFrameRequest =
+                    toExecute.Any(static action => action.action is SetActiveFrame_PassthroughAction);
+                foreach (IChangeInfo info in optimizedChanges)
+                {
+                    internals.Updater.ApplyChangeFromChangeInfo(info);
+                }
+
+                if (undoBoundaryPassed)
+                    internals.Updater.AfterUndoBoundaryPassed();
+
+                // update the contents of the bitmaps
+                var affectedAreas = new AffectedAreasGatherer(document.AnimationHandler.ActiveFrameTime,
+                    internals.Tracker,
+                    optimizedChanges);
+                if (DrawingBackendApi.Current.IsHardwareAccelerated)
+                {
+                    canvasUpdater.UpdateGatheredChunksSync(affectedAreas,
+                        undoBoundaryPassed || viewportRefreshRequest);
+                }
+                else
+                {
+                    await canvasUpdater.UpdateGatheredChunks(affectedAreas,
+                        undoBoundaryPassed || viewportRefreshRequest);
+                }
+
+                previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest,
+                    affectedAreas.ImagePreviewAreas.Keys,
+                    affectedAreas.MaskPreviewAreas.Keys,
+                    affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
+
+                // force refresh viewports for better responsiveness
+                foreach (var (_, value) in internals.State.Viewports)
+                {
+                    if (!value.Delayed)
+                        value.InvalidateVisual();
+                }
             }
             }
         }
         }
+        catch (Exception e)
+        {
+            busyTimer.Stop();
+            document.Busy = false;
+            executing = false;
+#if DEBUG
+            Console.WriteLine(e);
+#endif
+            await CrashHelper.SendExceptionInfoAsync(e);
+            throw;
+        }
 
 
         busyTimer.Stop();
         busyTimer.Stop();
         if (document.Busy)
         if (document.Busy)

+ 34 - 2
src/PixiEditor/Models/DocumentModels/ChangeExecutionController.cs

@@ -221,12 +221,28 @@ internal class ChangeExecutionController
         currentSession?.OnLeftMouseButtonUp(argsPositionOnCanvas);
         currentSession?.OnLeftMouseButtonUp(argsPositionOnCanvas);
     }
     }
 
 
-    public void TransformMovedInlet(ShapeCorners corners)
+    public void TransformChangedInlet(ShapeCorners corners)
     {
     {
         if (currentSession is ITransformableExecutor transformableExecutor)
         if (currentSession is ITransformableExecutor transformableExecutor)
         {
         {
             LastTransformState = corners;
             LastTransformState = corners;
-            transformableExecutor.OnTransformMoved(corners);
+            transformableExecutor.OnTransformChanged(corners);
+        }
+    }
+    
+    public void TransformDraggedInlet(VecD from, VecD to)
+    {
+        if (currentSession is ITransformDraggedEvent transformableExecutor)
+        {
+            transformableExecutor.OnTransformDragged(from, to);
+        }
+    }
+
+    public void TransformStoppedInlet()
+    {
+        if(currentSession is ITransformStoppedEvent transformStoppedEvent)
+        {
+            transformStoppedEvent.OnTransformStopped();
         }
         }
     }
     }
 
 
@@ -289,4 +305,20 @@ internal class ChangeExecutionController
             vectorPathToolExecutor.OnPathChanged(path);
             vectorPathToolExecutor.OnPathChanged(path);
         }
         }
     }
     }
+
+    public void TextOverlayTextChangedInlet(string text)
+    {
+        if (currentSession is ITextOverlayEvents textOverlayHandler)
+        {
+            textOverlayHandler.OnTextChanged(text);
+        }
+    }
+
+    public void QuickToolSwitchInlet()
+    {
+        if (currentSession is IQuickToolSwitchable quickToolSwitchable)
+        {
+            quickToolSwitchable.OnQuickToolSwitch();
+        }
+    }
 }
 }

+ 2 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentEventsModule.cs

@@ -51,4 +51,6 @@ internal class DocumentEventsModule
     public void OnSymmetryDragStarted(SymmetryAxisDirection dir) => Internals.ChangeController.SymmetryDragStartedInlet(dir);
     public void OnSymmetryDragStarted(SymmetryAxisDirection dir) => Internals.ChangeController.SymmetryDragStartedInlet(dir);
     public void OnSymmetryDragged(SymmetryAxisDragInfo info) => Internals.ChangeController.SymmetryDraggedInlet(info);
     public void OnSymmetryDragged(SymmetryAxisDragInfo info) => Internals.ChangeController.SymmetryDraggedInlet(info);
     public void OnSymmetryDragEnded(SymmetryAxisDirection dir) => Internals.ChangeController.SymmetryDragEndedInlet(dir);
     public void OnSymmetryDragEnded(SymmetryAxisDirection dir) => Internals.ChangeController.SymmetryDragEndedInlet(dir);
+
+    public void QuickToolSwitchInlet() => Internals.ChangeController.QuickToolSwitchInlet();
 }
 }

+ 17 - 6
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -89,7 +89,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
         if (drawOnMask && !member.HasMaskBindable)
         if (drawOnMask && !member.HasMaskBindable)
             return;
             return;
-        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id, drawOnMask, frame));
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id, Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
         if (clearSelection)
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
         Internals.ActionAccumulator.AddFinishedActions();
@@ -210,13 +210,13 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
         if (!isFolder)
         if (!isFolder)
         {
         {
-            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue));
+            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue, Guid.NewGuid()));
         }
         }
         else
         else
         {
         {
             Guid newGuid = Guid.NewGuid();
             Guid newGuid = Guid.NewGuid();
             Internals.ActionAccumulator.AddFinishedActions(
             Internals.ActionAccumulator.AddFinishedActions(
-                new DuplicateFolder_Action(guidValue, newGuid),
+                new DuplicateFolder_Action(guidValue, newGuid, null),
                 new SetSelectedMember_PassthroughAction(newGuid));
                 new SetSelectedMember_PassthroughAction(newGuid));
         }
         }
     }
     }
@@ -881,7 +881,8 @@ internal class DocumentOperationsModule : IDocumentOperations
 
 
         Internals.ChangeController.TryStopActiveExecutor();
         Internals.ChangeController.TryStopActiveExecutor();
 
 
-        Internals.ActionAccumulator.AddFinishedActions(new KeyFrameLength_Action(celId, startFrame, duration), new EndKeyFrameLength_Action());
+        Internals.ActionAccumulator.AddFinishedActions(new KeyFrameLength_Action(celId, startFrame, duration),
+            new EndKeyFrameLength_Action());
     }
     }
 
 
     public void DeleteNodes(Guid[] nodes)
     public void DeleteNodes(Guid[] nodes)
@@ -890,7 +891,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
             return;
 
 
         Internals.ChangeController.TryStopActiveExecutor();
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         List<IAction> actions = new();
         List<IAction> actions = new();
 
 
         for (var i = 0; i < nodes.Length; i++)
         for (var i = 0; i < nodes.Length; i++)
@@ -899,10 +900,20 @@ internal class DocumentOperationsModule : IDocumentOperations
             if (Document.StructureHelper.TryFindNode(node, out INodeHandler nodeHandler) &&
             if (Document.StructureHelper.TryFindNode(node, out INodeHandler nodeHandler) &&
                 nodeHandler.InternalName == OutputNode.UniqueName)
                 nodeHandler.InternalName == OutputNode.UniqueName)
                 return;
                 return;
-            
+
             actions.Add(new DeleteNode_Action(node));
             actions.Add(new DeleteNode_Action(node));
         }
         }
 
 
         Internals.ActionAccumulator.AddFinishedActions(actions.ToArray());
         Internals.ActionAccumulator.AddFinishedActions(actions.ToArray());
     }
     }
+
+    public void ConvertToCurve(Guid memberId)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        Internals.ActionAccumulator.AddFinishedActions(new ConvertToCurve_Action(memberId));
+    }
 }
 }

+ 14 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -146,7 +146,7 @@ internal class DocumentStructureModule
         return layers;
         return layers;
     }
     }
 
 
-    public List<IStructureMemberHandler> GetAllMembers()
+    public List<IStructureMemberHandler> TraverseAllMembers()
     {
     {
         List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
         List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
 
 
@@ -160,6 +160,19 @@ internal class DocumentStructureModule
         return members;
         return members;
     }
     }
 
 
+    public List<IStructureMemberHandler> GetAllMembers()
+    {
+        List<IStructureMemberHandler> members = new List<IStructureMemberHandler>();
+
+        foreach (INodeHandler node in doc.NodeGraphHandler.AllNodes)
+        {
+            if (node is IStructureMemberHandler member)
+                members.Add(member);
+        }
+
+        return members;
+    }
+
     private void FillPath(INodeHandler node, List<INodeHandler> toFill)
     private void FillPath(INodeHandler node, List<INodeHandler> toFill)
     {
     {
         node.TraverseForwards(newNode =>
         node.TraverseForwards(newNode =>

+ 6 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentToolsModule.cs

@@ -21,7 +21,6 @@ internal class DocumentToolsModule
 
 
     public void UseOpacitySlider() => Internals.ChangeController.TryStartExecutor<StructureMemberOpacityExecutor>();
     public void UseOpacitySlider() => Internals.ChangeController.TryStartExecutor<StructureMemberOpacityExecutor>();
 
 
-    public void UseShiftLayerTool() => Internals.ChangeController.TryStartExecutor<ShiftLayerExecutor>();
 
 
     public void UsePenTool() => Internals.ChangeController.TryStartExecutor<PenToolExecutor>();
     public void UsePenTool() => Internals.ChangeController.TryStartExecutor<PenToolExecutor>();
 
 
@@ -80,4 +79,10 @@ internal class DocumentToolsModule
     public void UseLassoTool() => Internals.ChangeController.TryStartExecutor<LassoToolExecutor>();
     public void UseLassoTool() => Internals.ChangeController.TryStartExecutor<LassoToolExecutor>();
 
 
     public void UseMagicWandTool() => Internals.ChangeController.TryStartExecutor<MagicWandToolExecutor>();
     public void UseMagicWandTool() => Internals.ChangeController.TryStartExecutor<MagicWandToolExecutor>();
+
+    public void UseTextTool()
+    {
+        bool force = Internals.ChangeController.GetCurrentExecutorType() == ExecutorType.ToolLinked;
+        Internals.ChangeController.TryStartExecutor<VectorTextToolExecutor>(force);
+    }
 }
 }

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

@@ -140,7 +140,7 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
         return pos1;
         return pos1;
     }
     }
 
 
-    public override void OnTransformMoved(ShapeCorners corners)
+    public override void OnTransformChanged(ShapeCorners corners)
     {
     {
         if (ActiveMode != ShapeToolMode.Transform)
         if (ActiveMode != ShapeToolMode.Transform)
             return;
             return;

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/IQuickToolSwitchable.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+internal interface IQuickToolSwitchable : IExecutorFeature
+{
+    public void OnQuickToolSwitch();
+}

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITextOverlayEvents.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface ITextOverlayEvents :IExecutorFeature
+{
+    public void OnTextChanged(string text);
+}

+ 9 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformDraggedEvent.cs

@@ -0,0 +1,9 @@
+using ChunkyImageLib.DataHolders;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface ITransformDraggedEvent : IExecutorFeature
+{
+    public void OnTransformDragged(VecD from, VecD to);
+}

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformStoppedEvent.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+
+public interface ITransformStoppedEvent : IExecutorFeature
+{
+    public void OnTransformStopped();
+}

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/Features/ITransformableExecutor.cs

@@ -6,7 +6,7 @@ namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 public interface ITransformableExecutor : IExecutorFeature
 public interface ITransformableExecutor : IExecutorFeature
 {
 {
     public bool IsTransforming { get; }
     public bool IsTransforming { get; }
-    public void OnTransformMoved(ShapeCorners corners); 
+    public void OnTransformChanged(ShapeCorners corners); 
     public void OnTransformApplied();
     public void OnTransformApplied();
     public void OnLineOverlayMoved(VecD start, VecD end);
     public void OnLineOverlayMoved(VecD start, VecD end);
     public void OnSelectedObjectNudged(VecI distance);
     public void OnSelectedObjectNudged(VecI distance);

+ 4 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineExecutor.cs

@@ -79,6 +79,9 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
         if (member is IVectorLayerHandler)
         if (member is IVectorLayerHandler)
         {
         {
             var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
             var node = (VectorLayerNode)internals.Tracker.Document.FindMember(member.Id);
+            
+            if(node is null)
+                return ExecutionState.Error;
 
 
             if (node.ShapeData is not IReadOnlyLineData data)
             if (node.ShapeData is not IReadOnlyLineData data)
             {
             {
@@ -94,7 +97,7 @@ internal abstract class LineExecutor<T> : SimpleShapeToolExecutor where T : ILin
 
 
             ActiveMode = ShapeToolMode.Transform;
             ActiveMode = ShapeToolMode.Transform;
 
 
-            document.LineToolOverlayHandler.Show(data.Start, data.End, false, AddToUndo);
+            document.LineToolOverlayHandler.Show(data.TransformedStart, data.TransformedEnd, false, AddToUndo);
         }
         }
         else
         else
         {
         {

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

@@ -20,7 +20,7 @@ internal class MagicWandToolExecutor : UpdateableChangeExecutor
     public override ExecutionState Start()
     public override ExecutionState Start()
     {
     {
         var magicWand = GetHandler<IMagicWandToolHandler>();
         var magicWand = GetHandler<IMagicWandToolHandler>();
-        var members = document!.ExtractSelectedLayers(true);
+        var members = document!.ExtractSelectedLayers(true).ToList();
 
 
         if (magicWand is null || members.Count == 0)
         if (magicWand is null || members.Count == 0)
             return ExecutionState.Error;
             return ExecutionState.Error;

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

@@ -62,7 +62,7 @@ internal class PasteImageExecutor : UpdateableChangeExecutor, ITransformableExec
 
 
     public bool IsTransforming => true; 
     public bool IsTransforming => true; 
 
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
     {
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, default));
         internals!.ActionAccumulator.AddActions(new PasteImage_Action(image, corners, memberGuid.Value, false, drawOnMask, document!.AnimationHandler.ActiveFrameBindable, default));
     }
     }

+ 0 - 74
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ShiftLayerExecutor.cs

@@ -1,74 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using PixiEditor.ChangeableDocument.Actions.Generated;
-using Drawie.Backend.Core.Numerics;
-using PixiEditor.Models.Handlers;
-using PixiEditor.Models.Handlers.Tools;
-using PixiEditor.Models.Tools;
-using Drawie.Numerics;
-
-namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
-#nullable enable
-internal class ShiftLayerExecutor : UpdateableChangeExecutor
-{
-    private List<Guid> _affectedMemberGuids = new List<Guid>();
-    private VecI startPos;
-    private IMoveToolHandler? tool;
-
-    public override ExecutorStartMode StartMode => ExecutorStartMode.OnMouseLeftButtonDown;
-
-    public override ExecutionState Start()
-    {
-        IStructureMemberHandler? member = document!.SelectedStructureMember;
-
-        tool = GetHandler<IMoveToolHandler>();
-        if (tool is null)
-            return ExecutionState.Error;
-
-
-        if (member != null)
-            _affectedMemberGuids.Add(member.Id);
-        _affectedMemberGuids.AddRange(document!.SoftSelectedStructureMembers.Select(x => x.Id));
-
-        RemoveDrawOnMaskLayers(_affectedMemberGuids);
-
-        startPos = controller!.LastPixelPosition;
-
-        ShiftLayer_Action action = new(_affectedMemberGuids, VecI.Zero, tool.KeepOriginalImage,
-            document!.AnimationHandler.ActiveFrameBindable);
-        internals!.ActionAccumulator.AddActions(action);
-
-        return ExecutionState.Success;
-    }
-
-    private void RemoveDrawOnMaskLayers(List<Guid> affectedMemberGuids)
-    {
-        for (var i = 0; i < affectedMemberGuids.Count; i++)
-        {
-            var guid = affectedMemberGuids[i];
-            if (document!.StructureHelper.FindOrThrow(guid) is ILayerHandler { ShouldDrawOnMask: true })
-            {
-                _affectedMemberGuids.Remove(guid);
-                i--;
-            }
-        }
-    }
-
-    public override void OnPixelPositionChange(VecI pos)
-    {
-        ShiftLayer_Action action = new(_affectedMemberGuids, pos - startPos, tool!.KeepOriginalImage,
-            document!.AnimationHandler.ActiveFrameBindable);
-        internals!.ActionAccumulator.AddActions(action);
-    }
-
-    public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)
-    {
-        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
-        onEnded?.Invoke(this);
-    }
-
-    public override void ForceStop()
-    {
-        internals!.ActionAccumulator.AddFinishedActions(new EndShiftLayer_Action());
-    }
-}

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

@@ -145,7 +145,7 @@ internal abstract class SimpleShapeToolExecutor : UpdateableChangeExecutor,
 
 
     public bool IsTransforming => ActiveMode == ShapeToolMode.Transform; 
     public bool IsTransforming => ActiveMode == ShapeToolMode.Transform; 
 
 
-    public virtual void OnTransformMoved(ShapeCorners corners)
+    public virtual void OnTransformChanged(ShapeCorners corners)
     {
     {
         
         
     }
     }

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

@@ -22,7 +22,7 @@ internal class TransformReferenceLayerExecutor : UpdateableChangeExecutor, ITran
 
 
     public bool IsTransforming => true;
     public bool IsTransforming => true;
 
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
     {
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
         internals!.ActionAccumulator.AddActions(new TransformReferenceLayer_Action(corners));
     }
     }

+ 180 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -1,21 +1,29 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using System.Linq;
 using Avalonia.Input;
 using Avalonia.Input;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Actions;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Controllers.InputDevice;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentPassthroughActions;
+using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.Document.Nodes;
 using PixiEditor.ViewModels.Document.Nodes;
 
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
 #nullable enable
-internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransformableExecutor, IMidChangeUndoableExecutor
+internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransformableExecutor, ITransformDraggedEvent,
+    IMidChangeUndoableExecutor,
+    ITransformStoppedEvent
 {
 {
     private Dictionary<Guid, ShapeCorners> memberCorners = new();
     private Dictionary<Guid, ShapeCorners> memberCorners = new();
     private IMoveToolHandler? tool;
     private IMoveToolHandler? tool;
@@ -25,9 +33,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     public override bool BlocksOtherActions => false;
     public override bool BlocksOtherActions => false;
 
 
     private List<Guid> selectedMembers = new();
     private List<Guid> selectedMembers = new();
+    private List<Guid> originalSelectedMembers = new();
 
 
+    private ShapeCorners cornersOnStartDuplicate;
     private ShapeCorners lastCorners = new();
     private ShapeCorners lastCorners = new();
     private bool movedOnce;
     private bool movedOnce;
+    private bool duplicateOnStop = false;
 
 
     public TransformSelectedExecutor(bool toolLinked)
     public TransformSelectedExecutor(bool toolLinked)
     {
     {
@@ -42,7 +53,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
 
         tool.TransformingSelectedArea = true;
         tool.TransformingSelectedArea = true;
         List<IStructureMemberHandler> members = new();
         List<IStructureMemberHandler> members = new();
-
+        originalSelectedMembers = document.SelectedMembers.ToList();
         var guids = document.ExtractSelectedLayers(false);
         var guids = document.ExtractSelectedLayers(false);
         members = guids.Select(g => document.StructureHelper.Find(g)).ToList();
         members = guids.Select(g => document.StructureHelper.Find(g)).ToList();
 
 
@@ -50,6 +61,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             return ExecutionState.Error;
             return ExecutionState.Error;
 
 
         document.TransformHandler.PassthroughPointerPressed += OnLeftMouseButtonDown;
         document.TransformHandler.PassthroughPointerPressed += OnLeftMouseButtonDown;
+
         return SelectMembers(members);
         return SelectMembers(members);
     }
     }
 
 
@@ -115,6 +127,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         selectedMembers = members.Select(m => m.Id).ToList();
         selectedMembers = members.Select(m => m.Id).ToList();
 
 
         lastCorners = masterCorners;
         lastCorners = masterCorners;
+
+
         document.TransformHandler.ShowTransform(mode, true, masterCorners,
         document.TransformHandler.ShowTransform(mode, true, masterCorners,
             Type == ExecutorType.Regular || tool.KeepOriginalImage);
             Type == ExecutorType.Regular || tool.KeepOriginalImage);
 
 
@@ -122,12 +136,22 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
 
         movedOnce = false;
         movedOnce = false;
         isInProgress = true;
         isInProgress = true;
+
         return ExecutionState.Success;
         return ExecutionState.Success;
     }
     }
 
 
     public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
     public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
     {
     {
         args.Handled = true;
         args.Handled = true;
+
+        if (args.ClickCount >= 2)
+        {
+            if (SwitchToLayerTool())
+            {
+                return;
+            }
+        }
+
         var allLayers = document.StructureHelper.GetAllLayers();
         var allLayers = document.StructureHelper.GetAllLayers();
         var topMostWithinClick = allLayers.Where(x =>
         var topMostWithinClick = allLayers.Where(x =>
                 x is { IsVisibleBindable: true, TightBounds: not null } &&
                 x is { IsVisibleBindable: true, TightBounds: not null } &&
@@ -164,6 +188,23 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         }
         }
     }
     }
 
 
+    private bool SwitchToLayerTool()
+    {
+        if (document.SelectedStructureMember is ILayerHandler layerHandler && layerHandler.QuickEditTool != null)
+        {
+            ViewModelMain.Current.ToolsSubViewModel.SetActiveTool(layerHandler.QuickEditTool, false);
+            ViewModelMain.Current.ToolsSubViewModel.QuickToolSwitchInlet();
+            return true;
+        }
+
+        return false;
+    }
+
+    public void OnTransformStopped()
+    {
+        DuplicateIfRequired();
+    }
+
     private void Deselect(List<ILayerHandler> topMostWithinClick)
     private void Deselect(List<ILayerHandler> topMostWithinClick)
     {
     {
         var topMost = topMostWithinClick.FirstOrDefault();
         var topMost = topMostWithinClick.FirstOrDefault();
@@ -212,6 +253,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         if (isInProgress)
         if (isInProgress)
         {
         {
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
             internals.ActionAccumulator.AddActions(new EndTransformSelected_Action());
+            internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
             document!.TransformHandler.HideTransform();
             document!.TransformHandler.HideTransform();
             AddSnappingForMembers(selectedMembers);
             AddSnappingForMembers(selectedMembers);
 
 
@@ -223,22 +265,48 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
 
     public bool IsTransforming => isInProgress;
     public bool IsTransforming => isInProgress;
 
 
-    public void OnTransformMoved(ShapeCorners corners)
+    public void OnTransformChanged(ShapeCorners corners)
     {
     {
         DoTransform(corners);
         DoTransform(corners);
         lastCorners = corners;
         lastCorners = corners;
     }
     }
 
 
+    public void OnTransformDragged(VecD from, VecD to)
+    {
+        if (!isInProgress)
+            return;
+
+        if (tool.DuplicateOnMove)
+        {
+            if (!duplicateOnStop)
+            {
+                cornersOnStartDuplicate = lastCorners;
+                duplicateOnStop = true;
+                internals.ActionAccumulator.AddFinishedActions(new EndTransformSelected_Action());
+            }
+
+            VecD delta = new VecD(
+                to.X - from.X,
+                to.Y - from.Y);
+
+            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(selectedMembers, delta,
+                document!.AnimationHandler.ActiveFrameBindable));
+        }
+    }
+
     private void DoTransform(ShapeCorners corners)
     private void DoTransform(ShapeCorners corners)
     {
     {
         if (!isInProgress)
         if (!isInProgress)
             return;
             return;
 
 
+        if (duplicateOnStop) return;
+
         if (!movedOnce)
         if (!movedOnce)
         {
         {
             internals!.ActionAccumulator.AddActions(
             internals!.ActionAccumulator.AddActions(
                 new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, memberCorners, false,
                 new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, memberCorners, false,
                     document.AnimationHandler.ActiveFrameBindable));
                     document.AnimationHandler.ActiveFrameBindable));
+
             movedOnce = true;
             movedOnce = true;
         }
         }
 
 
@@ -247,6 +315,101 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 document!.AnimationHandler.ActiveFrameBindable));
                 document!.AnimationHandler.ActiveFrameBindable));
     }
     }
 
 
+    private void DuplicateSelected()
+    {
+        List<IAction> actions = new();
+
+        List<Guid> newLayerGuids = new();
+        List<Guid> newGuidsOfOriginal = new();
+
+        internals.ActionAccumulator.StartChangeBlock();
+
+        actions.Add(new EndPreviewShiftLayers_Action());
+
+        VectorPath? original = document.SelectionPathBindable != null
+            ? new VectorPath(document.SelectionPathBindable)
+            : null;
+
+        VectorPath? clearArea = null;
+        if (original != null)
+        {
+            var selection = document.SelectionPathBindable;
+            var inverse = new VectorPath();
+            inverse.AddRect(new RectD(new(0, 0), document.SizeBindable));
+
+            clearArea = inverse.Op(selection, VectorPathOp.Difference);
+        }
+
+        for (var i = 0; i < originalSelectedMembers.Count; i++)
+        {
+            var member = originalSelectedMembers[i];
+            Guid newGuid = Guid.NewGuid();
+            if (document.StructureHelper.Find(member) is not FolderNodeViewModel folder)
+            {
+                newLayerGuids.Add(newGuid);
+                actions.Add(new DuplicateLayer_Action(member, newGuid));
+                if (document.SelectionPathBindable is { IsEmpty: false })
+                {
+                    actions.Add(new ClearSelectedArea_Action(newGuid, clearArea, false,
+                        document.AnimationHandler.ActiveFrameBindable));
+                }
+            }
+            else
+            {
+                int childCount = folder.CountChildrenRecursive();
+                Guid[] newGuidsArray = new Guid[childCount];
+                for (var j = 0; j < childCount; j++)
+                {
+                    newGuidsArray[j] = Guid.NewGuid();
+                }
+
+                actions.Add(new DuplicateFolder_Action(member, newGuid, newGuidsArray.ToImmutableList()));
+
+                for (int j = 0; j < childCount; j++)
+                {
+                    if (document.SelectionPathBindable is { IsEmpty: false })
+                    {
+                        actions.Add(new ClearSelectedArea_Action(newGuidsArray[j], clearArea, false,
+                            document.AnimationHandler.ActiveFrameBindable));
+                    }
+                }
+
+                newLayerGuids.AddRange(newGuidsArray);
+            }
+
+            newGuidsOfOriginal.Add(newGuid);
+        }
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+        actions.Clear();
+
+        VecD delta = new VecD(
+            lastCorners.AABBBounds.TopLeft.X - cornersOnStartDuplicate.AABBBounds.TopLeft.X,
+            lastCorners.AABBBounds.TopLeft.Y - cornersOnStartDuplicate.AABBBounds.TopLeft.Y);
+
+        actions.Add(new ShiftLayer_Action(newLayerGuids, delta, document!.AnimationHandler.ActiveFrameBindable));
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+        actions.Clear();
+
+        actions.Add(new ClearSoftSelectedMembers_PassthroughAction());
+        foreach (var newGuid in newGuidsOfOriginal)
+        {
+            actions.Add(new AddSoftSelectedMember_PassthroughAction(newGuid));
+        }
+
+        actions.Add(new SetSelectedMember_PassthroughAction(newGuidsOfOriginal.Last()));
+
+        internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
+
+
+        internals.ActionAccumulator.EndChangeBlock();
+
+        tool!.DuplicateOnMove = false;
+    }
+
     public void OnLineOverlayMoved(VecD start, VecD end) { }
     public void OnLineOverlayMoved(VecD start, VecD end) { }
 
 
     public void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);
     public void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);
@@ -264,6 +427,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             tool.TransformingSelectedArea = false;
             tool.TransformingSelectedArea = false;
         }
         }
 
 
+        internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
@@ -286,6 +450,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             tool.TransformingSelectedArea = false;
             tool.TransformingSelectedArea = false;
         }
         }
 
 
+        internals!.ActionAccumulator.AddActions(new EndPreviewShiftLayers_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
         document!.TransformHandler.HideTransform();
@@ -293,6 +458,16 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
 
         isInProgress = false;
         isInProgress = false;
         document.TransformHandler.PassthroughPointerPressed -= OnLeftMouseButtonDown;
         document.TransformHandler.PassthroughPointerPressed -= OnLeftMouseButtonDown;
+        DuplicateIfRequired();
+    }
+
+    private void DuplicateIfRequired()
+    {
+        if (duplicateOnStop)
+        {
+            DuplicateSelected();
+            duplicateOnStop = false;
+        }
     }
     }
 
 
     private void AddSnappingForMembers(List<Guid> memberGuids)
     private void AddSnappingForMembers(List<Guid> memberGuids)
@@ -314,6 +489,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
 
     public bool IsFeatureEnabled(IExecutorFeature feature)
     public bool IsFeatureEnabled(IExecutorFeature feature)
     {
     {
-        return feature is ITransformableExecutor && IsTransforming;
+        return feature is ITransformableExecutor && IsTransforming || feature is IMidChangeUndoableExecutor ||
+               feature is ITransformStoppedEvent || feature is ITransformDraggedEvent;
     }
     }
 }
 }

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

@@ -62,4 +62,14 @@ internal abstract class UpdateableChangeExecutor
     public virtual void OnSettingsChanged(string name, object value) { }
     public virtual void OnSettingsChanged(string name, object value) { }
     public virtual void OnColorChanged(Color color, bool primary) { }
     public virtual void OnColorChanged(Color color, bool primary) { }
     public virtual void OnMembersSelected(List<Guid> memberGuids) { }
     public virtual void OnMembersSelected(List<Guid> memberGuids) { }
+
+    protected T[] QueryLayers<T>(VecD pos) where T : ILayerHandler
+    {
+        var allLayers = document.StructureHelper.GetAllLayers();
+        var topMostWithinClick = allLayers.Where(x =>
+                x is T { IsVisibleBindable: true, TightBounds: not null } &&
+                x.TightBounds.Value.ContainsInclusive(pos))
+            .OrderByDescending(x => allLayers.IndexOf(x));
+        return topMostWithinClick.Cast<T>().ToArray();
+    }
 }
 }

+ 2 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorLineToolExecutor.cs

@@ -24,8 +24,8 @@ internal class VectorLineToolExecutor : LineExecutor<IVectorLineToolHandler>
         if (data is null)
         if (data is null)
             return false;
             return false;
 
 
-        startPoint = data.Start;
-        endPoint = data.End;
+        startPoint = data.TransformedStart;
+        endPoint = data.TransformedEnd;
 
 
         return true;
         return true;
     }
     }

+ 268 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -0,0 +1,268 @@
+using Avalonia.Input;
+using Avalonia.Threading;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Models.Handlers.Tools;
+using PixiEditor.Models.Tools;
+using PixiEditor.ViewModels;
+
+namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
+
+internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEvents, IQuickToolSwitchable
+{
+    private ITextToolHandler textHandler;
+    private ITextToolbar toolbar;
+    private IStructureMemberHandler selectedMember;
+
+    private string lastText = "";
+    private VecD position;
+    private Matrix3X3 lastMatrix = Matrix3X3.Identity;
+    private Font? cachedFont;
+    private bool isListeningForValidLayer;
+    private VectorPath? onPath;
+
+    public override bool BlocksOtherActions => false;
+
+    public override ExecutorType Type => ExecutorType.ToolLinked;
+
+    public override ExecutionState Start()
+    {
+        textHandler = GetHandler<ITextToolHandler>();
+        if (textHandler == null)
+        {
+            return ExecutionState.Error;
+        }
+
+        toolbar = textHandler.Toolbar as ITextToolbar;
+        if (toolbar == null)
+        {
+            return ExecutionState.Error;
+        }
+
+        selectedMember = document.SelectedStructureMember;
+
+        if (selectedMember is not IVectorLayerHandler layerHandler)
+        {
+            isListeningForValidLayer = true;
+            return ExecutionState.Success;
+        }
+
+        isListeningForValidLayer = false;
+        var shape = layerHandler.GetShapeData(document.AnimationHandler.ActiveFrameBindable);
+        if (shape is TextVectorData textData)
+        {
+            document.TextOverlayHandler.Show(textData.Text, textData.Position, textData.Font,
+                textData.TransformationMatrix, textData.Spacing);
+
+            toolbar.Fill = textData.Fill;
+            toolbar.FillColor = textData.FillColor.ToColor();
+            toolbar.StrokeColor = textData.StrokeColor.ToColor();
+            toolbar.ToolSize = textData.StrokeWidth;
+            toolbar.FontFamily = textData.Font.Family;
+            toolbar.FontSize = textData.Font.Size;
+            toolbar.Spacing = textData.Spacing ?? textData.Font.Size;
+            toolbar.Bold = textData.Font.Bold;
+            toolbar.Italic = textData.Font.Italic;
+
+            onPath = textData.Path;
+            lastText = textData.Text;
+            position = textData.Position;
+            lastMatrix = textData.TransformationMatrix;
+        }
+        else if (shape is null)
+        {
+            document.TextOverlayHandler.Show("", controller.LastPrecisePosition, toolbar.ConstructFont(),
+                Matrix3X3.Identity, toolbar.Spacing);
+            lastText = "";
+            position = controller.LastPrecisePosition;
+            // TODO: Implement proper putting on path editing
+            /*if (controller.LeftMousePressed)
+            {
+                TryPutOnPath(controller.LastPrecisePosition);
+            }*/
+        }
+        else
+        {
+            return ExecutionState.Error;
+        }
+
+        return ExecutionState.Success;
+    }
+
+    public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
+    {
+        var topMostWithinClick = QueryLayers<IVectorLayerHandler>(args.PositionOnCanvas);
+
+        var firstLayer = topMostWithinClick.FirstOrDefault();
+        args.Handled = firstLayer != null;
+        if (firstLayer is not IVectorLayerHandler layerHandler)
+        {
+            if (document.TextOverlayHandler.IsActive)
+            {
+                args.Handled = true;
+                document.TextOverlayHandler.Hide();
+            }
+
+            return;
+        }
+
+        document.Operations.SetSelectedMember(layerHandler.Id);
+        document.Operations.InvokeCustomAction(
+            () =>
+            {
+                document.TextOverlayHandler.SetCursorPosition(args.PositionOnCanvas);
+            }, false);
+    }
+
+    public void OnQuickToolSwitch()
+    {
+        document.TextOverlayHandler.SetCursorPosition(internals.ChangeController.LastPrecisePosition);
+    }
+
+    public override void ForceStop()
+    {
+        internals.ActionAccumulator.AddFinishedActions(new EndSetShapeGeometry_Action());
+        document.TextOverlayHandler.Hide();
+    }
+
+    public void OnTextChanged(string text)
+    {
+        var constructedText = ConstructTextData(text);
+        internals.ActionAccumulator.AddFinishedActions(
+            new SetShapeGeometry_Action(selectedMember.Id, constructedText),
+            new EndSetShapeGeometry_Action(),
+            new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering));
+        lastText = text;
+        document.TextOverlayHandler.Font = constructedText.Font;
+    }
+
+    public override void OnSettingsChanged(string name, object value)
+    {
+        if (isListeningForValidLayer)
+        {
+            return;
+        }
+
+        if (name == nameof(ITextToolbar.FontFamily))
+        {
+            Font toDispose = cachedFont;
+            Dispatcher.UIThread.Post(() =>
+            {
+                toDispose?.Dispose();
+            });
+
+            cachedFont = toolbar.ConstructFont();
+            document.TextOverlayHandler.Font = cachedFont;
+        }
+        else
+        {
+            if (cachedFont == null)
+            {
+                cachedFont = toolbar.ConstructFont();
+            }
+
+            document.TextOverlayHandler.Font.Size = toolbar.FontSize;
+            cachedFont.Size = toolbar.FontSize;
+            cachedFont.Bold = toolbar.Bold;
+            cachedFont.Italic = toolbar.Italic;
+        }
+
+        var constructedText = ConstructTextData(lastText);
+        internals.ActionAccumulator.AddActions(
+            new SetShapeGeometry_Action(selectedMember.Id, constructedText),
+            new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering));
+
+        document.TextOverlayHandler.Font = constructedText.Font;
+        document.TextOverlayHandler.Spacing = toolbar.Spacing;
+    }
+
+    public override void OnColorChanged(Color color, bool primary)
+    {
+        if (!primary || !toolbar.SyncWithPrimaryColor)
+        {
+            return;
+        }
+
+        toolbar.StrokeColor = color.ToColor();
+        toolbar.FillColor = color.ToColor();
+    }
+
+    private void TryPutOnPath(VecD pos)
+    {
+        var topMostWithinClick = QueryLayers<IVectorLayerHandler>(pos);
+        var firstValidLayer = topMostWithinClick.FirstOrDefault(x =>
+            x.GetShapeData(document.AnimationHandler.ActiveFrameTime) is not null and not TextVectorData);
+
+        if (firstValidLayer is null)
+        {
+            return;
+        }
+
+        var shape = firstValidLayer.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+
+        ShapeVectorData newShape = (ShapeVectorData)(shape as ShapeVectorData).Clone();
+
+        newShape.Fill = false;
+        newShape.StrokeWidth = 0;
+
+        onPath = newShape.ToPath();
+
+        var constructedText = ConstructTextData(lastText);
+        internals.ActionAccumulator.AddFinishedActions(
+            new SetShapeGeometry_Action(selectedMember.Id, constructedText),
+            new EndSetShapeGeometry_Action(),
+            new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering),
+            new SetShapeGeometry_Action(firstValidLayer.Id, newShape),
+            new EndSetShapeGeometry_Action());
+    }
+
+    private TextVectorData ConstructTextData(string text)
+    {
+        if (cachedFont == null || cachedFont.Family.Name != toolbar.FontFamily.Name)
+        {
+            Font toDispose = cachedFont;
+            Dispatcher.UIThread.Post(() =>
+            {
+                toDispose?.Dispose();
+            });
+            cachedFont = toolbar.ConstructFont();
+        }
+        else
+        {
+            cachedFont.Size = toolbar.FontSize;
+        }
+
+        return new TextVectorData()
+        {
+            Text = text,
+            Position = position,
+            Fill = toolbar.Fill,
+            FillColor = toolbar.FillColor.ToColor(),
+            StrokeWidth = (float)toolbar.ToolSize,
+            StrokeColor = toolbar.StrokeColor.ToColor(),
+            TransformationMatrix = lastMatrix,
+            Font = cachedFont,
+            Spacing = toolbar.Spacing,
+            AntiAlias = toolbar.AntiAliasing,
+            Path = onPath,
+            // TODO: MaxWidth = toolbar.MaxWidth
+            // TODO: Path
+        };
+    }
+
+    bool IExecutorFeature.IsFeatureEnabled(IExecutorFeature feature)
+    {
+        return feature is ITextOverlayEvents || feature is IQuickToolSwitchable;
+    }
+}

+ 2 - 0
src/PixiEditor/Models/Files/IoFileType.cs

@@ -39,6 +39,8 @@ internal abstract class IoFileType
     {
     {
         get { return Extensions.Select(GetExtensionFormattedForDialog).ToList(); }
         get { return Extensions.Select(GetExtensionFormattedForDialog).ToList(); }
     }
     }
+
+    public virtual bool CanSave => true;
     
     
     string GetExtensionFormattedForDialog(string extension)
     string GetExtensionFormattedForDialog(string extension)
     {
     {

+ 18 - 0
src/PixiEditor/Models/Files/OtfFileType.cs

@@ -0,0 +1,18 @@
+using PixiEditor.Models.IO;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Models.Files;
+
+internal class OtfFileType : IoFileType
+{
+    public override string[] Extensions { get; } = new[] { ".otf" };
+    public override string DisplayName { get; } = "OpenType Font";
+    public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Vector;
+
+    public override bool CanSave => false;
+
+    public override Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
+    {
+        throw new NotSupportedException("Saving OTF files is not supported.");
+    }
+}

+ 2 - 0
src/PixiEditor/Models/Files/SvgFileType.cs

@@ -26,6 +26,8 @@ internal class SvgFileType : IoFileType
         job?.Report(0.5, string.Empty); 
         job?.Report(0.5, string.Empty); 
         string xml = svgDocument.ToXml();
         string xml = svgDocument.ToXml();
 
 
+        xml = $"<!-- Created with PixiEditor (https://pixieditor.net) -->{Environment.NewLine}" + xml;
+
         job?.Report(0.75, string.Empty);
         job?.Report(0.75, string.Empty);
         await using FileStream fileStream = new(pathWithExtension, FileMode.Create);
         await using FileStream fileStream = new(pathWithExtension, FileMode.Create);
         await using StreamWriter writer = new(fileStream);
         await using StreamWriter writer = new(fileStream);

+ 18 - 0
src/PixiEditor/Models/Files/TtfFileType.cs

@@ -0,0 +1,18 @@
+using PixiEditor.Models.IO;
+using PixiEditor.ViewModels.Document;
+
+namespace PixiEditor.Models.Files;
+
+internal class TtfFileType : IoFileType
+{
+    public override string[] Extensions { get; } = new[] { ".ttf" };
+    public override string DisplayName { get; } = "TrueType Font";
+    public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Vector;
+
+    public override bool CanSave => false;
+
+    public override Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config, ExportJob? job)
+    {
+        throw new NotSupportedException("Saving TTF files is not supported.");
+    }
+}

+ 6 - 2
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -36,6 +36,7 @@ internal interface IDocument : IHandler
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
     public IReadOnlyCollection<IStructureMemberHandler> SoftSelectedStructureMembers { get; }
     public ITransformHandler TransformHandler { get; }
     public ITransformHandler TransformHandler { get; }
     public IPathOverlayHandler PathOverlayHandler { get; }
     public IPathOverlayHandler PathOverlayHandler { get; }
+    public ITextOverlayHandler TextOverlayHandler { get; }
     public bool Busy { get; set; }
     public bool Busy { get; set; }
     public ILineOverlayHandler LineToolOverlayHandler { get; }
     public ILineOverlayHandler LineToolOverlayHandler { get; }
     public bool HorizontalSymmetryAxisEnabledBindable { get; }
     public bool HorizontalSymmetryAxisEnabledBindable { get; }
@@ -57,8 +58,11 @@ internal interface IDocument : IHandler
     public void UpdateSelectionPath(VectorPath infoNewPath);
     public void UpdateSelectionPath(VectorPath infoNewPath);
     public void SetProcessingColorSpace(ColorSpace infoColorSpace);
     public void SetProcessingColorSpace(ColorSpace infoColorSpace);
     public void SetSize(VecI infoSize);
     public void SetSize(VecI infoSize);
-    public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference, bool includeCanvas, int frame, bool isTopMost);
-    public List<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
+
+    public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference,
+        bool includeCanvas, int frame, bool isTopMost);
+
+    public HashSet<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
     public void UpdateSavedState();
     public void UpdateSavedState();
 
 
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);

+ 1 - 0
src/PixiEditor/Models/Handlers/IDocumentOperations.cs

@@ -12,4 +12,5 @@ internal interface IDocumentOperations
     public void SetSelectedMember(Guid memberId);
     public void SetSelectedMember(Guid memberId);
     public void ClearSoftSelectedMembers();
     public void ClearSoftSelectedMembers();
     public Guid? CreateStructureMember(Type type, ActionSource source, string? name = null);
     public Guid? CreateStructureMember(Type type, ActionSource source, string? name = null);
+    public void InvokeCustomAction(Action action, bool stopActiveExecutor = true);
 }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/ILayerHandler.cs

@@ -3,4 +3,5 @@
 internal interface ILayerHandler : IStructureMemberHandler
 internal interface ILayerHandler : IStructureMemberHandler
 {
 {
     public bool ShouldDrawOnMask { get; set; }
     public bool ShouldDrawOnMask { get; set; }
+    public Type? QuickEditTool { get; }
 }
 }

+ 16 - 0
src/PixiEditor/Models/Handlers/ITextOverlayHandler.cs

@@ -0,0 +1,16 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Text;
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Handlers;
+
+public interface ITextOverlayHandler : IHandler
+{
+    public void Show(string text, VecD position, Font font, Matrix3X3 matrix, double? spacing = null);
+    public void Hide();
+    public Font Font { get; set; }
+    public VecD Position { get; set; }
+    public double? Spacing { get; set; }
+    public bool IsActive { get; }
+    public void SetCursorPosition(VecD closestToPosition);
+}

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

@@ -56,14 +56,17 @@ internal interface IToolHandler : IHandler
 
 
     public virtual string? DefaultNewLayerName => null;
     public virtual string? DefaultNewLayerName => null;
 
 
-    public void ModifierKeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown);
+    public void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey);
     public void UseTool(VecD pos);
     public void UseTool(VecD pos);
     public void OnToolSelected(bool restoring);
     public void OnToolSelected(bool restoring);
 
 
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
     public void SetToolSetSettings(IToolSetHandler toolset, Dictionary<string, object>? settings);
     public void ApplyToolSetSettings(IToolSetHandler toolset);
     public void ApplyToolSetSettings(IToolSetHandler toolset);
     public void OnToolDeselected(bool transient);
     public void OnToolDeselected(bool transient);
-    public void OnPostUndo();
-    public void OnPostRedo();
+    public void OnPostUndoInlet();
+    public void OnPostRedoInlet();
     public void OnActiveFrameChanged(int newFrame);
     public void OnActiveFrameChanged(int newFrame);
+    public void OnPreUndoInlet();
+    public void OnPreRedoInlet();
+    public void QuickToolSwitchInlet();
 }
 }

+ 3 - 0
src/PixiEditor/Models/Handlers/IToolsHandler.cs

@@ -33,4 +33,7 @@ internal interface IToolsHandler : IHandler
     public void AddPropertyChangedCallback(string propertyName, Action callback);
     public void AddPropertyChangedCallback(string propertyName, Action callback);
     public void OnPostUndoInlet();
     public void OnPostUndoInlet();
     public void OnPostRedoInlet();
     public void OnPostRedoInlet();
+    public void OnPreRedoInlet();
+    public void OnPreUndoInlet();
+    public void QuickToolSwitchInlet();
 }
 }

+ 15 - 0
src/PixiEditor/Models/Handlers/Toolbars/ITextToolbar.cs

@@ -0,0 +1,15 @@
+using Drawie.Backend.Core.Text;
+
+namespace PixiEditor.Models.Handlers.Toolbars;
+
+internal interface ITextToolbar : IFillableShapeToolbar
+{
+    public double FontSize { get; set; }
+    public FontFamilyName FontFamily { get; set; }
+    public double Spacing { get; set; }
+    public bool ForceLowDpiRendering { get; set; }
+    public bool Bold { get; set; }
+    public bool Italic { get; set; }
+
+    public Font ConstructFont();
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio