Browse Source

Merge branch 'master' into svg-import

flabbet 8 months ago
parent
commit
5aedecbe74
24 changed files with 297 additions and 45 deletions
  1. 5 1
      src/PixiEditor/Data/Localization/Languages/en.json
  2. 4 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs
  3. 1 0
      src/PixiEditor/Models/Handlers/Toolbars/IToolbar.cs
  4. 2 1
      src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs
  5. 4 2
      src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs
  6. 4 2
      src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs
  7. 4 2
      src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs
  8. 4 2
      src/PixiEditor/Models/Serialization/Factories/RectangleSerializationFactory.cs
  9. 145 6
      src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs
  10. 4 2
      src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs
  11. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  12. 20 4
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  13. 13 4
      src/PixiEditor/Views/Input/ToolSettingColorPicker.axaml.cs
  14. 1 0
      src/PixiEditor/Views/Main/Tools/Toolbar.axaml
  15. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  16. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  17. 14 2
      src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs
  18. 26 5
      src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs
  19. 10 0
      src/PixiEditor/Views/Overlays/PathOverlay/ShapePoint.cs
  20. 2 0
      src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs
  21. 23 4
      src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs
  22. 1 1
      src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml
  23. 5 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml
  24. 1 0
      src/PixiEditor/Views/Tools/ToolSettings/Settings/FloatSettingView.axaml

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

@@ -783,5 +783,9 @@
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "FILL_MODE": "Fill mode",
   "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 legacy 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_EVEN_ODD": "Even Odd",
+  "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
+  "FILL_TYPE_INVERSE_EVEN_ODD": "Inverse Even Odd"
 }

+ 4 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorPathToolExecutor.cs

@@ -17,6 +17,7 @@ using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors.Features;
 using PixiEditor.Models.Handlers.Toolbars;
 using PixiEditor.Models.Tools;
 using PixiEditor.ViewModels.Tools.Tools;
+using PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 using PixiEditor.Views.Overlays.PathOverlay;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Colors = Drawie.Backend.Core.ColorsImpl.Colors;
@@ -193,7 +194,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
     {
         if(startingPath == null)
         {
-            return new PathVectorData(new VectorPath() { FillType = vectorPathToolHandler.FillMode })
+            return new PathVectorData(new VectorPath() { FillType = (PathFillType)vectorPathToolHandler.FillMode })
             {
                 StrokeWidth = (float)toolbar.ToolSize,
                 StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -201,7 +202,7 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
             };
         }
         
-        return new PathVectorData(new VectorPath(startingPath) { FillType = vectorPathToolHandler.FillMode })
+        return new PathVectorData(new VectorPath(startingPath) { FillType = (PathFillType)vectorPathToolHandler.FillMode })
         {
             StrokeWidth = (float)toolbar.ToolSize,
             StrokeColor = toolbar.StrokeColor.ToColor(),
@@ -264,6 +265,6 @@ internal class VectorPathToolExecutor : UpdateableChangeExecutor, IPathExecutorF
         toolbar.ToolSize = pathData.StrokeWidth;
         toolbar.Fill = pathData.Fill;
         toolbar.FillColor = pathData.FillColor.ToColor();
-        toolbar.GetSetting(nameof(VectorPathToolViewModel.FillMode)).Value = pathData.Path.FillType;
+        toolbar.GetSetting<EnumSettingViewModel<VectorPathFillType>>(nameof(VectorPathToolViewModel.FillMode)).Value = (VectorPathFillType)pathData.Path.FillType;
     }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/Toolbars/IToolbar.cs

@@ -8,6 +8,7 @@ internal interface IToolbar : IHandler
 {
     public void AddSetting(Setting setting);
     public Setting GetSetting(string name);
+    public T GetSetting<T>(string name) where T : Setting;
     public IReadOnlyList<Setting> Settings { get; }
     public void SaveToolbarSettings();
     public void LoadSharedSettings();

+ 2 - 1
src/PixiEditor/Models/Handlers/Tools/IVectorPathToolHandler.cs

@@ -1,8 +1,9 @@
 using Drawie.Backend.Core.Vector;
+using PixiEditor.ViewModels.Tools.Tools;
 
 namespace PixiEditor.Models.Handlers.Tools;
 
 internal interface IVectorPathToolHandler : IToolHandler
 {
-    public PathFillType FillMode { get; }
+    public VectorPathFillType FillMode { get; }
 }

+ 4 - 2
src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs

@@ -15,8 +15,10 @@ public class EllipseSerializationFactory : VectorShapeSerializationFactory<Ellip
         builder.AddVecD(original.Radius);
     }
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out EllipseVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out EllipseVectorData original)
     {
         VecD center = extractor.GetVecD();
         VecD radius = extractor.GetVecD();

+ 4 - 2
src/PixiEditor/Models/Serialization/Factories/LineSerializationFactory.cs

@@ -15,8 +15,10 @@ internal class LineSerializationFactory : VectorShapeSerializationFactory<LineVe
         builder.AddVecD(original.End);
     }
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out LineVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out LineVectorData original)
     {
         VecD start = extractor.GetVecD();
         VecD end = extractor.GetVecD();

+ 4 - 2
src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs

@@ -13,8 +13,10 @@ internal class PointsDataSerializationFactory : VectorShapeSerializationFactory<
         builder.AddVecDList(original.Points);
     }
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out PointsVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out PointsVectorData original)
     {
         List<VecD> points = extractor.GetVecDList();
         original = new PointsVectorData(points)

+ 4 - 2
src/PixiEditor/Models/Serialization/Factories/RectangleSerializationFactory.cs

@@ -16,8 +16,10 @@ internal class RectangleSerializationFactory : VectorShapeSerializationFactory<R
         builder.AddVecD(original.Size);
     }
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out RectangleVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out RectangleVectorData original)
     {
         VecD center = extractor.GetVecD();
         VecD size = extractor.GetVecD();

+ 145 - 6
src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs

@@ -3,24 +3,49 @@ using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using PixiEditor.Views.Overlays.PathOverlay;
 
 namespace PixiEditor.Models.Serialization.Factories;
 
-internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<PathVectorData> 
+internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<PathVectorData>
 {
     public override string DeserializationId { get; } = "PixiEditor.PathData";
 
     protected override void AddSpecificData(ByteBuilder builder, PathVectorData original)
     {
-        builder.AddString(original.Path.ToSvgPathData());
+        if (original.Path == null)
+        {
+            return;
+        }
+
+        EditableVectorPath path = new EditableVectorPath(original.Path);
+
+        builder.AddInt((int)path.Path.FillType);
+        builder.AddInt(path.SubShapes.Count);
+
+        foreach (var subShape in path.SubShapes)
+        {
+            SerializeSubShape(builder, subShape);
+        }
     }
 
-    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor,
-        float strokeWidth, out PathVectorData original)
+    protected override bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor,
+        float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out PathVectorData original)
     {
-        string path = extractor.GetString();
+        VectorPath path;
+        if (IsOldSerializer(serializerData))
+        {
+            string svgPath = extractor.GetString();
+            path = VectorPath.FromSvgPath(svgPath);
+        }
+        else
+        {
+            path = DeserializePath(extractor).ToVectorPath();
+        }
 
-        original = new PathVectorData(VectorPath.FromSvgPath(path))
+        original = new PathVectorData(path)
         {
             StrokeColor = strokeColor,
             FillColor = fillColor,
@@ -30,4 +55,118 @@ internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<
 
         return true;
     }
+
+    private void SerializeSubShape(ByteBuilder builder, SubShape subShape)
+    {
+        builder.AddInt(subShape.IsClosed ? 1 : 0);
+        builder.AddInt(subShape.Points.Count);
+
+        foreach (var point in subShape.Points)
+        {
+            builder.AddFloat(point.Position.X);
+            builder.AddFloat(point.Position.Y);
+
+            builder.AddInt(point.Verb.IsEmptyVerb() ? -1 : (int)point.Verb.VerbType);
+            builder.AddFloat(point.Verb.From.X);
+            builder.AddFloat(point.Verb.From.Y);
+            
+            builder.AddFloat(point.Verb.To.X);
+            builder.AddFloat(point.Verb.To.Y);
+
+            if (HasControlPoint1(point))
+            {
+                builder.AddFloat(point.Verb.ControlPoint1.Value.X);
+                builder.AddFloat(point.Verb.ControlPoint1.Value.Y);
+            }
+
+            if (HasControlPoint2(point))
+            {
+                builder.AddFloat(point.Verb.ControlPoint2.Value.X);
+                builder.AddFloat(point.Verb.ControlPoint2.Value.Y);
+            }
+
+            if (IsConic(point))
+            {
+                builder.AddFloat(point.Verb.ConicWeight);
+            }
+        }
+    }
+    
+    private EditableVectorPath DeserializePath(ByteExtractor extractor)
+    {
+        PathFillType fillType = (PathFillType)extractor.GetInt();
+        int subShapesCount = extractor.GetInt();
+        List<SubShape> subShapes = new List<SubShape>();
+
+        for (int i = 0; i < subShapesCount; i++)
+        {
+            SubShape subShape = DeserializeSubShape(extractor);
+            subShapes.Add(subShape);
+        }
+
+        return new EditableVectorPath(subShapes, fillType);
+    }
+    
+    private SubShape DeserializeSubShape(ByteExtractor extractor)
+    {
+        bool isClosed = extractor.GetInt() == 1;
+        int pointsCount = extractor.GetInt();
+        List<ShapePoint> points = new List<ShapePoint>();
+
+        for (int i = 0; i < pointsCount; i++)
+        {
+            VecF position = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            PathVerb verbType = (PathVerb)extractor.GetInt();
+            
+            VecF from = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            VecF to = new VecF(extractor.GetFloat(), extractor.GetFloat());
+            
+            VecF? controlPoint1 = verbType is PathVerb.Cubic or PathVerb.Quad or PathVerb.Conic
+                ? new VecF(extractor.GetFloat(), extractor.GetFloat())
+                : null;
+            VecF? controlPoint2 = verbType is PathVerb.Cubic or PathVerb.Quad
+                ? new VecF(extractor.GetFloat(), extractor.GetFloat())
+                : null;
+            float conicWeight = verbType == PathVerb.Conic ? extractor.GetFloat() : 0;
+
+            Verb verb = new Verb(verbType, from, to, controlPoint1, controlPoint2, conicWeight);
+            
+            points.Add(new ShapePoint(position, points.Count, verb));
+        }
+
+        return new SubShape(points, isClosed);
+    }
+
+    private bool HasControlPoint1(ShapePoint point)
+    {
+        return (point.Verb.VerbType is PathVerb.Conic or PathVerb.Cubic or PathVerb.Quad) &&
+               point.Verb.ControlPoint1.HasValue;
+    }
+
+    private bool HasControlPoint2(ShapePoint point)
+    {
+        return (point.Verb.VerbType is PathVerb.Cubic or PathVerb.Quad) &&
+               point.Verb.ControlPoint2.HasValue;
+    }
+
+    private bool IsConic(ShapePoint point)
+    {
+        return point.Verb.VerbType == PathVerb.Conic;
+    }
+
+    private bool IsOldSerializer((string serializerName, string serializerVersion) serializerData)
+    {
+        if (string.IsNullOrEmpty(serializerData.serializerName) ||
+            string.IsNullOrEmpty(serializerData.serializerVersion))
+        {
+            return false;
+        }
+
+        if (Version.TryParse(serializerData.serializerVersion, out Version version))
+        {
+            return version is { Major: 2, Minor: 0, Build: 0, Revision: < 35 };
+        }
+
+        return false;
+    }
 }

+ 4 - 2
src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs

@@ -46,8 +46,10 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
             strokeWidth = extractor.GetFloat();
         }
 
-        return DeserializeVectorData(extractor, matrix, strokeColor, fillColor, strokeWidth, out original);
+        return DeserializeVectorData(extractor, matrix, strokeColor, fillColor, strokeWidth, serializerData, out original);
     }
     
-    protected abstract bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor, Color fillColor, float strokeWidth, out T original);
+    protected abstract bool DeserializeVectorData(ByteExtractor extractor, Matrix3X3 matrix, Color strokeColor,
+        Color fillColor, float strokeWidth, (string serializerName, string serializerVersion) serializerData,
+        out T original);
 }

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

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

+ 20 - 4
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -1,4 +1,5 @@
-using Avalonia.Input;
+using System.ComponentModel;
+using Avalonia.Input;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
@@ -33,10 +34,10 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
     private LocalizedString actionDisplayShift;
     private LocalizedString actionDisplayCtrlShift;
 
-    [Settings.Enum("FILL_MODE", PathFillType.Winding)]
-    public PathFillType FillMode
+    [Settings.Enum("FILL_MODE", VectorPathFillType.Winding)]
+    public VectorPathFillType FillMode
     {
-        get => GetValue<PathFillType>();
+        get => GetValue<VectorPathFillType>();
     }
 
     public VectorPathToolViewModel()
@@ -119,3 +120,18 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         OnSelected(false);
     }
 }
+
+enum VectorPathFillType
+{
+    [Description("FILL_TYPE_WINDING")]
+    
+    Winding,
+    [Description("FILL_TYPE_EVEN_ODD")]
+    EvenOdd,
+    
+    [Description("FILL_TYPE_INVERSE_WINDING")]
+    InverseWinding,
+    
+    [Description("FILL_TYPE_INVERSE_EVEN_ODD")]
+    InverseEvenOdd
+}

+ 13 - 4
src/PixiEditor/Views/Input/ToolSettingColorPicker.axaml.cs

@@ -1,25 +1,34 @@
 using Avalonia;
 using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
 using Avalonia.Media;
 using Avalonia.Threading;
+using Avalonia.VisualTree;
+using PixiEditor.Helpers.Behaviours;
 
 namespace PixiEditor.Views.Input;
 
 internal partial class ToolSettingColorPicker : UserControl
 {
-    public static readonly StyledProperty<Color> SelectedColorProperty = AvaloniaProperty.Register<ToolSettingColorPicker, Color>(
-        nameof(SelectedColor));
+    public static readonly StyledProperty<Color> SelectedColorProperty =
+        AvaloniaProperty.Register<ToolSettingColorPicker, Color>(
+            nameof(SelectedColor));
 
     public Color SelectedColor
     {
         get => GetValue(SelectedColorProperty);
         set => SetValue(SelectedColorProperty, value);
     }
-    
+
     public ToolSettingColorPicker()
     {
         InitializeComponent();
         ColorPicker.SecondaryColor = Colors.Black;
+        ColorPicker.TemplateApplied += ColorPickerOnTemplateApplied;
     }
-}
 
+    private void ColorPickerOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
+    {
+        ColorPicker.FindDescendantOfType<ToggleButton>().Focusable = false;
+    }
+}

+ 1 - 0
src/PixiEditor/Views/Main/Tools/Toolbar.axaml

@@ -12,6 +12,7 @@
             BorderBrush="{DynamicResource ThemeBorderMidBrush}"
             BorderThickness="{DynamicResource ThemeBorderThickness}"
             Cursor="Arrow"
+            IsHitTestVisible="True"
             Padding="5"
             Height="40"
             HorizontalAlignment="Left"

+ 1 - 1
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -162,7 +162,7 @@
                     ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}"
                     VerticalAlignment="Center" />
             </Border>
-            <tools:ToolsPicker Grid.Row="2" 
+            <tools:ToolsPicker Grid.Row="2" IsHitTestVisible="True"
                 HorizontalAlignment="Left"
                 ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet, Mode=TwoWay}"
                                ToolSets="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.AllToolSets, Mode=OneWay}"

+ 1 - 1
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -394,7 +394,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
     private void Image_MouseDown(object? sender, PointerPressedEventArgs e)
     {
-        if (Document is null)
+        if (Document is null || e.Source != Scene)
             return;
 
         bool isMiddle = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;

+ 14 - 2
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -111,6 +111,7 @@ internal class LineToolOverlay : Overlay
 
         startHandle = new AnchorHandle(this);
         startHandle.StrokePaint = blackPaint;
+        startHandle.OnPress += OnHandlePress;
         startHandle.OnDrag += StartHandleOnDrag;
         startHandle.OnHover += (handle, _) => Refresh();
         startHandle.OnRelease += OnHandleRelease;
@@ -119,6 +120,7 @@ internal class LineToolOverlay : Overlay
 
         endHandle = new AnchorHandle(this);
         endHandle.StrokePaint = blackPaint;
+        endHandle.OnPress += OnHandlePress;
         endHandle.OnDrag += EndHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         endHandle.OnHover += (handle, _) => Refresh();
@@ -127,6 +129,7 @@ internal class LineToolOverlay : Overlay
 
         moveHandle = new TransformHandle(this);
         moveHandle.StrokePaint = blackPaint;
+        moveHandle.OnPress += OnHandlePress;
         moveHandle.OnDrag += MoveHandleOnDrag;
         endHandle.Cursor = new Cursor(StandardCursorType.Arrow);
         moveHandle.OnHover += (handle, _) => Refresh();
@@ -192,10 +195,10 @@ internal class LineToolOverlay : Overlay
             var matrix = context.TotalMatrix;
             VecD pos = matrix.MapPoint(lastMousePos);
             context.SetMatrix(Matrix3X3.Identity);
-            
+
             string length = $"L: {(mappedEnd - mappedStart).Length:0.#} px";
             infoBox.DrawInfo(context, length, pos);
-            
+
             context.RestoreToCount(toRestore);
         }
     }
@@ -263,6 +266,15 @@ internal class LineToolOverlay : Overlay
         return final;
     }
 
+    private void OnHandlePress(Handle source, OverlayPointerArgs args)
+    {
+        movedWhileMouseDown = false;
+        mouseDownPos = args.Point;
+
+        lineStartOnMouseDown = LineStart;
+        lineEndOnMouseDown = LineEnd;
+    }
+
     private void MoveHandleOnDrag(Handle source, OverlayPointerArgs args)
     {
         var delta = args.Point - mouseDownPos;

+ 26 - 5
src/PixiEditor/Views/Overlays/PathOverlay/EditableVectorPath.cs

@@ -5,9 +5,9 @@ namespace PixiEditor.Views.Overlays.PathOverlay;
 
 public class EditableVectorPath
 {
-    private VectorPath path;
+    private VectorPath? path;
 
-    public VectorPath Path
+    public VectorPath? Path
     {
         get => path;
         set
@@ -23,6 +23,8 @@ public class EditableVectorPath
 
     public int TotalPoints => subShapes.Sum(x => x.Points.Count);
 
+    public PathFillType FillType { get; set; }
+
     public int ControlPointsCount
     {
         get
@@ -32,6 +34,12 @@ public class EditableVectorPath
         }
     }
 
+    public EditableVectorPath(IEnumerable<SubShape> subShapes, PathFillType fillType)
+    {
+        this.subShapes = new List<SubShape>(subShapes);
+        FillType = fillType;
+    }
+
     public EditableVectorPath(VectorPath path)
     {
         if (path != null)
@@ -47,8 +55,19 @@ public class EditableVectorPath
 
     public VectorPath ToVectorPath()
     {
-        VectorPath newPath = new VectorPath(Path);
-        newPath.Reset(); // preserve fill type and other properties
+        VectorPath newPath;
+        if (Path != null)
+        {
+            newPath = new VectorPath(Path);
+            newPath.Reset(); // preserve fill type and other properties
+        }
+        else
+        {
+            newPath = new VectorPath();
+        }
+        
+        newPath.FillType = FillType;
+
         foreach (var subShape in subShapes)
         {
             AddVerbToPath(CreateMoveToVerb(subShape), newPath);
@@ -127,7 +146,7 @@ public class EditableVectorPath
                     {
                         subShapes.Add(new SubShape(currentSubShapePoints, isSubShapeClosed));
                         currentSubShapePoints.Clear();
-                        
+
                         currentSubShapePoints.Add(new ShapePoint(data.points[0], 0, new Verb()));
                     }
                     else
@@ -143,6 +162,8 @@ public class EditableVectorPath
 
             globalVerbIndex++;
         }
+
+        FillType = from.FillType;
     }
 
     private void AddVerbToPath(Verb verb, VectorPath newPath)

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

@@ -78,6 +78,16 @@ public class Verb
         VerbType = null;
     }
     
+    public Verb(PathVerb verb, VecF from, VecF to, VecF? controlPoint1, VecF? controlPoint2, float conicWeight)
+    {
+        VerbType = verb;
+        From = from;
+        To = to;
+        ControlPoint1 = controlPoint1;
+        ControlPoint2 = controlPoint2;
+        ConicWeight = conicWeight;
+    }
+    
     public Verb((PathVerb verb, VecF[] points, float conicWeight) verbData)
     {
         VerbType = verbData.verb;

+ 2 - 0
src/PixiEditor/Views/Overlays/SymmetryOverlay/SymmetryOverlay.cs

@@ -359,6 +359,8 @@ internal class SymmetryOverlay : Overlay
 
             CallSymmetryDragCommand((SymmetryAxisDirection)capturedDirection, verticalAxisX);
         }
+        
+        Refresh();
     }
 
     protected override void OnOverlayPointerExited(OverlayPointerArgs args)

+ 23 - 4
src/PixiEditor/Views/Overlays/TransformOverlay/TransformOverlay.cs

@@ -306,13 +306,16 @@ internal class TransformOverlay : Overlay
         ForAllHandles<AnchorHandle>(x =>
         {
             x.OnPress += OnAnchorHandlePressed;
+            x.OnDrag += OnAnchorHandleDrag;
             x.OnRelease += OnAnchorHandleReleased;
         });
 
         originHandle.OnPress += OnAnchorHandlePressed;
+        originHandle.OnDrag += OnAnchorHandleDrag;
         originHandle.OnRelease += OnAnchorHandleReleased;
 
         moveHandle.OnPress += OnMoveHandlePressed;
+        moveHandle.OnDrag += OnMoveHandleDrag;
         moveHandle.OnRelease += OnMoveHandleReleased;
 
         infoBox = new InfoBox();
@@ -506,6 +509,20 @@ internal class TransformOverlay : Overlay
         args.Handled = true;
     }
 
+    private void OnAnchorHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        HandleCapturedAnchorMovement(args.Point);
+        lastPointerPos = args.Point;
+    }
+
+    private void OnMoveHandleDrag(Handle source, OverlayPointerArgs args)
+    {
+        HandleTransform(lastPointerPos);
+        Cursor = new Cursor(StandardCursorType.DragMove);
+        actuallyMoved = true;
+        lastPointerPos = args.Point;
+    }
+
     protected override void OnOverlayPointerMoved(OverlayPointerArgs e)
     {
         Cursor finalCursor = new Cursor(StandardCursorType.Arrow);
@@ -522,7 +539,7 @@ internal class TransformOverlay : Overlay
 
         if (capturedAnchor is not null)
         {
-            HandleCapturedAnchorMovement(e);
+            HandleCapturedAnchorMovement(e.Point);
             return;
         }
 
@@ -594,7 +611,7 @@ internal class TransformOverlay : Overlay
             TopLeft = scaled.TopLeft - new VecD(offsetToScale, offsetToScale),
             TopRight = scaled.TopRight - new VecD(-offsetToScale, offsetToScale),
         };
-        
+
         scaledCorners = scaledCorners.AsRotated(Corners.RectRotation, Corners.RectCenter);
 
         return base.TestHit(point) || scaledCorners.IsPointInside(point);
@@ -719,7 +736,7 @@ internal class TransformOverlay : Overlay
         return true;
     }
 
-    private void HandleCapturedAnchorMovement(OverlayPointerArgs e)
+    private void HandleCapturedAnchorMovement(VecD point)
     {
         if (capturedAnchor is null)
             throw new InvalidOperationException("No anchor is captured");
@@ -728,7 +745,7 @@ internal class TransformOverlay : Overlay
             (TransformHelper.IsSide((Anchor)capturedAnchor) && SideFreedom == TransformSideFreedom.Locked))
             return;
 
-        pos = e.Point;
+        pos = point;
 
         if (TransformHelper.IsCorner((Anchor)capturedAnchor))
         {
@@ -1073,6 +1090,8 @@ internal class TransformOverlay : Overlay
 
         if (ActionCompleted is not null && ActionCompleted.CanExecute(null))
             ActionCompleted.Execute(null);
+
+        IsSizeBoxEnabled = false;
     }
 
     private Handle? GetSnapHandleOfOrigin()

+ 1 - 1
src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml

@@ -9,5 +9,5 @@
         <settings:BoolSettingViewModel/>
     </Design.DataContext>
     
-    <CheckBox VerticalAlignment="Center" IsChecked="{Binding Value, Mode=TwoWay}"/>
+    <CheckBox VerticalAlignment="Center" Focusable="False" IsChecked="{Binding Value, Mode=TwoWay}"/>
 </UserControl>

+ 5 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/EnumSettingView.axaml

@@ -22,5 +22,10 @@
                 <Setter Property="(ui:Translator.Key)" Value="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
             </ControlTheme>
         </ComboBox.ItemContainerTheme>
+       <ComboBox.ItemTemplate>
+           <DataTemplate>
+               <TextBlock ui:Translator.Key="{Binding ., Converter={helpers:EnumDescriptionConverter}}"/>
+           </DataTemplate>
+       </ComboBox.ItemTemplate>
     </ComboBox>
 </UserControl>

+ 1 - 0
src/PixiEditor/Views/Tools/ToolSettings/Settings/FloatSettingView.axaml

@@ -14,5 +14,6 @@
                        Min="{Binding Min}"
                        Max="{Binding Max}"
                        Margin="0,0,0,0" 
+                       FocusNext="False"
                        Width="40" />
 </UserControl>