Browse Source

Merge pull request #963 from PixiEditor/fixes/3.06.2025

Fixes/3.06.2025
Krzysztof Krysiński 2 months ago
parent
commit
532dd74dd1

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 13d7c363f5bee90cbde15f1c98cf1744b2bcd493
+Subproject commit 3b9c90113ed6b7600d1b80828d3ddf1d70ead90a

+ 22 - 0
src/PixiEditor.SVG/Elements/SvgPolygon.cs

@@ -0,0 +1,22 @@
+using System.Xml;
+using Drawie.Numerics;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgPolygon() : SvgPrimitive("polygon")
+{
+    public SvgProperty<SvgStringUnit> RawPoints { get; } = new SvgProperty<SvgStringUnit>("points");
+    public SvgProperty<SvgNumericUnit> PathLength { get; set; } = new SvgProperty<SvgNumericUnit>("pathLength");
+
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return RawPoints;
+        yield return PathLength;
+    }
+
+    public VecD[] GetPoints()
+    {
+        return SvgPolyline.GetPoints(RawPoints.Unit?.Value ?? string.Empty);
+    }
+}

+ 83 - 0
src/PixiEditor.SVG/Elements/SvgPolyline.cs

@@ -0,0 +1,83 @@
+using System.Xml;
+using Drawie.Numerics;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgPolyline() : SvgPrimitive("polyline")
+{
+    public SvgProperty<SvgStringUnit> RawPoints { get; } = new SvgProperty<SvgStringUnit>("points");
+    public SvgProperty<SvgNumericUnit> PathLength { get; set; } = new SvgProperty<SvgNumericUnit>("pathLength");
+
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return RawPoints;
+        yield return PathLength;
+    }
+
+    public VecD[] GetPoints()
+    {
+        return GetPoints(RawPoints.Unit?.Value ?? string.Empty);
+    }
+
+    public static VecD[] GetPoints(string input)
+    {
+        if (string.IsNullOrWhiteSpace(input))
+        {
+            return [];
+        }
+
+        double? x = null;
+        bool nextSpaceIsSeparator = false;
+        string currentNumberString = string.Empty;
+
+        List<VecD> points = new List<VecD>();
+        foreach (char character in input)
+        {
+            if (char.IsWhiteSpace(character))
+            {
+                if (nextSpaceIsSeparator)
+                {
+                    if (x.HasValue)
+                    {
+                        points.Add(new VecD(x.Value, ParseNumber(currentNumberString)));
+                        x = null;
+                        currentNumberString = string.Empty;
+                    }
+
+                    nextSpaceIsSeparator = false;
+                }
+            }
+            else if (char.IsDigit(character) || character == '.' || character == '-' || character == '+')
+            {
+                currentNumberString += character;
+                nextSpaceIsSeparator = x.HasValue;
+            }
+            else if (character == ',')
+            {
+                x = ParseNumber(currentNumberString);
+                currentNumberString = string.Empty;
+                nextSpaceIsSeparator = false;
+            }
+        }
+
+        if (currentNumberString.Length > 0)
+        {
+            if (x.HasValue)
+            {
+                points.Add(new VecD(x.Value, ParseNumber(currentNumberString)));
+            }
+            else
+            {
+                points.Add(new VecD(ParseNumber(currentNumberString), 0));
+            }
+        }
+
+        return points.ToArray();
+    }
+
+    private static double ParseNumber(string currentNumberString)
+    {
+        return double.Parse(currentNumberString, System.Globalization.CultureInfo.InvariantCulture);
+    }
+}

+ 2 - 15
src/PixiEditor.SVG/SvgElement.cs

@@ -113,20 +113,7 @@ public class SvgElement(string tagName)
 
     private void ParseAttribute(SvgProperty property, XmlReader reader, SvgDefs defs)
     {
-        if (property is SvgList list)
-        {
-            ParseListProperty(list, reader, defs);
-        }
-        else
-        {
-            property.Unit ??= property.CreateDefaultUnit();
-            property.Unit.ValuesFromXml(reader.Value, defs);
-        }
-    }
-
-    private void ParseListProperty(SvgList list, XmlReader reader, SvgDefs defs)
-    {
-        list.Unit ??= list.CreateDefaultUnit();
-        list.Unit.ValuesFromXml(reader.Value, defs);
+        property.Unit ??= property.CreateDefaultUnit();
+        property.Unit.ValuesFromXml(reader.Value, defs);
     }
 }

+ 16 - 19
src/PixiEditor.SVG/SvgParser.cs

@@ -1,4 +1,5 @@
 using System.Globalization;
+using System.Reflection;
 using System.Xml;
 using System.Xml.Linq;
 using Drawie.Numerics;
@@ -10,24 +11,10 @@ namespace PixiEditor.SVG;
 
 public class SvgParser
 {
-    private static Dictionary<string, Type> wellKnownElements = new()
-    {
-        { "ellipse", typeof(SvgEllipse) },
-        { "rect", typeof(SvgRectangle) },
-        { "circle", typeof(SvgCircle) },
-        { "line", typeof(SvgLine) },
-        { "path", typeof(SvgPath) },
-        { "g", typeof(SvgGroup) },
-        { "mask", typeof(SvgMask) },
-        { "image", typeof(SvgImage) },
-        { "svg", typeof(SvgDocument) },
-        { "text", typeof(SvgText) },
-        { "linearGradient", typeof(SvgLinearGradient) },
-        { "radialGradient", typeof(SvgRadialGradient) },
-        { "stop", typeof(SvgStop) },
-        { "defs", typeof(SvgDefs) },
-        { "clipPath", typeof(SvgClipPath) }
-    };
+    private static Dictionary<string, Type> wellKnownElements = Assembly.GetExecutingAssembly()
+        .GetTypes()
+        .Where(t => t.IsSubclassOf(typeof(SvgElement)) && !t.IsAbstract)
+        .ToDictionary(t => (Activator.CreateInstance(t) as SvgElement).TagName, t => t);
 
     public string Source { get; set; }
 
@@ -60,12 +47,22 @@ public class SvgParser
         {
             if (reader.NodeType == XmlNodeType.Element)
             {
+                if (reader.LocalName == "defs")
+                {
+                    // already parsed defs, skip
+                    reader.Skip();
+                    if(reader.NodeType != XmlNodeType.Element)
+                    {
+                        continue;
+                    }
+                }
+
                 SvgElement? element = ParseElement(reader, root.Defs);
                 if (element != null)
                 {
                     root.Children.Add(element);
 
-                    if (element is IElementContainer container && element.TagName != "defs")
+                    if (element is IElementContainer container)
                     {
                         ParseChildren(reader, container, root.Defs, element.TagName);
                     }

+ 2 - 0
src/PixiEditor.SVG/SvgProperty.cs

@@ -20,6 +20,8 @@ public abstract class SvgProperty
     public ISvgUnit? Unit { get; set; }
     public string? SvgFullName => NamespaceName == null ? SvgName : $"{NamespaceName}:{SvgName}";
 
+    protected Type? TypeToCreate { get; set; }
+
     public ISvgUnit? CreateDefaultUnit()
     {
         var genericType = this.GetType().GetGenericArguments();

+ 0 - 19
src/PixiEditor.SVG/Units/SvgList.cs

@@ -1,19 +0,0 @@
-namespace PixiEditor.SVG.Units;
-
-public class SvgList : SvgProperty
-{
-    public char Separator { get; set; }
-    public SvgList(string svgName, char separator) : base(svgName)
-    {
-        Separator = separator;
-    }
-}
-
-public class SvgList<T> : SvgList where T : ISvgUnit
-{
-
-    public SvgList(string svgName, char separator, params T[] units) : base(svgName, separator)
-    {
-        
-    }
-}

+ 50 - 0
src/PixiEditor.SVG/Units/SvgVectorUnit.cs

@@ -0,0 +1,50 @@
+using Drawie.Numerics;
+using PixiEditor.SVG.Elements;
+
+namespace PixiEditor.SVG.Units;
+
+public class SvgVectorUnit : ISvgUnit
+{
+    public SvgNumericUnit X { get; set; }
+    public SvgNumericUnit Y { get; set; }
+
+    public SvgVectorUnit()
+    {
+        X = new SvgNumericUnit();
+        Y = new SvgNumericUnit();
+    }
+
+    public SvgVectorUnit(VecD vector)
+    {
+        X = new SvgNumericUnit(vector.X, "");
+        Y = new SvgNumericUnit(vector.Y, "");
+    }
+
+    public string ToXml(DefStorage defs)
+    {
+        string xValue = X.ToXml(defs);
+        string yValue = Y.ToXml(defs);
+
+        return $"{xValue},{yValue}";
+    }
+
+    public void ValuesFromXml(string readerValue, SvgDefs defs)
+    {
+        if (string.IsNullOrEmpty(readerValue))
+        {
+            X = new SvgNumericUnit();
+            Y = new SvgNumericUnit();
+            return;
+        }
+
+        string[] values =
+            readerValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+        if (values.Length != 2)
+        {
+            throw new FormatException("Invalid vector format. Expected two values.");
+        }
+
+        X.ValuesFromXml(values[0], defs);
+        Y.ValuesFromXml(values[1], defs);
+    }
+}

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


+ 2 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -23,6 +23,7 @@
             <system:String x:Key="icon-center">&#xE92B;</system:String>
             <system:String x:Key="icon-chain">&#xE990;</system:String>
             <system:String x:Key="icon-check">&#xE98A;</system:String>
+            <system:String x:Key="icon-check-tick">&#xE97E;</system:String>
             <system:String x:Key="icon-chevron-down">&#xE92C;</system:String>
             <system:String x:Key="icon-chevron-left">&#xE92D;</system:String>
             <system:String x:Key="icon-chevron-right">&#xE92E;</system:String>
@@ -188,6 +189,7 @@
             <system:String x:Key="icon-up-vector">&#xE91A;</system:String>
             <system:String x:Key="icon-upload">&#xE98D;</system:String>
             <system:String x:Key="icon-upload-cloud">&#xE98E;</system:String>
+            <system:String x:Key="icon-user">&#xE97B;</system:String>
             <system:String x:Key="icon-vector-pen">&#xE965;</system:String>
             <system:String x:Key="icon-write">&#xE988;</system:String>
             <system:String x:Key="icon-x-flip">&#xE900;</system:String>

+ 2 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -19,6 +19,7 @@ public static partial class PixiPerfectIcons
     public const string Center = "\uE92B";
     public const string Chain = "\uE990";
     public const string Check = "\uE98A";
+    public const string CheckTick = "\uE97E";
     public const string ChevronDown = "\uE92C";
     public const string ChevronLeft = "\uE92D";
     public const string ChevronRight = "\uE92E";
@@ -184,6 +185,7 @@ public static partial class PixiPerfectIcons
     public const string UpVector = "\uE91A";
     public const string Upload = "\uE98D";
     public const string UploadCloud = "\uE98E";
+    public const string User = "\uE97B";
     public const string VectorPen = "\uE965";
     public const string Write = "\uE988";
     public const string XFlip = "\uE900";

+ 0 - 155
src/PixiEditor.UI.Common/Fonts/PixiPerfectStyles.axaml.cs

@@ -1,155 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Layout;
-using Avalonia.Media;
-using Avalonia.Platform;
-using PixiEditor.UI.Common.Rendering;
-
-namespace PixiEditor.UI.Common.Fonts;
-
-//TODO: Make autogenerated from PixiPerfectIcons.axaml
-public static partial class PixiPerfectIcons
-{
-    public const string AddReference = "\ue900";
-    public const string AddToMask = "\ue901";
-    public const string AlphaLock = "\ue902";
-    public const string AlphaUnlock = "\ue903";
-    public const string ArrowDown = "\ue904";
-    public const string ArrowLeft = "\ue905";
-    public const string ArrowRight = "\ue906";
-    public const string ArrowUp = "\ue907";
-    public const string Book = "\ue908";
-    public const string Bucket = "\ue909";
-    public const string CanvasResize = "\ue90a";
-    public const string Center = "\ue90b";
-    public const string ChevronDown = "\ue90c";
-    public const string ChevronLeft = "\ue90d";
-    public const string ChevronRight = "\ue90e";
-    public const string ChevronUp = "\ue90f";
-    public const string Circle = "\ue910";
-    public const string Clock = "\ue911";
-    public const string ColorPalette = "\ue912";
-    public const string ColorPicker = "\ue913";
-    public const string ColorSliders = "\ue914";
-    public const string ColorsSwap = "\ue915";
-    public const string Compass = "\ue916";
-    public const string Copy = "\ue917";
-    public const string CornerUpLeft = "\ue918";
-    public const string CornerUpRight = "\ue919";
-    public const string CreateMask = "\ue91a";
-    public const string CropToSelection = "\ue91b";
-    public const string Crop = "\ue91c";
-    public const string Database = "\ue91d";
-    public const string Deselect = "\ue91e";
-    public const string Droplet = "\ue91f";
-    public const string DuplicateFile = "\ue920";
-    public const string Duplicate = "\ue921";
-    public const string Edit = "\ue922";
-    public const string Exit = "\ue923";
-    public const string EyeOff = "\ue924";
-    public const string Eye = "\ue925";
-    public const string RotateFileMinus90 = "\ue926";
-    public const string FilePlus = "\ue927";
-    public const string FileText = "\ue928";
-    public const string File = "\ue929";
-    public const string RotateFile90 = "\ue92a";
-    public const string RotateFile180 = "\ue92b";
-
-    public const string FolderPlus = "\ue92c";
-    public const string Folder = "\ue92d";
-    public const string Globe = "\ue92e";
-    public const string Grid = "\uE92F";
-    public const string GridLines = "\uE941";
-    public const string Home = "\ue930";
-    public const string RotateImageMinus90 = "\ue931";
-    public const string Image = "\ue932";
-    public const string RotateImage90 = "\ue933";
-    public const string RotateImage180 = "\ue934";
-    public const string Info = "\ue935";
-    public const string Intersect = "\ue936";
-    public const string Invert = "\ue937";
-    public const string Lasso = "\ue938";
-    public const string Layers = "\ue939";
-    public const string Line = "\ue93a";
-    public const string Lock = "\uE93B";
-    public const string LogOut = "\uE93C";
-    public const string MagicWand = "\uE93D";
-    public const string Minimize = "\uE93E";
-    public const string Merge = "\uE93F";
-    public const string MousePointer = "\uE940";
-    public const string MoveView = "\uE942";
-    public const string NewMask = "\uE943";
-    public const string Paste = "\uE944";
-    public const string VectorPen = "\uE945";
-    public const string Picker = "\uE946";
-    public const string PlusSquare = "\uE947";
-    public const string RectangleSelection = "\uE948";
-    public const string Redo = "\uE949";
-    public const string ReferenceLayer = "\uE94A";
-    public const string Resize = "\uE94B";
-    public const string RotateView = "\uE94C";
-    public const string Eraser = "\uE94D";
-    public const string Save = "\uE94E";
-    public const string Scissors = "\uE94F";
-    public const string SelectAll = "\uE950";
-    public const string Settings = "\uE951";
-    public const string Sliders = "\uE952";
-    public const string Square = "\uE953";
-    public const string Subtract = "\uE954";
-    public const string Sun = "\uE955";
-    public const string Swap = "\uE956";
-    public const string Tool = "\uE957";
-    public const string Trash = "\uE958";
-    public const string Undo = "\uE959";
-    public const string Unlock = "\uE95A";
-    public const string XFlip = "\uE95E";
-    public const string XSelectedFlip = "\uE95F";
-    public const string XSymmetry = "\uE95D";
-    public const string YFlip = "\uE95B";
-    public const string YSelectedFlip = "\uE95C";
-    public const string YSymmetry = "\uE960";
-    public const string ZoomIn = "\uE961";
-    public const string ZoomOut = "\uE962";
-    public const string PasteReferenceLayer = "\ue977";
-    public const string PasteAsNewLayer = "\ue978";
-    public const string CopyAdd = "\ue921";
-    public const string Message = "\uE96F";
-    public const string Download = "\uE969";
-    public const string YouTube = "\uE975";
-    public const string Write = "\uE968";
-    public const string Misc = "\uE970";
-    public const string Play = "\uE981";
-    public const string Pause = "\uE980";
-    public const string Swatches = "\uE982";
-    public const string Timeline = "\uE983";
-    public const string Dot = "\uE963";
-    public const string Nodes = "\uE984";
-    public const string Onion = "\uE965";
-        
-    public const string Reset = "\uE98A";
-    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 Pen = "\uE971";
-    public const string LowResCircle = "\uE986";
-    public const string Snapping = "\ue987";
-    public const string LowResSquare = "\uE988";
-    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 const string Cone = "\uE99c";
-    public const string Camera = "\uE99d";
-    public const string ChartSpline = "\ue99e";
-}

+ 2 - 0
src/PixiEditor.UI.Common/Fonts/defs.svg

@@ -146,8 +146,10 @@
 <glyph unicode="&#xe978;" glyph-name="trash" data-tags="uniE958" d="M85 704q0 18 12 30 13 13 31 13h768q18 0 30-12 13-13 13-31t-12-30q-13-13-31-13h-768q-18 0-30 12-13 13-13 31zM427 832q-9 0-17-3t-14-9-9-14-3-17v-85q0-18-12-30-13-13-31-13-17 0-29 12-13 13-13 31v85q0 26 9 49 10 24 28 42t41 27q24 10 50 10h170q26 0 49-9 24-10 42-28t27-41q10-24 10-50v-85q0-18-12-30-13-13-30-13-18 0-30 12-13 13-13 31v85q0 9-3 17t-9 14-14 9-17 3h-170zM213 747q18 0 30-12 13-13 13-31v-597q0-9 3-17t9-14 14-9 17-3h426q9 0 17 3t14 9 9 14 3 17v597q0 18 12 30 13 13 31 13 17 0 29-12 13-13 13-31v-597q0-26-9-49-10-24-28-42t-41-27q-24-10-50-10h-426q-26 0-49 9-24 10-42 28t-27 41q-10 24-10 50v597q0 18 12 30 13 13 30 13v0zM427 533q17 0 29-12 13-13 13-30v-256q0-18-12-30-13-13-30-13-18 0-30 12-13 13-13 31v256q0 17 12 29 13 13 31 13v0zM597 533q18 0 30-12 13-13 13-30v-256q0-18-12-30-13-13-31-13-17 0-29 12-13 13-13 31v256q0 17 12 29 13 13 30 13z" />
 <glyph unicode="&#xe979;" glyph-name="undo" data-tags="uniE959" d="M128 704q18 0 30-12 13-13 13-31v-160l57 51q58 53 131 81 74 28 153 28v0q42 0 84-8 41-8 79-24 39-16 74-39 35-24 65-54t53-65 39-73q16-39 24-80 9-41 9-83 0-18-12-30-13-13-31-13t-30 12q-13 13-13 31 0 68-26 130-26 63-74 111t-110 74q-63 26-131 26-63 0-122-22-59-23-105-65v0 0l-46-41h145q18 0 30-12 13-13 13-31 0-17-12-29-13-13-31-13h-256q-18 0-30 12-13 13-13 30v256q0 18 12 30 13 13 31 13v0z" />
 <glyph unicode="&#xe97a;" glyph-name="unlock" data-tags="uniE95A" d="M213 448q-17 0-29-12-13-13-13-31v-298q0-18 12-30 13-13 30-13h598q17 0 29 12 13 13 13 31v298q0 18-12 30-13 13-30 13h-598zM85 405q0 53 37 90 38 38 91 38h598q53 0 90-37 38-38 38-91v-298q0-53-37-90-38-38-91-38h-598q-53 0-90 37-38 38-38 91v298zM512 832q-36 0-68-10-33-11-57-30-23-18-34-40-12-22-12-43v-176q0-17-12-29-13-13-30-13-18 0-30 12-13 13-13 30v176q0 44 21 83t58 68q36 28 82 42 46 15 95 15t95-14q46-15 82-43-36 28 0 0 37-29 0 0 37-29 44-52 8-24-8-40t-40-6q-24 11-48 30-25 20-1 1t1-1q-24 19-56 29-33 11-69 11v0z" />
+<glyph unicode="&#xe97b;" glyph-name="user" data-tags="user" d="M810.667 106.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667c0 117.035-96.299 213.333-213.333 213.333s-213.333-96.299-213.333-213.333c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667c0 163.84 134.827 298.667 298.667 298.667s298.667-134.827 298.667-298.667zM512 746.667c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333c-117.76 0-213.333 95.573-213.333 213.333s95.573 213.333 213.333 213.333zM512 661.333c-70.656 0-128-57.344-128-128s57.344-128 128-128c70.656 0 128 57.344 128 128s-57.344 128-128 128zM512 917.333c259.029 0 469.333-210.304 469.333-469.333s-210.304-469.333-469.333-469.333c-259.029 0-469.333 210.304-469.333 469.333s210.304 469.333 469.333 469.333zM512 832c-211.925 0-384-172.075-384-384s172.075-384 384-384c211.925 0 384 172.075 384 384s-172.075 384-384 384z" />
 <glyph unicode="&#xe97c;" glyph-name="mirror-vertical" data-tags="uniE95C" d="M259 848q5 12 16 19 11 8 24 8h426q13 0 24-7 11-8 16-20t3-24q-3-13-12-22l-214-214q-12-12-30-12t-30 12l-214 214q-9 9-11 21-3 13 2 25v0zM402 789l110-110 110 110h-220zM43 448q0 18 12 30 13 13 30 13h86q17 0 29-12 13-13 13-31t-12-30q-13-13-30-13h-86q-17 0-29 12-13 13-13 31zM299 448q0 18 12 30 13 13 30 13h86q17 0 29-12 13-13 13-31t-12-30q-13-13-30-13h-86q-17 0-29 12-13 13-13 31v0zM555 448q0 18 12 30 13 13 30 13h86q17 0 29-12 13-13 13-31t-12-30q-13-13-30-13h-86q-17 0-29 12-13 13-13 31zM811 448q0 18 12 30 13 13 30 13h86q17 0 29-12 13-13 13-31t-12-30q-13-13-30-13h-86q-17 0-29 12-13 13-13 31zM268 94l214 214q12 12 30 12t30-12l214-214q9-9 11-21 3-13-2-25t-16-19q-11-8-24-8h-426q-13 0-24 7-11 8-16 20t-3 24q3 13 12 22v0zM402 107h220l-110 110z" />
 <glyph unicode="&#xe97d;" glyph-name="y-symmetry" data-tags="uniE95D" d="M43 448q0 18 12 30 13 13 30 13h854q17 0 29-12 13-13 13-31t-12-30q-13-13-30-13h-854q-17 0-29 12-13 13-13 31v0zM512 875q-53 0-90-37-38-38-38-91t37-90q38-38 91-38t90 37q38 38 38 91t-37 90q-38 38-91 38zM299 747q0 44 16 83 17 39 46 67 29 29 68 46t83 17 83-17 68-45q29-29 46-68 16-39 16-83 0-45-16-83-17-39-46-68t-68-46-83-17-83 17-68 46-46 68q-16 38-16 83v0zM347 285q3 4 6 7l8 8 8 7q8 8 8 7 13 11 30 10 18-2 30-16 11-13 9-30-2-18-15-30-3-2-5-4t-5-4l-4-4q-2-3-4-5-11-14-28-16-18-2-32 10-14 11-15 28-2 18 9 32v0zM453 315q-2 18 9 31 12 14 29 16 11 1 21 1t21-1q17-2 28-15 12-14 10-32t-15-29q-14-11-31-9h-26q-17-2-30 9-14 11-16 29v0zM346 209q18-2 29-16t10-31q-1-6-1-12 0-7 1-13 1-18-10-31-11-14-29-16-17-2-31 9-14 12-15 29-1 11-1 21 0 11 1 21 1 18 15 29t31 10v0zM587 308q12 14 29 15 18 2 31-9l8-6 15-15 7-8q11-14 10-31-2-18-16-29-14-12-31-10-18 2-29 16l-4 4q-2 3-4 5-3 2-5 4t-5 4q-13 12-15 29-2 18 9 31v0zM353 74q14 11 31 10 18-2 29-16l4-4q2-3 4-5 3-2 5-4 2-3 5-5 13-11 15-28 2-18-9-32-12-13-29-15-18-2-31 9-4 4-8 7-4 4-8 7-4 4-7 8-4 4-7 8-11 14-10 31 2 18 16 29v0zM678 209q17 1 31-10t15-29q1-10 1-20 0-11-1-22-1-17-15-28-14-12-31-10-18 2-29 15-11 14-10 32 1 6 1 12 0 7-1 13-1 17 10 31t29 16v0zM453-16q2 17 15 28 14 12 31 10 7-1 13-1t13 1q17 2 30-9 14-12 16-29 2-18-9-31-12-14-29-16-11-1-21-1t-21 1q-17 2-28 15-12 14-10 32v0zM587-10q-11 14-9 31 2 18 15 29 3 2 5 4 2 3 5 5l4 4q2 3 4 5 11 14 28 15 18 2 32-9t15-28q2-18-9-32l-6-8-8-8-8-6-8-8q-13-11-30-9-18 2-30 15v0z" />
+<glyph unicode="&#xe97e;" glyph-name="check-tick" data-tags="check-tick" d="M823.168 734.165c16.64 16.64 43.691 16.64 60.331 0s16.64-43.691 0-60.331l-469.333-469.333c-16.64-16.683-43.691-16.683-60.331 0l-213.333 213.333c-16.64 16.64-16.64 43.691 0 60.331s43.691 16.64 60.331 0l183.168-183.168 439.168 439.168z" />
 <glyph unicode="&#xe97f;" glyph-name="mirror-horizontal" data-tags="uniE95F" d="M512 917q18 0 30-12 13-13 13-30v-86q0-17-12-29-13-13-31-13t-30 12q-13 13-13 30v86q0 17 12 29 13 13 31 13zM112 701q12 5 24 3 13-3 22-12l214-214q12-12 12-30t-12-30l-214-214q-9-9-21-11-13-3-25 2t-19 16q-8 11-8 24v426q0 13 7 24 8 11 20 16v0zM912 701q12-5 19-16 8-11 8-24v-426q0-13-7-24-8-11-20-16t-24-3q-13 3-22 12l-214 214q-12 12-12 30t12 30l214 214q9 9 21 11 13 3 25-2v0zM171 558v-220l110 110zM743 448l110-110v220zM512 661q18 0 30-12 13-13 13-30v-86q0-17-12-29-13-13-31-13t-30 12q-13 13-13 30v86q0 17 12 29 13 13 31 13zM512 405q18 0 30-12 13-13 13-30v-86q0-17-12-29-13-13-31-13t-30 12q-13 13-13 30v86q0 17 12 29 13 13 31 13zM512 149q18 0 30-12 13-13 13-30v-86q0-17-12-29-13-13-31-13t-30 12q-13 13-13 30v86q0 17 12 29 13 13 31 13z" />
 <glyph unicode="&#xe980;" glyph-name="x-symmetry" data-tags="uniE960" d="M512-21q-18 0-30 12-13 13-13 30v854q0 17 12 29 13 13 31 13t30-12q13-13 13-30v-854q0-17-12-29-13-13-31-13zM85 448q0-53 37-90 38-38 91-38t90 37q38 38 38 91t-37 90q-38 38-91 38t-90-37q-38-38-38-91v0zM213 235q-44 0-83 16-39 17-67 46-29 29-46 68t-17 83 17 83 45 68q29 29 68 46 39 16 83 16 45 0 83-16 39-17 68-46t46-68 17-83-17-83-46-68-68-46q-38-16-83-16zM675 283q-4 3-7 6l-8 8-7 8q-8 8-7 8-11 13-10 30 2 18 16 30 13 11 30 9 18-2 30-15 2-3 4-5t4-5l4-4q3-2 5-4 14-11 16-28 2-18-10-32-11-14-28-15-18-2-32 9v0zM645 389q-18-2-31 9-14 12-16 29-1 11-1 21t1 21q2 17 15 28 14 12 32 10t29-15q11-14 9-31v-26q2-17-9-30-11-14-29-16v0zM751 282q2 18 16 29t31 10q6-1 12-1 7 0 13 1 18 1 31-10 14-11 16-29 2-17-9-31-12-14-29-15-11-1-21-1-11 0-21 1-18 1-29 15t-10 31v0zM652 523q-14 12-15 29-2 18 9 31l6 8 15 15 8 7q14 11 31 10 18-2 29-16 12-14 10-31-2-18-16-29l-4-4q-3-2-5-4-2-3-4-5t-4-5q-12-13-29-15-18-2-31 9v0zM886 289q-11 14-10 31 2 18 16 29l4 4q3 2 5 4 2 3 4 5 3 2 5 5 11 13 28 15 18 2 32-9 13-12 15-29 2-18-9-31-4-4-7-8-4-4-7-8-4-4-8-7-4-4-8-7-14-11-31-10-18 2-29 16v0zM751 614q-1 17 10 31t29 15q10 1 20 1 11 0 22-1 17-1 28-15 12-14 10-31-2-18-15-29-14-11-32-10-6 1-12 1-7 0-13-1-17-1-31 10t-16 29v0zM976 389q-17 2-28 15-12 14-10 31 1 7 1 13t-1 13q-2 17 9 30 12 14 29 16 18 2 31-9 14-12 16-29 1-11 1-21t-1-21q-2-17-15-28-14-12-32-10v0zM970 523q-14-11-31-9-18 2-29 15-2 3-4 5-3 2-5 5l-4 4q-3 2-5 4-14 11-15 28-2 18 9 32t28 15q18 2 32-9l8-6 8-8 6-8 8-8q11-13 9-30-2-18-15-30v0z" />
 <glyph unicode="&#xe981;" glyph-name="zoom-in" data-tags="uniE961" d="M469 789q-62 0-116-23-54-24-94-64-41-41-65-95-23-54-23-116t23-117q24-54 64-94 41-41 95-65 54-23 116-23t117 23q54 24 94 64 41 41 65 95 23 55 23 117t-23 116q-24 54-64 94-41 41-95 65-55 23-117 23zM85 491q0 79 31 149 30 70 82 122t122 82q70 31 149 31 80 0 150-31 70-30 122-82t82-122 30-149q0-80-30-150t-82-122-122-82-150-30q-79 0-149 30t-122 82-82 122q-31 70-31 150v0zM680 280q13 12 30 12 18 0 31-12l185-186q13-12 13-30t-13-30q-12-13-30-13t-30 13l-186 185q-12 13-12 30 0 18 12 31v0zM469 661q18 0 30-12 13-13 13-30v-256q0-18-12-30-13-13-31-13-17 0-29 12-13 13-13 31v256q0 17 12 29 13 13 30 13v0zM299 491q0 17 12 29 13 13 30 13h256q18 0 30-12 13-13 13-30 0-18-12-30-13-13-31-13h-256q-17 0-29 12-13 13-13 31v0z" />

+ 19 - 0
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -102,6 +102,11 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             shapeData = AddText(text);
             name = TextToolViewModel.NewLayerKey;
         }
+        else if (element is SvgPolyline or SvgPolygon)
+        {
+            shapeData = AddPoly(element);
+            name = VectorPathToolViewModel.NewLayerKey;
+        }
 
         name = element.Id.Unit?.Value ?? name;
 
@@ -376,6 +381,20 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         };
     }
 
+    private PathVectorData AddPoly(SvgElement element)
+    {
+        if (element is SvgPolyline polyline)
+        {
+            return new PathVectorData(VectorPath.FromPoints(polyline.GetPoints(), false));
+        }
+        if (element is SvgPolygon polygon)
+        {
+            return new PathVectorData(VectorPath.FromPoints(polygon.GetPoints(), true));
+        }
+
+        return null;
+    }
+
     private void AddCommonShapeData(ShapeVectorData? shapeData, StyleContext styleContext)
     {
         if (shapeData == null)

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -120,7 +120,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     }
 
     public string ZipExtension => IOperatingSystem.Current.IsLinux ? "tar.gz" : "zip";
-    public string ZipContentType => IOperatingSystem.Current.IsLinux ? "x-gzip" : "zip";
+    public string ZipContentType => IOperatingSystem.Current.IsLinux ? "octet-stream" : "zip";
     public string InstallerExtension => IOperatingSystem.Current.IsWindows ? "exe" : "dmg";
 
     public string BinaryExtension => IOperatingSystem.Current.IsWindows ? ".exe" : string.Empty;

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

@@ -113,13 +113,13 @@
                             <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="FLIP_VIEWPORT_HORIZONTALLY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                          Content="{DynamicResource icon-y-flip}"
+                                          Content="{DynamicResource icon-x-flip}"
                                           Cursor="Hand" />
                             <ToggleButton Width="32" Height="32"
                                           ui:Translator.TooltipKey="FLIP_VIEWPORT_VERTICALLY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                          Content="{DynamicResource icon-x-flip}"
+                                          Content="{DynamicResource icon-image180}"
                                           Cursor="Hand" />
                         </StackPanel>
                         <Separator />

+ 46 - 6
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -62,6 +62,9 @@ public class VectorPathOverlay : Overlay
     private bool canInsert = false;
 
     private bool isDragging = false;
+    private bool pointerPressed = false;
+    private bool convertSelectedOnDrag = false;
+    private bool converted = false;
 
     private List<int> lastSelectedIndices = new();
 
@@ -515,6 +518,8 @@ public class VectorPathOverlay : Overlay
             return;
         }
 
+        pointerPressed = true;
+
         if (IsOverPath(args.Point, out VecD closestPoint))
         {
             AddPointAt(closestPoint);
@@ -524,12 +529,43 @@ public class VectorPathOverlay : Overlay
         else if (args.Modifiers == KeyModifiers.None)
         {
             args.Handled = AddNewPointFromClick(SnappingController.GetSnapPoint(args.Point, out _, out _));
+            if (args.Handled)
+            {
+                convertSelectedOnDrag = true;
+                converted = false;
+            }
+
             AddToUndoCommand.Execute(Path);
         }
     }
 
     protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
     {
+        if (pointerPressed && convertSelectedOnDrag)
+        {
+            var anchor = anchorHandles.FirstOrDefault(h => h.IsSelected);
+            if (anchor == null)
+            {
+                return;
+            }
+
+            int index = anchorHandles.IndexOf(anchor);
+            var path = editableVectorPath;
+            if (!converted)
+            {
+                path = ConvertTouchingVerbsToCubic(anchor);
+                Path = path.ToVectorPath();
+                AdjustHandles(path);
+                converted = true;
+            }
+
+            SubShape subShapeContainingIndex = path.GetSubShapeContainingIndex(index);
+            int localIndex = path.GetSubShapePointIndex(index, subShapeContainingIndex);
+
+            HandleContinousCubicDrag(args.Point, subShapeContainingIndex, localIndex, true);
+            Path = editableVectorPath.ToVectorPath();
+        }
+
         if (IsOverPath(args.Point, out VecD closestPoint))
         {
             insertPreviewHandle.Position = closestPoint;
@@ -541,10 +577,12 @@ public class VectorPathOverlay : Overlay
         }
     }
 
-    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    protected override void OnOverlayPointerReleased(OverlayPointerArgs args)
     {
-        base.OnPointerReleased(e);
         isDragging = false;
+        pointerPressed = false;
+        convertSelectedOnDrag = false;
+        converted = false;
     }
 
     private bool AddNewPointFromClick(VecD point)
@@ -586,7 +624,9 @@ public class VectorPathOverlay : Overlay
     {
         int? insertedAt = editableVectorPath.AddPointAt(point);
         Path = editableVectorPath.ToVectorPath();
-        SelectAnchor(insertedAt is > 0 && insertedAt.Value < anchorHandles.Count ? anchorHandles[insertedAt.Value] : anchorHandles.Last());
+        SelectAnchor(insertedAt is > 0 && insertedAt.Value < anchorHandles.Count
+            ? anchorHandles[insertedAt.Value]
+            : anchorHandles.Last());
     }
 
     private bool IsOverPath(VecD point, out VecD closestPoint)
@@ -669,7 +709,7 @@ public class VectorPathOverlay : Overlay
                 subShapeContainingIndex = newPath.GetSubShapeContainingIndex(index);
                 localIndex = newPath.GetSubShapePointIndex(index, subShapeContainingIndex);
 
-                HandleContinousCubicDrag(targetPos, anchor, subShapeContainingIndex, localIndex, true);
+                HandleContinousCubicDrag(targetPos, subShapeContainingIndex, localIndex, true);
             }
             else
             {
@@ -687,7 +727,7 @@ public class VectorPathOverlay : Overlay
         Path = editableVectorPath.ToVectorPath();
     }
 
-    private void HandleContinousCubicDrag(VecD targetPos, AnchorHandle handle, SubShape subShapeContainingIndex,
+    private void HandleContinousCubicDrag(VecD targetPos, SubShape subShapeContainingIndex,
         int localIndex, bool constrainRatio, bool swapOrder = false)
     {
         var previousPoint = subShapeContainingIndex.GetPreviousPoint(localIndex);
@@ -756,7 +796,7 @@ public class VectorPathOverlay : Overlay
         {
             bool isDraggingFirst = controlPointHandles.IndexOf(controlPointHandle) % 2 == 0;
             bool constrainRatio = args.Modifiers.HasFlag(KeyModifiers.Control);
-            HandleContinousCubicDrag(targetPos, to, subShapeContainingIndex, localIndex, constrainRatio,
+            HandleContinousCubicDrag(targetPos, subShapeContainingIndex, localIndex, constrainRatio,
                 !isDraggingFirst);
         }
         else