Kaynağa Gözat

Merge remote-tracking branch 'origin/master' into text-nodes

# Conflicts:
#	src/PixiEditor/Data/Localization/Languages/en.json
CPKreuz 1 gün önce
ebeveyn
işleme
0f0b25e928
87 değiştirilmiş dosya ile 1667 ekleme ve 635 silme
  1. 1 2
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  2. 22 24
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  3. 1 1
      src/ColorPicker
  4. 0 81
      src/Custom.ruleset
  5. 3 9
      src/Directory.Build.props
  6. 1 1
      src/Drawie
  7. 1 1
      src/PixiDocks
  8. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs
  9. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  10. 166 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  11. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  14. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  15. 77 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromFolder_Change.cs
  16. 4 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromLayer_Change.cs
  17. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  18. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  19. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs
  20. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  21. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  22. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  23. 62 14
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  24. 2 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs
  25. 0 1
      src/PixiEditor.sln
  26. 124 124
      src/PixiEditor/Data/Localization/Languages/ar.json
  27. 16 1
      src/PixiEditor/Data/Localization/Languages/en.json
  28. 106 100
      src/PixiEditor/Data/Localization/Languages/es.json
  29. 131 1
      src/PixiEditor/Data/Localization/Languages/ru.json
  30. 2 5
      src/PixiEditor/Data/Localization/Languages/tr.json
  31. 5 5
      src/PixiEditor/Data/Localization/LocalizationData.json
  32. 1 0
      src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs
  33. 3 1
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  34. 2 0
      src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs
  35. 14 3
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  36. 2 1
      src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs
  37. 5 3
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  38. 16 15
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  39. 7 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  40. 12 5
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  41. 9 0
      src/PixiEditor/Models/EnumTranslations.cs
  42. 11 7
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  43. 1 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  44. 4 3
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  45. 4 4
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  46. 144 0
      src/PixiEditor/Models/Structures/ObservableHashSet.cs
  47. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  48. 4 7
      src/PixiEditor/Styles/Templates/NodeFrameView.axaml
  49. 4 10
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  50. 9 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  51. 3 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  52. 83 0
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  53. 11 0
      src/PixiEditor/ViewModels/Document/Nodes/Effects/PosterizationNodeViewModel.cs
  54. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs
  55. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ModifyImageRightNodeViewModel.cs
  56. 2 2
      src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs
  57. 2 1
      src/PixiEditor/ViewModels/Document/StructureTree.cs
  58. 6 0
      src/PixiEditor/ViewModels/Nodes/IPairNodeEndViewModel.cs
  59. 6 0
      src/PixiEditor/ViewModels/Nodes/IPairNodeStartViewModel.cs
  60. 2 28
      src/PixiEditor/ViewModels/Nodes/NodeFrameViewModel.cs
  61. 23 21
      src/PixiEditor/ViewModels/Nodes/NodeFrameViewModelBase.cs
  62. 99 24
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  63. 222 35
      src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs
  64. 19 0
      src/PixiEditor/ViewModels/Nodes/Traverse.cs
  65. 4 0
      src/PixiEditor/ViewModels/PixiObservableObject.cs
  66. 22 2
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  67. 3 1
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  68. 53 1
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  69. 19 2
      src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs
  70. 28 0
      src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs
  71. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  72. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  73. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  74. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  75. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  76. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  77. 9 0
      src/PixiEditor/ViewModels/UserPreferences/Settings/SceneSettings.cs
  78. 16 1
      src/PixiEditor/ViewModels/UserPreferences/SettingsGroup.cs
  79. 1 1
      src/PixiEditor/Views/Dock/DocumentTemplate.axaml
  80. 1 1
      src/PixiEditor/Views/Main/MainTitleBar.axaml
  81. 1 1
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  82. 9 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  83. 6 21
      src/PixiEditor/Views/Nodes/NodeFrameView.cs
  84. 0 4
      src/PixiEditor/Views/Nodes/NodeGraphView.cs
  85. 7 0
      src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml
  86. 0 20
      src/stylecop.json
  87. 1 1
      tests/Directory.Build.props

+ 1 - 2
src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs

@@ -70,9 +70,8 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
             newTo = (VecI)newTo.ReflectY((double)horAxisY).Round();
         }
 
-        Color color = paint.Paintable is ColorPaintable colorPaintable ? colorPaintable.Color : paint.Color;
 
-        return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, color, paint.BlendMode);
+        return new DrawingSurfaceLineOperation(newFrom, newTo, paint);
     }
 
     public void Dispose()

+ 22 - 24
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -40,11 +40,11 @@ internal class RectangleOperation : IMirroredDrawOperation
         surf.Canvas.RotateRadians((float)Data.Angle, (float)rect.Center.X, (float)rect.Center.Y);
 
         double maxRadiusInPx = Math.Min(Data.Size.X, Data.Size.Y) / 2;
-        double radiusInPx = Data.CornerRadius * maxRadiusInPx;
+        double radiusInPx = Data.CornerRadius * Math.Abs(maxRadiusInPx);
 
         if (Data.AntiAliasing)
         {
-            DrawAntiAliased(surf, rect, radiusInPx);
+            DrawAntiAliased(surf, rect, innerRect, radiusInPx);
         }
         else
         {
@@ -90,8 +90,24 @@ internal class RectangleOperation : IMirroredDrawOperation
         surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
     }
 
-    private void DrawAntiAliased(DrawingSurface surf, RectD rect, double radius)
+    private void DrawAntiAliased(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
     {
+        surf.Canvas.Save();
+        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
+        paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
+        paint.Style = PaintStyle.Fill;
+
+        if (radius == 0)
+        {
+            surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
+                (float)rect.Height, paint);
+        }
+        else
+        {
+            surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
+                (float)rect.Height, (float)radius, (float)radius, paint);
+        }
+
         // draw fill
         if (Data.FillPaintable.AnythingVisible)
         {
@@ -102,34 +118,16 @@ internal class RectangleOperation : IMirroredDrawOperation
             paint.Style = PaintStyle.Fill;
             if (radius == 0)
             {
-                surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height, paint);
+                surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);
             }
             else
             {
-                surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
-                    (float)rect.Height, (float)radius, (float)radius, paint);
+                surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
+                    (float)innerRect.Height, (float)radius, (float)radius, paint);
             }
 
             surf.Canvas.RestoreToCount(saved);
         }
-
-        // draw stroke
-        surf.Canvas.Save();
-        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
-        paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
-        paint.Style = PaintStyle.Stroke;
-        RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
-
-        if (radius == 0)
-        {
-            surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
-                (float)innerRect.Height, paint);
-        }
-        else
-        {
-            surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
-                (float)innerRect.Height, (float)radius, (float)radius, paint);
-        }
     }
 
     public AffectedArea FindAffectedArea(VecI imageSize)

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 943e9abbb60b73c4965b947e987dc2696e0b08f8
+Subproject commit f39e02c617d9a4a57b1a911fc4f03bbf7a7fd987

+ 0 - 81
src/Custom.ruleset

@@ -13,85 +13,4 @@
     <Rule Id="CA1303" Action="None" />
     <Rule Id="CA1416" Action="None" />
   </Rules>
-  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
-    <Rule Id="SA0001" Action="None" />
-    <Rule Id="SA1000" Action="None" />
-    <Rule Id="SA1005" Action="None" />
-    <Rule Id="SA1008" Action="None" />
-    <Rule Id="SA1009" Action="None" />
-    <Rule Id="SA1011" Action="None" />
-    <Rule Id="SA1023" Action="None" />
-    <Rule Id="SA1028" Action="None" />
-    <Rule Id="SA1101" Action="None" />
-    <Rule Id="SA1110" Action="None" />
-    <Rule Id="SA1111" Action="None" />
-    <Rule Id="SA1112" Action="None" />
-    <Rule Id="SA1117" Action="None" />
-    <Rule Id="SA1119" Action="None" />
-    <Rule Id="SA1121" Action="None" />
-    <Rule Id="SA1122" Action="None" />
-    <Rule Id="SA1124" Action="None" />
-    <Rule Id="SA1127" Action="None" />
-    <Rule Id="SA1128" Action="None" />
-    <Rule Id="SA1129" Action="None" />
-    <Rule Id="SA1130" Action="None" />
-    <Rule Id="SA1132" Action="None" />
-    <Rule Id="SA1135" Action="None" />
-    <Rule Id="SA1136" Action="None" />
-    <Rule Id="SA1139" Action="None" />
-    <Rule Id="SA1200" Action="None" />
-    <Rule Id="SA1201" Action="None" />
-    <Rule Id="SA1202" Action="None" />
-    <Rule Id="SA1204" Action="None" />
-    <Rule Id="SA1207" Action="None" />
-    <Rule Id="SA1208" Action="None" />
-    <Rule Id="SA1209" Action="None" />
-    <Rule Id="SA1210" Action="None" />
-    <Rule Id="SA1211" Action="None" />
-    <Rule Id="SA1214" Action="None" />
-    <Rule Id="SA1216" Action="None" />
-    <Rule Id="SA1217" Action="None" />
-    <Rule Id="SA1303" Action="None" />
-    <Rule Id="SA1304" Action="None" />
-    <Rule Id="SA1307" Action="None" />
-    <Rule Id="SA1309" Action="None" />
-    <Rule Id="SA1310" Action="None" />
-    <Rule Id="SA1311" Action="None" />
-    <Rule Id="SA1313" Action="None" />
-    <Rule Id="SA1316" Action="None" />
-    <Rule Id="SA1400" Action="None" />
-    <Rule Id="SA1401" Action="None" />
-    <Rule Id="SA1402" Action="None" />
-    <Rule Id="SA1405" Action="None" />
-    <Rule Id="SA1406" Action="None" />
-    <Rule Id="SA1407" Action="None" />
-    <Rule Id="SA1408" Action="None" />
-    <Rule Id="SA1410" Action="None" />
-    <Rule Id="SA1411" Action="None" />
-    <Rule Id="SA1413" Action="None" />
-    <Rule Id="SA1501" Action="None" />
-    <Rule Id="SA1502" Action="None" />
-    <Rule Id="SA1503" Action="None" />
-    <Rule Id="SA1505" Action="None" />
-    <Rule Id="SA1507" Action="None" />
-    <Rule Id="SA1508" Action="None" />
-    <Rule Id="SA1512" Action="None" />
-    <Rule Id="SA1513" Action="None" />
-    <Rule Id="SA1515" Action="None" />
-    <Rule Id="SA1516" Action="None" />
-    <Rule Id="SA1518" Action="None" />
-    <Rule Id="SA1600" Action="None" />
-    <Rule Id="SA1601" Action="None" />
-    <Rule Id="SA1602" Action="None" />
-    <Rule Id="SA1604" Action="None" />
-    <Rule Id="SA1605" Action="None" />
-    <Rule Id="SA1606" Action="None" />
-    <Rule Id="SA1607" Action="None" />
-    <Rule Id="SA1623" Action="None" />
-    <Rule Id="SA1629" Action="None" />
-    <Rule Id="SA1633" Action="None" />
-    <Rule Id="SA1642" Action="None" />
-    <Rule Id="SA1643" Action="None" />
-    <Rule Id="SA1648" Action="None" />
-  </Rules>
 </RuleSet>

+ 3 - 9
src/Directory.Build.props

@@ -1,15 +1,9 @@
 <Project>
     <PropertyGroup>
-        <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
+        <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
+		    <AvaloniaVersion>11.3.5</AvaloniaVersion>
     </PropertyGroup>
-    <ItemGroup>
-        <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
-    </ItemGroup>
-    <ItemGroup>
-        <AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
-    </ItemGroup>
-
+  
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>
   </PropertyGroup>

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit b6c34c96ac5b01abad69604465445270270270d2
+Subproject commit adfaa90105229e3183e3049276af982f3e5b1b5d

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 6e745d0309ad7a00a53f62f2aa362be77903a5fd
+Subproject commit af479aac479fd1a0494cb61fa49a03a3713c96cf

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs

@@ -8,7 +8,7 @@ public class KeyFrameData : IDisposable, IReadOnlyKeyFrameData
 {
     public int StartFrame { get; set; }
     public int Duration { get; set; }
-    public Guid KeyFrameGuid { get; }
+    public Guid KeyFrameGuid { get; internal set; }
     public string AffectedElement { get; set; }
     public object Data { get; set; }
     public bool IsVisible { get; set; } = true;

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -444,12 +444,12 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     private void ExtractLayers(FolderNode folder, List<Guid> list)
     {
-        List<Guid> result = new();
-        folder.TraverseBackwards(node =>
+        if(folder.Content.Connection == null) return;
+        folder.Content.Connection.Node.TraverseBackwards(node =>
         {
-            if (node is LayerNode layer && !result.Contains(layer.Id))
+            if (node is LayerNode layer && !list.Contains(layer.Id))
             {
-                result.Add(layer.Id);
+                list.Add(layer.Id);
             }
 
             return true;

+ 166 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs

@@ -0,0 +1,166 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.ColorSpaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
+
+[NodeInfo("Posterization")]
+public class PosterizationNode : RenderNode, IRenderInput
+{
+    public RenderInputProperty Background { get; }
+    public InputProperty<PosterizationMode> Mode { get; }
+    public InputProperty<int> Levels { get; }
+    public InputProperty<ColorSpaceType> PosterizationColorSpace { get; }
+    
+    private Paint paint;
+    private Shader shader;
+    
+    private Shader? lastImageShader;
+    private VecI lastDocumentSize;
+    
+    private string shaderCode = """
+                                 uniform shader iImage;
+                                 uniform int iMode;
+                                 uniform float iLevels;
+                                 
+                                 half posterize(half value, float levels) {
+                                     return clamp(floor(value * (levels - 1) + 0.5) / (levels - 1), 0.0, 1.0);
+                                 }
+                                 
+                                 half4 posterizeRgb(half4 color, float levels) {
+                                     return half4(
+                                         posterize(color.r, levels),
+                                         posterize(color.g, levels),
+                                         posterize(color.b, levels),
+                                         color.a
+                                     );
+                                 }
+                                 
+                                 half4 posterizeLuminance(half4 color, float levels) {
+                                     half lum = dot(color.rgb, half3(0.299, 0.587, 0.114));
+                                     half posterizedLum = posterize(lum, levels);
+                                     return half4(posterizedLum, posterizedLum, posterizedLum, color.a);
+                                 }
+                                 
+                                 half4 main(float2 uv)
+                                 {
+                                    half4 color = iImage.eval(uv);
+                                    half4 result;
+                                    
+                                    if(iMode == 0) {
+                                        result = posterizeRgb(color, iLevels);
+                                    } else if(iMode == 1) {
+                                        result = posterizeLuminance(color, iLevels);
+                                    } 
+                                    
+                                    return result;
+                                 }
+                                 """;
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
+    public PosterizationNode()
+    {
+        Background = CreateRenderInput("Background", "BACKGROUND");
+        Mode = CreateInput("Mode", "MODE", PosterizationMode.Rgb);
+        Levels = CreateInput("Levels", "LEVELS", 8)
+            .WithRules(v => v.Min(2).Max(256));
+        PosterizationColorSpace = CreateInput("PosterizationColorSpace", "COLOR_SPACE", ColorSpaceType.Srgb);
+        
+        paint = new Paint();
+        paint.BlendMode = BlendMode.Src;
+        Output.FirstInChain = null;
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+        lastDocumentSize = context.RenderOutputSize;
+        
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("iMode", new Uniform("iMode", (int)Mode.Value));
+        uniforms.Add("iLevels", new Uniform("iLevels", (float)Levels.Value));
+        shader?.Dispose();
+        shader = Shader.Create(shaderCode, uniforms, out _);
+    }
+    
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (Background.Value == null)
+        {
+            return;
+        }
+
+        ColorSpace colorSpace;
+        switch (PosterizationColorSpace.Value)
+        {
+            case ColorSpaceType.Srgb:
+                colorSpace = ColorSpace.CreateSrgb();
+                break;
+            case ColorSpaceType.LinearSrgb:
+                colorSpace = ColorSpace.CreateSrgbLinear();
+                break;
+            case ColorSpaceType.Inherit:
+                colorSpace = context.ProcessingColorSpace;
+                break;
+            default:
+                colorSpace = ColorSpace.CreateSrgb();
+                break;
+        }
+        
+        using Texture temp = Texture.ForProcessing(surface, colorSpace);
+        Background.Value.Paint(context, temp.DrawingSurface);
+        var snapshot = temp.DrawingSurface.Snapshot();
+        
+        lastImageShader?.Dispose();
+        lastImageShader = snapshot.ToShader();
+
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("iMode", new Uniform("iMode", (int)Mode.Value));
+        uniforms.Add("iLevels", new Uniform("iLevels", (float)Levels.Value));
+        shader = shader.WithUpdatedUniforms(uniforms);
+        paint.Shader = shader;
+        snapshot.Dispose();
+        
+        var savedTemp = temp.DrawingSurface.Canvas.Save();
+        temp.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        temp.DrawingSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
+        temp.DrawingSurface.Canvas.RestoreToCount(savedTemp);
+        
+        var saved = surface.Canvas.Save();
+        surface.Canvas.SetMatrix(Matrix3X3.Identity);
+        surface.Canvas.DrawSurface(temp.DrawingSurface, 0, 0);
+        surface.Canvas.RestoreToCount(saved);
+    }
+    
+    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+    }
+
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        OnPaint(context, renderOn);
+        return true;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new PosterizationNode();
+    }
+}
+
+public enum PosterizationMode
+{
+    Rgb = 0,
+    Luminance = 1,
+}

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

@@ -58,11 +58,11 @@ public class SampleImageNode : Node
 
         if (SampleMode.Value == ColorSampleMode.ColorManaged)
         {
-            color = Image.Value.GetSRGBPixel(pixelCoordinate);
+            color = Image.Value.GetSrgbPixel(pixelCoordinate);
         }
         else
         {
-            color = Image.Value.GetPixel(pixelCoordinate);
+            color = Image.Value.GetRawPixel(pixelCoordinate);
         }
 
         return new Half4("") { ConstantValue = color };

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

@@ -21,7 +21,7 @@ public class EllipseVectorData : ShapeVectorData, IReadOnlyEllipseData
         RectD.FromCenterAndSize(Center, Radius * 2).Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
 
     public EllipseVectorData(VecD center, VecD radius)

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

@@ -15,7 +15,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(Path.TightBounds).WithMatrix(TransformationMatrix);
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
 

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

@@ -27,7 +27,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
     }
 
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
 
     public RectangleVectorData(VecD center, VecD size)

+ 77 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromFolder_Change.cs

@@ -0,0 +1,77 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class CreateAnimationDataFromFolder_Change : Change
+{
+    private readonly Guid folderGuid;
+    private Guid[] layerGuids;
+
+    [GenerateMakeChangeAction]
+    public CreateAnimationDataFromFolder_Change(Guid folderGuid)
+    {
+        this.folderGuid = folderGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (!target.TryFindMember<FolderNode>(folderGuid, out FolderNode? layer))
+        {
+            return false;
+        }
+
+        var layers = target.ExtractLayers([layer.Id]);
+        if (layers.Count == 0) return false;
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        FolderNode folder = target.FindNode(folderGuid) as FolderNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+        layerGuids = target.ExtractLayers([folder.Id]).ToArray();
+
+        foreach (var layer in layerGuids)
+        {
+            var node = target.FindNode(layer);
+            if(node is not LayerNode) continue;
+            foreach (var frame in node.KeyFrames)
+            {
+                if(frame.StartFrame == 0 && frame.Duration == 0) continue;
+                Guid keyFrameId = frame.KeyFrameGuid;
+                target.AnimationData.AddKeyFrame(new RasterKeyFrame(keyFrameId, node.Id, frame.StartFrame, target)
+                {
+                    Duration = frame.Duration,
+                    IsVisible = frame.IsVisible,
+                });
+                infos.Add(new CreateRasterKeyFrame_ChangeInfo(node.Id, frame.StartFrame, keyFrameId, true));
+                infos.Add(new KeyFrameLength_ChangeInfo(keyFrameId, frame.StartFrame, frame.Duration));
+                infos.Add(new KeyFrameVisibility_ChangeInfo(keyFrameId, frame.IsVisible));
+            }
+        }
+
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var layer = target.FindNode(folderGuid) as FolderNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+
+        var keyFrame = target.AnimationData.KeyFrames;
+        var ids = keyFrame.Where(x => x.NodeId == layer.Id || layerGuids.Contains(x.NodeId)).Select(x => x.Id).ToList();
+
+        foreach (var id in ids)
+        {
+            target.AnimationData.RemoveKeyFrame(id);
+            infos.Add(new DeleteKeyFrame_ChangeInfo(id));
+        }
+
+        return infos;
+    }
+}

+ 4 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromLayer_Change.cs

@@ -30,9 +30,12 @@ internal class CreateAnimationDataFromLayer_Change : Change
             Guid keyFrameId = frame.KeyFrameGuid;
             target.AnimationData.AddKeyFrame(new RasterKeyFrame(keyFrameId, layer.Id, frame.StartFrame, target)
             {
-                Duration = frame.Duration
+                Duration = frame.Duration,
+                IsVisible = frame.IsVisible,
             });
             infos.Add(new CreateRasterKeyFrame_ChangeInfo(layer.Id, frame.StartFrame, keyFrameId, true));
+            infos.Add(new KeyFrameLength_ChangeInfo(keyFrameId, frame.StartFrame, frame.Duration));
+            infos.Add(new KeyFrameVisibility_ChangeInfo(keyFrameId, frame.IsVisible));
         }
 
         ignoreInUndo = false;

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -91,7 +91,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         {
-            tightBounds = vectorLayer.EmbeddedShapeData?.VisualAABB ?? default;
+            tightBounds = vectorLayer.EmbeddedShapeData?.GeometryAABB ?? default;
         }
 
         for (var i = 1; i < memberData.Count; i++)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -52,9 +52,9 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(img =>
+                layer.ForEveryFrame((img, id) =>
                 {
-                    Resize(img, layer.Id, size, -(VecI)newBounds.Pos, deletedChunks);
+                    Resize(img, id, size, -(VecI)newBounds.Pos, deletedChunks);
                 });
             }
             else if (member is ITransformableObject transformableObject)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs

@@ -37,9 +37,9 @@ internal class Crop_Change : ResizeBasedChangeBase
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(frame =>
+                layer.ForEveryFrame((frame, id) =>
                 {
-                    Resize(frame, layer.Id, rect.Size, rect.Pos * -1, deletedChunks);
+                    Resize(frame, id, rect.Size, rect.Pos * -1, deletedChunks);
                 });
             }
             if (member.EmbeddedMask is null)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -52,10 +52,10 @@ internal abstract class ResizeBasedChangeBase : Change
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(img =>
+                layer.ForEveryFrame((img, id) =>
                 {
                     img.EnqueueResize(_originalSize);
-                    foreach (var stored in deletedChunks[layer.Id])
+                    foreach (var stored in deletedChunks[id])
                         stored.ApplyChunksToImage(img);
                     img.CommitChanges();
                 });

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -49,9 +49,9 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(img =>
+                layer.ForEveryFrame((img, id) =>
                 {
-                    Resize(img, layer.Id, newSize, offset, deletedChunks);
+                    Resize(img, id, newSize, offset, deletedChunks);
                 });
             }
 

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -91,11 +91,11 @@ internal class ResizeImage_Change : Change
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(img =>
+                layer.ForEveryFrame((img, id) =>
                 {
                     ScaleChunkyImage(img);
                     var affected = img.FindAffectedArea();
-                    savedChunks[layer.Id] = new CommittedChunkStorage(img, affected.Chunks);
+                    savedChunks[id] = new CommittedChunkStorage(img, affected.Chunks);
                     img.CommitChanges();
                 });
             }
@@ -127,11 +127,11 @@ internal class ResizeImage_Change : Change
         {
             if (member is ImageLayerNode layer)
             {
-                layer.ForEveryFrame(layerImage =>
+                layer.ForEveryFrame((layerImage, id) =>
                 {
                     layerImage.EnqueueResize(originalSize);
                     layerImage.EnqueueClear();
-                    savedChunks[layer.Id].ApplyChunksToImage(layerImage);
+                    savedChunks[id].ApplyChunksToImage(layerImage);
                     layerImage.CommitChanges();
                 });
             }

+ 62 - 14
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,6 +1,7 @@
 using System.Collections.Immutable;
 using System.Collections.ObjectModel;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -17,6 +18,8 @@ internal class DuplicateFolder_Change : Change
     private Guid[] contentDuplicateGuids;
 
     private Guid[]? childGuidsToUse;
+    private Dictionary<Guid, List<Guid>> keyFramesMap = new();
+    private Dictionary<Guid, Guid> nodeMap = new();
 
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
@@ -64,14 +67,15 @@ internal class DuplicateFolder_Change : Change
         List<IChangeInfo> operations = new();
 
         target.NodeGraph.AddNode(clone);
-        
+
         var previousConnection = targetInput.Connection;
 
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
-        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node, previousConnection?.Node as Node, out originalPositions));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
 
-        DuplicateContent(target, clone, existingLayer, operations);
+        DuplicateContent(target, clone, existingLayer, operations, firstApply);
 
         ignoreInUndo = false;
 
@@ -116,29 +120,70 @@ internal class DuplicateFolder_Change : Change
     }
 
     private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
-        List<IChangeInfo> operations)
+        List<IChangeInfo> operations, bool firstApply)
     {
-        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+        if (firstApply)
+        {
+            nodeMap = new Dictionary<Guid, Guid>();
+            nodeMap[existingLayer.Id] = clone.Id;
+        }
 
-        nodeMap[existingLayer.Id] = clone.Id;
         int counter = 0;
         List<Guid> contentGuidList = new();
 
+        if (firstApply)
+        {
+            keyFramesMap = new Dictionary<Guid, List<Guid>>();
+        }
+
+        int childCounter = 0;
+
         existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
         {
             if (x is not Node targetNode)
                 return false;
 
             Node? node = targetNode.Clone();
-            
-            if(node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+
+            if (contentDuplicateGuids != null && contentDuplicateGuids.Length > 0)
+            {
+                node.Id = contentDuplicateGuids[childCounter];
+                childCounter++;
+            }
+            else
             {
-                node.Id = childGuidsToUse[counter];
-                counter++;
+                if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+                {
+                    node.Id = childGuidsToUse[counter];
+                    counter++;
+                }
+            }
+
+            if (firstApply)
+            {
+                keyFramesMap[node.Id] = new List<Guid>();
+                keyFramesMap[node.Id].AddRange(x.KeyFrames.Select(kf => kf.KeyFrameGuid));
+            }
+            else
+            {
+                if (keyFramesMap.TryGetValue(node.Id, out List<Guid>? keyFrameGuids))
+                {
+                    for (int i = 0; i < x.KeyFrames.Count; i++)
+                    {
+                        if (i < keyFrameGuids.Count)
+                        {
+                            var kf = x.KeyFrames[i] as KeyFrameData;
+                            kf.KeyFrameGuid = keyFrameGuids[i];
+                        }
+                    }
+                }
+            }
+
+            if (firstApply)
+            {
+                nodeMap[x.Id] = node.Id;
+                contentGuidList.Add(node.Id);
             }
-            
-            nodeMap[x.Id] = node.Id;
-            contentGuidList.Add(node.Id);
 
             target.NodeGraph.AddNode(node);
 
@@ -154,6 +199,9 @@ internal class DuplicateFolder_Change : Change
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
 
-        contentDuplicateGuids = contentGuidList.ToArray();
+        if (firstApply)
+        {
+            contentDuplicateGuids = contentGuidList.ToArray();
+        }
     }
 }

+ 2 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs

@@ -28,6 +28,8 @@ public static class PixiEditorSettings
     {
         public static SyncedSetting<bool> EnableSharedToolbar { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
 
+        public static SyncedSetting<bool> SelectionTintingEnabled { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
         public static SyncedSetting<RightClickMode> RightClickMode { get; } =
             SyncedSetting.NonOwned<RightClickMode>(PixiEditor);
 

+ 0 - 1
src/PixiEditor.sln

@@ -11,7 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildConfiguration", "Build
 	ProjectSection(SolutionItems) = preProject
 		Custom.ruleset = Custom.ruleset
 		Directory.Build.props = Directory.Build.props
-		stylecop.json = stylecop.json
 	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{6A9DA760-1E47-414C-B8E8-3B4927F18131}"

+ 124 - 124
src/PixiEditor/Data/Localization/Languages/ar.json

@@ -8,16 +8,16 @@
   "DISCORD": "ديسكورد",
   "KEY_BINDINGS": "ارتباطات المفاتيح",
   "MISC": "متنوع",
-  "SHOW_STARTUP_WINDOW": "عرض نافذة بدء التشغيل",
+  "SHOW_STARTUP_WINDOW": "عرض نافذة بدئ التشغيل",
   "RECENT_FILE_LENGTH": "طول قائمة الملفات الاخيرة",
   "RECENT_FILE_LENGTH_TOOLTIP": "كم عدد المستندات التي يتم اضهارها في ملف > الملفات الاخيرة. الافتراضي: 8",
   "DEFAULT_NEW_SIZE": "الحجم الافتراضي للملف الجديد",
   "WIDTH": "العرض",
   "HEIGHT": "الطول",
-  "TOOLS": "ألادوات",
+  "TOOLS": "الأدوات",
   "ENABLE_SHARED_TOOLBAR": "تمكين شريط الادوات المشترك",
   "AUTOMATIC_UPDATES": "التحديثات التلقائية",
-  "CHECK_FOR_UPDATES": "تحقق من التحديثات عند بدء التشغيل",
+  "CHECK_FOR_UPDATES": "تحقق من التحديثات عند بدئ التشغيل",
   "UPDATE_STREAM": "مصدر التحديث",
   "UPDATE_CHANNEL_HELP_TOOLTIP": "لا يمكن تغيير قنوات التحديث إلا في إصدار مستقل (يتم تنزيله من https://pixieditor.net).\nتتعامل إصدارات Steam و Microsoft Store مع التحديثات بشكل منفصل.",
   "DEBUG": "معالجة",
@@ -25,14 +25,14 @@
   "OPEN_CRASH_REPORTS_DIR": "افتح دليل تقارير الاعطال",
   "DISCORD_RICH_PRESENCE": "Rich Presence",
   "ENABLED": "مفعل",
-  "SHOW_IMAGE_NAME": "اضهار اسم الصورة",
-  "SHOW_IMAGE_SIZE": "اضهار حجم الصورة",
-  "SHOW_LAYER_COUNT": "اضهار عدد الطبقات",
+  "SHOW_IMAGE_NAME": "إظهار اسم الصورة",
+  "SHOW_IMAGE_SIZE": "إظهار حجم الصورة",
+  "SHOW_LAYER_COUNT": "إظهار عدد الطبقات",
   "FILE": "ملف",
   "RECENT": "مؤخرًا",
   "OPEN": "فتح",
-  "SAVE_PIXI": "حفض ( .pixi )",
-  "SAVE_AS_PIXI": "حفض جديد ك ( .pixi )",
+  "SAVE_PIXI": "حفظ ( .pixi )",
+  "SAVE_AS_PIXI": "حفظ جديد كـ( .pixi )",
   "EXPORT_IMG": "تصدير (png, .jpg., الخ.)",
   "EDIT": "تعديل",
   "EXIT": "خروج",
@@ -85,7 +85,7 @@
   "PATH_DOES_NOT_EXIST": "{0} غير موجود.",
   "LOCATION_DOES_NOT_EXIST": "الموقع غير موجود.",
   "FILE_NOT_FOUND": "لم يتم العثور على الملف.",
-  "ARE_YOU_SURE": "هل انت متاكد؟",
+  "ARE_YOU_SURE": "هل انت متأكد؟",
   "ARE_YOU_SURE_PATH_FULL_PATH": "هل أنت متأكد أنك تريد حذف {0}؟\nستفقد هذه البيانات لجميع التركيبات.\n(مسار كامل: {1})",
   "FAILED_TO_OPEN_FILE": "فشل في فتح الملف",
   "OLD_FILE_FORMAT": "تنسيق الملف القديم",
@@ -93,14 +93,14 @@
   "NOTHING_FOUND": "لم يتم العثور على شيء",
   "EXPORT": "تصدير",
   "EXPORT_IMAGE": "تصدير الصورة",
-  "IMPORT": "اضافة",
-  "SHORTCUT_TEMPLATES": "اختصارات القوالب",
+  "IMPORT": "إضافة",
+  "SHORTCUT_TEMPLATES": "إختصارات القوالب",
   "RESET_ALL": "إعادة ضبط الكل",
   "LAYER": "طبقة",
   "LAYER_DELETE_SELECTED": "حذف الطبقة/المجلد النشط",
   "LAYER_DELETE_SELECTED_DESCRIPTIVE": "حذف الطبقة أو المجلد النشط",
-  "LAYER_DELETE_ALL_SELECTED": "احذف جميع الطبقات / المجلدات المحددة",
-  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "احذف كل الطبقات و / أو المجلدات المحددة",
+  "LAYER_DELETE_ALL_SELECTED": "حذف جميع الطبقات / المجلدات المحددة",
+  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "حذف كل الطبقات و / أو المجلدات المحددة",
   "DELETE_SELECTED_PIXELS": "حذف البكسلات المحددة",
   "NEW_FOLDER": "مجلد جديد",
   "CREATE_NEW_FOLDER": "انشاء مجلد جديد",
@@ -108,34 +108,34 @@
   "CREATE_NEW_LAYER": "انشاء طبقة جديدة",
   "NEW_IMAGE": "صورة جديدة",
   "CREATE_NEW_IMAGE": "انشاء صورة جديدة",
-  "SAVE": "حفض",
-  "SAVE_AS": "حفض جديد...",
+  "SAVE": "حفظ",
+  "SAVE_AS": "حفظ جديد...",
   "IMAGE": "صورة",
-  "SAVE_IMAGE": "حفض الصورة",
-  "SAVE_IMAGE_AS": "حفض الصورة كجديدة",
+  "SAVE_IMAGE": "حفظ الصورة",
+  "SAVE_IMAGE_AS": "حفظ الصورة كجديدة",
   "DUPLICATE": "تكرار",
   "DUPLICATE_SELECTED_LAYER": "تكرار الطبقة المجددة",
-  "CREATE_MASK": "انشاء قناع",
-  "DELETE_MASK": "حذف قناع",
+  "CREATE_MASK": "إنشاء قناع",
+  "DELETE_MASK": "حذف القناع",
   "TOGGLE_MASK": "تبديل القناع",
   "APPLY_MASK": "تطبيق القناع",
   "TOGGLE_VISIBILITY": "تبديل الرؤية",
-  "MOVE_MEMBER_UP": "حرك العضو لأعلى",
-  "MOVE_MEMBER_UP_DESCRIPTIVE": "انقل الطبقة أو المجلد المحدد لأعلى",
-  "MOVE_MEMBER_DOWN": "حرك العضو لأسفل",
-  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "انقل الطبقة أو المجلد المحدد لأسفل",
+  "MOVE_MEMBER_UP": "تحريك العضو لأعلى",
+  "MOVE_MEMBER_UP_DESCRIPTIVE": "نقل الطبقة أو المجلد المحدد لأعلى",
+  "MOVE_MEMBER_DOWN": "تحريك العضو لأسفل",
+  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "نقل الطبقة أو المجلد المحدد لأسفل",
   "MERGE_ALL_SELECTED_LAYERS": "دمج كل الطبقات المحددة",
-  "MERGE_WITH_ABOVE": "دمج الطبقة المحددة أعلاه",
+  "MERGE_WITH_ABOVE": "دمج الطبقة المحددة مع أعلاها",
   "MERGE_WITH_ABOVE_DESCRIPTIVE": "دمج الطبقة المحددة مع الطبقة التي فوقها",
-  "MERGE_WITH_BELOW": "دمج الطبقة المحددة أدناه",
+  "MERGE_WITH_BELOW": "دمج الطبقة المحددة مع أدناها",
   "MERGE_WITH_BELOW_DESCRIPTIVE": "دمج الطبقة المحددة مع الطبقة الموجودة تحتها",
-  "ADD_REFERENCE_LAYER": "أضف طبقة مرجعية",
-  "DELETE_REFERENCE_LAYER": "احذف الطبقة المرجعية",
+  "ADD_REFERENCE_LAYER": "إضافة طبقة مرجعية",
+  "DELETE_REFERENCE_LAYER": "حذف الطبقة المرجعية",
   "TRANSFORM_REFERENCE_LAYER": "تحويل الطبقة المرجعية",
   "TOGGLE_REFERENCE_LAYER_POS": "تبديل موضع الطبقة المرجعية",
   "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "تبديل الطبقة المرجعية بين الأعلى أو الأكثر أدناه",
   "RESET_REFERENCE_LAYER_POS": "إعادة تعيين موضع الطبقة المرجعية",
-  "CLIP_CANVAS": "مقطع الصورة",
+  "CLIP_CANVAS": "حف الصورة",
   "FLIP_IMG_VERTICALLY": "قلب الصورة عموديًا",
   "FLIP_IMG_HORIZONTALLY": "قلب الصورة أفقيًا",
   "FLIP_LAYERS_VERTICALLY": "قلب الطبقات المحددة عموديًا",
@@ -149,40 +149,40 @@
   "TOGGLE_VERT_SYMMETRY_AXIS": "تبديل محور التناظر العمودي",
   "TOGGLE_HOR_SYMMETRY_AXIS": "تبديل محور التناظر الأفقي",
   "RESIZE_DOCUMENT": "تغيير حجم العنصر",
-  "RESIZE_CANVAS": "تغيير حجم الصورة",
+  "RESIZE_CANVAS": "تغيير حجم اللوحة",
   "CENTER_CONTENT": "توسيط المحتوى",
   "CUT": "قص",
   "CUT_DESCRIPTIVE": "قص المنطقة/الطبقات المحددة",
   "PASTE": "لصق",
   "PASTE_DESCRIPTIVE": "لصق محتويات الحافظة",
   "PASTE_AS_NEW_LAYER": "لصق كطبقة جديدة",
-  "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "لصق من الحافضةكطبقة جديدة",
+  "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "لصق من الحافظة كطبقة جديدة",
   "PASTE_REFERENCE_LAYER": "لصق الطبقة المرجعية",
   "PASTE_REFERENCE_LAYER_DESCRIPTIVE": "لصق محتويات الحافظة كطبقة مرجعية",
   "PASTE_COLOR": "لصق اللون",
   "PASTE_COLOR_DESCRIPTIVE": "لصق اللون من الحافظة",
-  "PASTE_COLOR_SECONDARY": "لصق اللون على أنه ثانوي",
-  "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "الصق اللون من الحافظة كلون ثانوي",
+  "PASTE_COLOR_SECONDARY": "لصق اللون كلون ثانوي",
+  "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "لصق اللون من الحافظة كلون ثانوي",
   "CLIPBOARD": "الحافظة",
   "COPY": "نسخ",
-  "COPY_DESCRIPTIVE": "نسخ الي الحافضة",
+  "COPY_DESCRIPTIVE": "نسخ الي الحافظة",
   "COPY_COLOR_HEX": "نسخ اللون الاساسي (HEX)",
-  "COPY_COLOR_HEX_DESCRIPTIVE": "نسخ اللون الاساسي ك HEX",
+  "COPY_COLOR_HEX_DESCRIPTIVE": "نسخ اللون الاساسي كـ HEX",
   "COPY_COLOR_RGB": "نسخ اللون الاساسي (RGB)",
-  "COPY_COLOR_RGB_DESCRIPTIVE": "نسخ اللون الاساسي ك RGB",
+  "COPY_COLOR_RGB_DESCRIPTIVE": "نسخ اللون الاساسي كـ RGB",
   "COPY_COLOR_SECONDARY_HEX": "نسخ اللون الثانوي (HEX)",
-  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "نسخ اللون الثانوي ك HEX",
+  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "نسخ اللون الثانوي كـ HEX",
   "COPY_COLOR_SECONDARY_RGB": "نسخ اللون الثانوي (RGB)",
-  "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "نسخ اللون الثانوي ك RGB",
-  "PALETTE_COLORS": "لوحة الالوان",
-  "REPLACE_SECONDARY_BY_PRIMARY": "استبدل اللون الثانوي باللون الأساسي",
-  "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "استبدل اللون الثانوي باللون الأساسي",
-  "REPLACE_PRIMARY_BY_SECONDARY": "استبدل اللون الأساسي باللون الثانوي",
-  "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "استبدل اللون الأساسي باللون الثانوي",
-  "OPEN_PALETTE_BROWSER": "افتح متصفح لوحة الألوان",
+  "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "نسخ اللون الثانوي كـ RGB",
+  "PALETTE_COLORS": "لوحة الألوان",
+  "REPLACE_SECONDARY_BY_PRIMARY": "استبدال اللون الأساسي بالثانوي",
+  "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "استبدال اللون الأساسي باللون الثانوي",
+  "REPLACE_PRIMARY_BY_SECONDARY": "استبدال اللون الثانوي بالأساسي",
+  "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "استبدال اللون الثانوي باللون الأساسي",
+  "OPEN_PALETTE_BROWSER": "فتح متصفح لوحة الألوان",
   "OVERWRITE_PALETTE_CONSENT": "اللوحة '{0}' موجودة بالفعل ، هل تريد استبدالها؟",
   "PALETTE_EXISTS": "لوح الألوان موجود بالفعل",
-  "REPLACE_PALETTE_CONSENT": "هل تريد استبدال اللوحة الحالية باللوحة المحددة؟",
+  "REPLACE_PALETTE_CONSENT": "هل تريد استبدال اللوحة المحددة باللوحة الحالية؟",
   "REPLACE_PALETTE": "استبدال اللوحة الحالية",
   "SELECT_COLOR_1": "حدد اللون 1",
   "SELECT_COLOR_2": "حدد اللون 2",
@@ -194,7 +194,7 @@
   "SELECT_COLOR_8": "حدد اللون 8",
   "SELECT_COLOR_9": "حدد اللون 9",
   "SELECT_COLOR_10": "حدد اللون 10",
-  "SELECT_TOOL": "حدد الاداة {0}",
+  "SELECT_TOOL": "حدد الأداة {0}",
   "SELECT_COLOR_1_DESCRIPTIVE": "حدد اللون الأول في اللوحة",
   "SELECT_COLOR_2_DESCRIPTIVE": "حدد اللون الثاني في اللوحة",
   "SELECT_COLOR_3_DESCRIPTIVE": "حدد اللون الثالث في اللوحة",
@@ -208,23 +208,23 @@
   "SWAP_COLORS": "تبديل الألوان",
   "SWAP_COLORS_DESCRIPTIVE": "تبديل الألوان الأساسية والثانوية",
   "SEARCH": "بحث",
-  "COMMAND_SEARCH": "اوامر البحث",
-  "OPEN_COMMAND_SEARCH": "فتح نافذة اوامر البحث",
+  "COMMAND_SEARCH": "أوامر البحث",
+  "OPEN_COMMAND_SEARCH": "فتح نافذة أوامر البحث",
   "SELECT": "حدد",
-  "DESELECT": "الغاء التحديد",
+  "DESELECT": "إلغاء التحديد",
   "INVERT": "عكس",
-  "SELECTION": "اختيار",
-  "SELECT_ALL": "حدد الكل",
-  "SELECT_ALL_DESCRIPTIVE": "حدد كل شيء",
+  "SELECTION": "تحديد",
+  "SELECT_ALL": "تحديد الكل",
+  "SELECT_ALL_DESCRIPTIVE": "تحديد كل شيء",
   "CLEAR_SELECTION": "حذف المحدد",
-  "INVERT_SELECTION": "اقلب المحدد",
-  "INVERT_SELECTION_DESCRIPTIVE": "اقلب المنطقة المحددة",
+  "INVERT_SELECTION": "عكس المحدد",
+  "INVERT_SELECTION_DESCRIPTIVE": "عكس المنطقة المحددة",
   "TRANSFORM_SELECTED_AREA": "تحويل المنطقة المحددة",
   "NUDGE_SELECTED_LEFT": "دفع العنصر المحدد لليسار",
   "NUDGE_SELECTED_RIGHT": "دفع العنصر المحدد لليمين",
   "NUDGE_SELECTED_UP": "دفع العنصر المحدد لأعلى",
   "NUDGE_SELECTED_DOWN": "دفع العنصر المحدد لأسفل",
-  "MASK_FROM_SELECTION": "قناع جديد من الاختيار",
+  "MASK_FROM_SELECTION": "قناع جديد من المحدد",
   "MASK_FROM_SELECTION_DESCRIPTIVE": "التحديد لقناع جديد",
   "ADD_SELECTION_TO_MASK": "إضافة التحديد إلى القناع",
   "SUBTRACT_SELECTION_FROM_MASK": "قطع التحديد من القناع",
@@ -232,8 +232,8 @@
   "SELECTION_TO_MASK": "التحديد للقناع",
   "TO_NEW_MASK": "إلى قناع جديد",
   "ADD_TO_MASK": "أضف إلى القناع",
-  "SUBTRACT_FROM_MASK": "قص من القناع",
-  "INTERSECT_WITH_MASK": "تتقاطع مع القناع",
+  "SUBTRACT_FROM_MASK": "إنقاص من القناع",
+  "INTERSECT_WITH_MASK": "تقاطع مع القناع",
   "STYLUS": "قلم",
   "TOGGLE_PEN_MODE": "تبديل وضع القلم",
   "UNDO": "تراجع",
@@ -247,12 +247,12 @@
   "NEW_WINDOW_FOR_IMG": "نافذة جديدة للصورة الحالية",
   "CENTER_ACTIVE_VIEWPORT": "توسيط إطار العرض النشط",
   "FLIP_VIEWPORT_HORIZONTALLY": "قلب إطار العرض أفقيًا",
-  "FLIP_VIEWPORT_VERTICALLY": "قلب إطار العرض رأسيًا",
+  "FLIP_VIEWPORT_VERTICALLY": "قلب إطار العرض عموديًا",
   "SETTINGS": "الاعدادات",
   "OPEN_SETTINGS": "فتح الاعدادات",
   "OPEN_SETTINGS_DESCRIPTIVE": "فتح نافذة الاعدادات",
   "OPEN_STARTUP_WINDOW": "فتح نافذة التشغيل",
-  "OPEN_SHORTCUT_WINDOW": "افتح نافذة الاختصارات",
+  "OPEN_SHORTCUT_WINDOW": "فتح نافذة الاختصارات",
   "OPEN_ABOUT_WINDOW": "فتح نافذة المعلومات",
   "ERROR": "خطأ",
   "INTERNAL_ERROR": "خطأ داخلي",
@@ -266,51 +266,51 @@
   "DONATE": "تبرع",
   "YES": "نعم",
   "NO": "لا",
-  "CANCEL": "الغاء",
+  "CANCEL": "إلغاء",
   "UNNAMED": "بدون اسم",
-  "OPEN_COMMAND_DEBUG_WINDOW": "افتح نافذة تصحيح أخطاء الأمر",
+  "OPEN_COMMAND_DEBUG_WINDOW": "افتح نافذة معالجة أخطاء الأمر",
   "DELETE": "حذف",
   "USER_PREFS": "تفضيلات المستخدم (Roaming)",
-  "SHORTCUT_FILE": "اختصار الملف (Roaming)",
+  "SHORTCUT_FILE": "ملف الاختصارات (Roaming)",
   "EDITOR_DATA": "بيانات البرنامج (Local)",
   "MOVE_VIEWPORT_TOOLTIP": "ينقل إطار العرض. ({0})",
-  "MOVE_VIEWPORT_ACTION_DISPLAY": "انقر وانتقل لتحريك منفذ العرض",
+  "MOVE_VIEWPORT_ACTION_DISPLAY": "انقر وانتقل لتحريك إطار العرض",
   "MOVE_TOOL_TOOLTIP": "ينقل وحدات البكسل المحددة ({0}). اضغط مع الاستمرار على Ctrl لتحريك كل الطبقات.",
   "MOVE_TOOL_ACTION_DISPLAY": "استمر في الضغط على الماوس لتحريك وحدات البكسل المحددة. اضغط مع الاستمرار على Ctrl لتحريك كل الطبقات.",
   "PEN_TOOL_TOOLTIP": "قلم. ({0})",
   "PEN_TOOL_ACTION_DISPLAY": "اضغط وحرك للرسم.",
   "PIXEL_PERFECT_SETTING": "بكسل مثالي",
-  "RECTANGLE_TOOL_TOOLTIP": "رسم مستطيل على الصورة ({0}). اضغط مع الاستمرار على Shift لرسم مربع.",
+  "RECTANGLE_TOOL_TOOLTIP": "رسم مستطيل على اللوحة ({0}). اضغط مع الاستمرار على Shift لرسم مربع.",
   "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لرسم مستطيل. اضغط مع الاستمرار على Shift لرسم مربع.",
   "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT": "انقر وحرك لرسم مربع.",
   "KEEP_ORIGINAL_IMAGE_SETTING": "احتفظ بالصورة الأصلية",
-  "ROTATE_VIEWPORT_TOOLTIP": "يدور منفذ العرض. ({0})",
+  "ROTATE_VIEWPORT_TOOLTIP": "يدير إطار العرض. ({0})",
   "ROTATE_VIEWPORT_ACTION_DISPLAY": "انقر وحرك لتدوير منفذ العرض",
   "SELECT_TOOL_TOOLTIP": "يختار المنطقة. ({0})",
-  "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لتحديد منطقة. اضغط مع الاستمرار على Shift للإضافة إلى التحديد الحالي. استمر في الضغط على Ctrl لقصها منه.",
+  "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لتحديد منطقة. اضغط مع الاستمرار على Shift للإضافة إلى التحديد الحالي. استمر في الضغط على Ctrl لإنقاصها منه.",
   "SELECT_TOOL_ACTION_DISPLAY_SHIFT": "انقر وانتقل للإضافة إلى التحديد الحالي.",
-  "SELECT_TOOL_ACTION_DISPLAY_CTRL": "انقر وانتقل للقص من التحديد الحالي.",
+  "SELECT_TOOL_ACTION_DISPLAY_CTRL": "انقر وانتقل للإنقاص من التحديد الحالي.",
   "ZOOM_TOOL_TOOLTIP": "تكبير / تصغير منفذ العرض ({0}). انقر للتكبير ، او مع الاستمرار في الضغط على مفتاح alt وانقر للتصغير.",
   "ZOOM_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك للتكبير. انقر للتكبير ، واضغط باستمرار على ctrl وانقر للتصغير.",
   "ZOOM_TOOL_ACTION_DISPLAY_CTRL": "انقر وتحرك للتكبير. انقر للتصغير ، وحرر مفتاح التحكم وانقر للتكبير.",
   "BRIGHTNESS_TOOL_TOOLTIP": "جعل وحدات البكسل أفتح أو أغمق ({0}). اضغط باستمرار على مفتاح Ctrl لجعل البكسل أغمق.",
-  "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "ارسم على وحدات البكسل لجعلها أكثر إشراقًا. اضغط باستمرار على Ctrl للتغميق.",
-  "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "ارسم بالبكسل لجعلها أكثر قتامة. اترك Ctrl لتفتيح.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "ارسم على وحدات البكسل لجعلها أفتح. اضغط باستمرار على Ctrl للتغميق.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "ارسم بالبكسل لجعلها أغمق. اترك Ctrl لتفتيح.",
   "COLOR_PICKER_TOOLTIP": "يختار اللون الأساسي من الصورة. ({0})",
-  "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "انقر لاختيار الألوان. اضغط باستمرار على Ctrl لإخفاء الصورة. اضغط مع الاستمرار على Shift لإخفاء الطبقة المرجعية",
+  "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "انقر لاختيار الألوان. اضغط باستمرار على Ctrl لإخفاء اللوحة. اضغط مع الاستمرار على Shift لإخفاء الطبقة المرجعية",
   "ELLIPSE_TOOL_TOOLTIP": "رسم قطع ناقص على الصورة ({0}). اضغط مع الاستمرار على Shift لرسم دائرة.",
   "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وحرك الماوس لرسم شكل بيضاوي. اضغط مع الاستمرار على Shift لرسم دائرة.",
   "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT": "انقر وحرك الماوس لرسم دائرة.",
   "ERASER_TOOL_TOOLTIP": "يمحو اللون من البكسل. ({0})",
   "ERASER_TOOL_ACTION_DISPLAY": "انقر وحرك للمسح.",
   "FLOOD_FILL_TOOL_TOOLTIP": "املأ المنطقة باللون. ({0})",
-  "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "اضغط على منطقة لملئها. اضغط باستمرار على مفتاح Ctrl للتطبيق على جميع الطبقات.",
-  "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "اضغط على منطقة لملئها. حرر Ctrl للنظر في الطبقات الحالية فقط.",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "اضغط على منطقة لملئها. اضغط باستمرار على مفتاح Ctrl لاعتبار جميع الطبقات.",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "اضغط على منطقة لملئها. حرر Ctrl لاعتبار الطبقات الحالية فقط.",
   "LASSO_TOOL_TOOLTIP": "لاسو. ({0})",
-  "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لتحديد وحدات البكسل داخل لاسو. اضغط مع الاستمرار على Shift للإضافة إلى التحديد الحالي. استمر في الضغط على Ctrl للحذف منه.",
+  "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لتحديد وحدات البكسل داخل لاسو. اضغط مع الاستمرار على Shift للإضافة إلى التحديد الحالي. استمر في الضغط على Ctrl للإنقاص منه.",
   "LASSO_TOOL_ACTION_DISPLAY_SHIFT": "انقر وتحرك لإضافة وحدات بكسل داخل لاسو إلى التحديد.",
   "LASSO_TOOL_ACTION_DISPLAY_CTRL": "انقر وتحرك لحذف وحدات البكسل داخل لاسو من التحديد.",
-  "LINE_TOOL_TOOLTIP": "رسم خط على الصورة ({0}). اضغط مع الاستمرار على Shift لتمكين الالتقاط.",
+  "LINE_TOOL_TOOLTIP": "رسم خط على اللوحة ({0}). اضغط مع الاستمرار على Shift لتمكين الالتقاط.",
   "LINE_TOOL_ACTION_DISPLAY_DEFAULT": "انقر وتحرك لرسم خط. اضغط مع الاستمرار على Shift لتمكين الالتقاط.",
   "LINE_TOOL_ACTION_DISPLAY_SHIFT": "انقر وحرك الماوس لرسم خط مع تمكين الالتقاط.",
   "MAGIC_WAND_TOOL_TOOLTIP": "العصا السحرية ({0}). ملء المحدد",
@@ -320,42 +320,42 @@
   "COLOR_PICKER_TOOL": "أداة انتقاء اللون",
   "ELLIPSE_TOOL": "القطع الناقص",
   "ERASER_TOOL": "ممحاة",
-  "FLOOD_FILL_TOOL": "ملء الفيضانات",
+  "FLOOD_FILL_TOOL": "ملؤ الفيضانات",
   "LASSO_TOOL": "لاسو",
   "LINE_TOOL": "خط",
   "MAGIC_WAND_TOOL": "العصا السحرية",
   "MOVE_TOOL": "تحريك",
-  "MOVE_VIEWPORT_TOOL": "تحريك منفذ العرض",
+  "MOVE_VIEWPORT_TOOL": "تحريك إطار العرض",
   "RECTANGLE_TOOL": "مستطيل",
-  "ROTATE_VIEWPORT_TOOL": "تدوير منفذ العرض",
+  "ROTATE_VIEWPORT_TOOL": "تدوير إطار العرض",
   "SELECT_TOOL_NAME": "حدد",
   "ZOOM_TOOL": "تكبير",
   "SHAPE_LABEL": "شكل",
   "MODE_LABEL": "الوضع",
   "SCOPE_LABEL": "نطاق",
-  "FILL_SHAPE_LABEL": "ملء الشكل",
-  "FILL_COLOR_LABEL": "ملء اللون",
+  "FILL_SHAPE_LABEL": "ملؤ الشكل",
+  "FILL_COLOR_LABEL": "ملؤ اللون",
   "TOOL_SIZE_LABEL": "حجم الأداة",
   "STRENGTH_LABEL": "قوة",
   "NEW": "جديد",
-  "ADD": "اضافة",
-  "SUBTRACT": "قص",
-  "INTERSECT": "تتقاطع",
+  "ADD": "إضافة",
+  "SUBTRACT": "إنقاص",
+  "INTERSECT": "تقاطع",
   "RECTANGLE": "مستطيل",
   "CIRCLE": "دائرة",
   "ABOUT": "حول البرنامج",
   "MINIMIZE": "تصغير",
-  "RESTORE": "اعادة",
+  "RESTORE": "إعادة",
   "MAXIMIZE": "تكبير",
-  "CLOSE": "اغلق",
-  "EXPORT_SIZE_HINT": "إذا كنت ترغب في مشاركة الصورة ، فجرّب {0}٪ للحصول على أفضل وضوح",
-  "CREATE": "انشاء",
+  "CLOSE": "إغلاق",
+  "EXPORT_SIZE_HINT": "إن كنت ترغب في مشاركة الصورة ، فجرّب {0}٪ للحصول على أفضل وضوح",
+  "CREATE": "إنشاء",
   "BASE_LAYER_NAME": "الطبقة الرئيسية",
   "ENABLE_MASK": "تمكين القناع",
   "SELECTED_AREA_EMPTY": "المنطقة المحددة فارغة",
   "NOTHING_TO_COPY": "لا يوجد شي لنسخه",
   "REFERENCE_LAYER_PATH": "مسار الطبقة المرجعية",
-  "FLIP": "توجيه",
+  "FLIP": "قلب",
   "ROTATION": "دوران",
   "ROT_IMG_90_D": "تدوير الصورة 90 درجة",
   "ROT_IMG_180_D": "تدوير الصورة 180 درجة",
@@ -366,63 +366,63 @@
   "UNNAMED_PALETTE": "لوح غير مسمى",
   "CLICK_SELECT_PRIMARY": "انقر لتحديد اللون الرئيسي.",
   "PEN_MODE": "وضع القلم",
-  "VIEW": "منظر",
+  "VIEW": "إظهار",
   "HORIZONTAL_LINE_SYMMETRY": "تناظر الخط الأفقي",
   "VERTICAL_LINE_SYMMETRY": "تناظر الخط العمودي",
   "COLOR_PICKER_TITLE": "أداة اختيار اللون",
-  "COLOR_SLIDERS_TITLE": "لوحة الالوان",
+  "COLOR_SLIDERS_TITLE": "أشرطة الألوان",
   "PALETTE_TITLE": "اللوحة",
   "SWATCHES_TITLE": "حوامل",
   "LAYERS_TITLE": "الطبقات",
   "NORMAL_BLEND_MODE": "عادي",
-  "DARKEN_BLEND_MODE": "أغمق",
+  "DARKEN_BLEND_MODE": "تغميق",
   "MULTIPLY_BLEND_MODE": "تضاعف",
   "COLOR_BURN_BLEND_MODE": "احتراق الالوان",
-  "LIGHTEN_BLEND_MODE": "فاتح",
+  "LIGHTEN_BLEND_MODE": "تفتيح",
   "SCREEN_BLEND_MODE": "شاشة",
   "COLOR_DODGE_BLEND_MODE": "انقاص كثافة اللون",
   "OVERLAY_BLEND_MODE": "تراكب",
   "SOFT_LIGHT_BLEND_MODE": "ضوء خافت",
-  "HARD_LIGHT_BLEND_MODE": "ضوء غامق",
-  "DIFFERENCE_BLEND_MODE": "اختلاف",
+  "HARD_LIGHT_BLEND_MODE": "ضوء شديد",
+  "DIFFERENCE_BLEND_MODE": "الفرق",
   "EXCLUSION_BLEND_MODE": "استبعاد",
-  "HUE_BLEND_MODE": "مسحة",
+  "HUE_BLEND_MODE": "المسحة",
   "SATURATION_BLEND_MODE": "التشبع",
-  "LUMINOSITY_BLEND_MODE": "لمعان",
+  "LUMINOSITY_BLEND_MODE": "اللمعان",
   "COLOR_BLEND_MODE": "اللون",
   "NOT_SUPPORTED_BLEND_MODE": "غير مدعوم",
-  "RESTART": "اعد تشغيل",
+  "RESTART": "إعادة التشغيل",
   "SORT_BY": "ترتيب حسب",
   "NAME": "الاسم",
-  "COLORS": "الالوان",
+  "COLORS": "الألوان",
   "DEFAULT": "الافتراضي",
-  "ALPHABETICAL": "مرتب حسب الحروف الأبجدية",
+  "ALPHABETICAL": "أبجدي",
   "COLOR_COUNT": "عدد الألوان",
-  "ANY": "اي",
+  "ANY": "أي",
   "MAX": "الاقصى",
-  "MIN": "الاقل",
+  "MIN": "الأدنى",
   "EXACT": "بالضبط",
   "ASCENDING": "تصاعدي",
   "DESCENDING": "تنازلي",
-  "NAME_IS_TOO_LONG": "الاسم طويل جدا",
+  "NAME_IS_TOO_LONG": "الاسم طويل جدًا",
   "STOP_IT_TEXT1": "هذا يكفي. رتب أسماء ملفاتك.",
   "STOP_IT_TEXT2": "هل يمكنك التوقف عن نسخ هذه الأسماء من فضلك؟",
-  "REPLACER_TOOLTIP": "انقر بزر الماوس الأيمن على لوحة الألوان واختر 'استبدال' أو أسقطها هنا.",
+  "REPLACER_TOOLTIP": "انقر زر الماوس الأيمن على لوحة الألوان واختر 'استبدال' أو ألقها هنا.",
   "CLICK_TO_CHOOSE_COLOR": "انقر لاختيار اللون",
   "REPLACE_COLOR": "استبدل اللون",
-  "PALETTE_COLOR_TOOLTIP": "انقر لتحديد اللون الرئيسي. قم بالسحب والإفلات على لون لوح آخر لتبديلها.",
+  "PALETTE_COLOR_TOOLTIP": "انقر لتحديده كلون رئيسي. قم بالسحب والإفلات على لون لوح آخر لتبديلها.",
   "ADD_FROM_SWATCHES": "أضف من العينات",
   "ADD_COLOR_TO_PALETTE": "أضف اللون إلى لوح الألوان",
   "USE_IN_CURRENT_IMAGE": "استخدم في الصورة الحالية",
-  "ADD_TO_FAVORITES": "اضافة الى المفضلة",
+  "ADD_TO_FAVORITES": "إضافة الى المفضلة",
   "BROWSE_PALETTES": "تصفح اللوحات",
   "LOAD_PALETTE": "تحميل لوحة",
-  "SAVE_PALETTE": "حفض اللوحة",
+  "SAVE_PALETTE": "حفظ اللوحة",
   "FAVORITES": "المفضلة",
   "ADD_FROM_CURRENT_PALETTE": "أضف من اللوحة الحالية",
   "OPEN_PALETTES_DIR_TOOLTIP": "افتح دليل اللوحات في المستكشف",
   "BROWSE_ON_LOSPEC_TOOLTIP": "تصفح اللوحات على Lospec",
-  "IMPORT_FROM_FILE_TOOLTIP": "اضافة من ملف",
+  "IMPORT_FROM_FILE_TOOLTIP": "استيراد من ملف",
   "TOP_LEFT": "أعلى اليسار",
   "TOP_CENTER": "اعلى الوسط",
   "TOP_RIGHT": "اعلى اليمين",
@@ -433,29 +433,29 @@
   "BOTTOM_CENTER": "اسفل الوسط",
   "BOTTOM_RIGHT": "أسفل اليمين",
   "CLIP_TO_BELOW": "مقطع للطبقة أدناه",
-  "MOVE_UPWARDS": "تحرك لأعلى",
+  "MOVE_UPWARDS": "تحريك لأعلى",
   "MOVE_DOWNWARDS": "تحريك للاسفل",
   "MERGE_SELECTED": "دمج المحدد",
   "LOCK_TRANSPARENCY": "قفل الشفافية",
   "COULD_NOT_LOAD_PALETTE": "تعذر إحضار اللوحات",
   "NO_PALETTES_FOUND": "لم يتم العثور على لوحات.",
   "LOSPEC_LINK_TEXT": "سمعت أنه يمكنك العثور على بعضها هنا: lospec.com/palette-list",
-  "PALETTE_BROWSER": "متصفح لوح الألوان",
-  "DELETE_PALETTE_CONFIRMATION": "هل أنت متأكد أنك تريد حذف هذه اللوحة؟ هذا لا يمكن التراجع عنها.",
-  "SHORTCUTS_IMPORTED": "تم اضافة الاختصارات من {0} بنجاح.",
-  "SHORTCUT_PROVIDER_DETECTED": "لقد اكتشفنا أنك قمت بتثبيت {0}. هل تريد اضافة الاختصارات منه؟",
-  "IMPORT_FROM_INSTALLATION": "اضافة من التثبيت",
-  "IMPORT_INSTALLATION_OPTION1": "اضافة من التنزيلات",
-  "IMPORT_INSTALLATION_OPTION2": "استخدام الافتراضي",
-  "IMPORT_FROM_TEMPLATE": "اضافة من قالب",
+  "PALETTE_BROWSER": "متصفح ألواح الألوان",
+  "DELETE_PALETTE_CONFIRMATION": "هل أنت متأكد أنك تريد حذف هذه اللوحة؟ لا يمكن التراجع عن هذا الفعل.",
+  "SHORTCUTS_IMPORTED": "تم استيراد الاختصارات من {0} بنجاح.",
+  "SHORTCUT_PROVIDER_DETECTED": "لقد وجدنا أنك قمت بتثبيت {0}. هل تريد إضافة الاختصارات منها؟",
+  "IMPORT_FROM_INSTALLATION": "إضافة من التثبيت",
+  "IMPORT_INSTALLATION_OPTION1": "إضافة من التثبيت",
+  "IMPORT_INSTALLATION_OPTION2": "استخدام الافتراضيات",
+  "IMPORT_FROM_TEMPLATE": "استيراد من قالب",
   "SHORTCUTS_IMPORTED_SUCCESS": "تم اضافة الاختصارات بنجاح.",
-  "WARNING_RESET_SHORTCUTS_DEFAULT": "هل أنت متأكد من أنك تريد إعادة تعيين جميع الاختصارات إلى قيمتها الافتراضية؟",
+  "WARNING_RESET_SHORTCUTS_DEFAULT": "هل أنت متأكد من أنك تريد إعادة تعيين جميع الاختصارات إلى قيمها الافتراضية؟",
   "SUCCESS": "نجاح",
   "WARNING": "تحذير",
   "ERROR_IMPORTING_IMAGE": "حدث خطأ أثناء استيراد الصورة.",
   "SHORTCUTS_CORRUPTED_TITLE": "ملف الاختصارات تالف",
-  "SHORTCUTS_CORRUPTED": "تعرض ملف الاختصارات للتلف ، اعادة التعيين إلى الوضع الافتراضي.",
-  "FAILED_DOWNLOAD_PALETTE": "فشل تحميل لوح الألوان",
+  "SHORTCUTS_CORRUPTED": "كان ملف الاختصارات مصابًا بالتلف، اعادة التعيين إلى الوضع الافتراضي.",
+  "FAILED_DOWNLOAD_PALETTE": "فشل تنزيل لوح الألوان",
   "FILE_INCORRECT_FORMAT": "لم يكن الملف بتنسيق صحيح",
   "INVALID_FILE": "ملف غير صالح",
   "SHORTCUTS_FILE_INCORRECT_FORMAT": "تنسيق ملف الاختصارات ليس صحيحًا",
@@ -465,14 +465,14 @@
   "SWAP": "تبديل",
   "SHORTCUT_ALREADY_ASSIGNED_SWAP": "تم بالفعل تعيين هذا الاختصار لـ '{0}'\nهل تريد استبدال الاختصار الحالي أم تبديل الاختصارين؟",
   "SHORTCUT_ALREADY_ASSIGNED_OVERWRITE": "تم بالفعل تعيين هذا الاختصار لـ '{0}'\nهل تريد استبدال الاختصار الموجود؟",
-  "UNSAVED_CHANGES": "تغييرات غير محفوضة",
+  "UNSAVED_CHANGES": "تغييرات غير محفوظة",
   "DOCUMENT_MODIFIED_SAVE": "تم تعديل العنصر. هل تريد ان تحفظ التغييرات؟",
   "SESSION_UNSAVED_DATA": "{0} ببيانات غير محفوظة. هل أنت متأكد؟",
   "PROJECT_MAINTAINERS": "مشرفو المشروع",
   "OTHER_AWESOME_CONTRIBUTORS": "وغيرهم من المساهمين الرائعين",
-  "HELP": "مساعد",
-  "STOP_IT_TEXT3": "لا ، حقًا ، توقف عن ذلك.",
-  "STOP_IT_TEXT4": "أليس لديك أي شيء أفضل لتفعله؟",
+  "HELP": "مساعدة",
+  "STOP_IT_TEXT3": "لا، حقًا ، توقف عن ذلك.",
+  "STOP_IT_TEXT4": "أليس لديك شيء أفضل تفعله؟",
   "LINEAR_DODGE_BLEND_MODE": "مراوغة خطية (إضافة)",
   "PRESS_ANY_KEY": "اضغط اي مفتاح",
   "NONE_SHORTCUT": "غير محدد",
@@ -481,15 +481,15 @@
   "PUT_REFERENCE_LAYER_BELOW": "ضع طبقة مرجعية أدناه",
   "TOGGLE_VERTICAL_SYMMETRY": "تبديل التناظر العمودي",
   "TOGGLE_HORIZONTAL_SYMMETRY": "تبديل التناظر الأفقي",
-  "RESET_VIEWPORT": "إعادة تعيين منفذ العرض",
-  "VIEWPORT_SETTINGS": "إعدادات منفذ العرض",
+  "RESET_VIEWPORT": "إعادة تعيين إطار العرض",
+  "VIEWPORT_SETTINGS": "إعدادات إطار العرض",
   "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "انقر بالماوس مع الاستمرار لتحريك وحدات البكسل في الطبقات المحددة.",
   "CTRL_KEY": "Ctrl",
   "SHIFT_KEY": "Shift",
   "ALT_KEY": "Alt",
   "RENAME": "إعادة تسمية",
   "PIXEL_UNIT": "بكسل",
-  "OPEN_LOCALIZATION_DEBUG_WINDOW": "افتح نافذة تصحيح أخطاء الترجمة",
+  "OPEN_LOCALIZATION_DEBUG_WINDOW": "افتح نافذة معالجة أخطاء الترجمة",
   "FORCE_OTHER_FLOW_DIRECTION": "فرض اتجاه تدفق آخر",
   "API_KEY": "مفتاح API",
   "LOCALIZATION_VIEW_TYPE": "نوع عرض الترجمة",

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

@@ -1127,5 +1127,20 @@
   "TEXT_INFO_NODE": "Text info",
   "TEXT_LENGTH": "Length",
   "TEXT_LINE_COUNT": "Line count",
-  "TEXT_SLICE_USE_LENGTH": "Use length"
+  "TEXT_SLICE_USE_LENGTH": "Use length",
+  "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
+  "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
+  "TINT_SELECTION": "Selection tinting",
+  "PAINT_BRUSH_SHAPE_CIRCLE": "Circle",
+  "PAINT_BRUSH_SHAPE_SQUARE": "Square",
+  "BRIGHTNESS_MODE_DEFAULT": "Default",
+  "BRIGHTNESS_MODE_REPEAT": "Repeat",
+  "ROUND_STROKE_CAP": "Round",
+  "BUTT_STROKE_CAP": "Butt",
+  "SQUARE_STROKE_CAP": "Square",
+  "ROUND_STROKE_JOIN": "Round",
+  "MITER_STROKE_JOIN": "Miter",
+  "BEVEL_STROKE_JOIN": "Bevel",
+  "TOGGLE_FULLSCREEN": "Toggle Fullscreen",
+  "TOGGLE_FULLSCREEN_DESCRIPTIVE": "Enter or Exit Fullscreen Mode"
 }

+ 106 - 100
src/PixiEditor/Data/Localization/Languages/es.json

@@ -1,33 +1,33 @@
 {
-  "RECENT_FILES": "Archivo Reciente",
-  "OPEN_FILE": "Abrir Archivo",
+  "RECENT_FILES": "Archivos recientes",
+  "OPEN_FILE": "Abrir archivo",
   "NEW_FILE": "Nuevo",
-  "RECENT_EMPTY_TEXT": "Demasiado espacio vacio",
+  "RECENT_EMPTY_TEXT": "Bastante vacío aquí",
   "LANGUAGE": "Idioma",
   "GENERAL": "General",
   "DISCORD": "Discord",
-  "KEY_BINDINGS": "Fijaciones de teclas",
-  "MISC": "Otros",
-  "SHOW_STARTUP_WINDOW": "Abrir Al Iniciar Windows",
+  "KEY_BINDINGS": "Atajos del teclado",
+  "MISC": "Misceláneo",
+  "SHOW_STARTUP_WINDOW": "Mostrar ventana de bienvenida",
   "RECENT_FILE_LENGTH": "Longitud de la lista de archivos reciente",
-  "RECENT_FILE_LENGTH_TOOLTIP": "Cuántos documentos se muestran en Archivo > Recientes. Por defecto: 8",
-  "DEFAULT_NEW_SIZE": "Nuevo archivo por defecto",
-  "WIDTH": "Anchura",
-  "HEIGHT": "Altura",
+  "RECENT_FILE_LENGTH_TOOLTIP": "Cuántos documentos se muestran en Archivo > Recientes. Predeterminado: 8",
+  "DEFAULT_NEW_SIZE": "Tamaño de imagen predeterminado",
+  "WIDTH": "Ancho",
+  "HEIGHT": "Alto",
   "TOOLS": "Herramientas",
   "ENABLE_SHARED_TOOLBAR": "Activar la barra de herramientas compartida",
-  "AUTOMATIC_UPDATES": "Actualizaciones Automáticas",
+  "AUTOMATIC_UPDATES": "Actualizaciones automáticas",
   "CHECK_FOR_UPDATES": "Buscar actualizaciones al iniciar",
-  "UPDATE_STREAM": "Actualizar stream",
-  "UPDATE_CHANNEL_HELP_TOOLTIP": "Los canales de actualización sólo se pueden cambiar en la versión independiente (descargada de https://pixieditor.net).\nLas versiones de Steam y Microsoft Store gestionan las actualizaciones por separado.",
-  "DEBUG": "Debug",
-  "ENABLE_DEBUG_MODE": "Activar modo Debug",
-  "OPEN_CRASH_REPORTS_DIR": "Abrir lista de reportes",
+  "UPDATE_STREAM": "Frecuencia de actualizaciones",
+  "UPDATE_CHANNEL_HELP_TOOLTIP": "Las actualizaciones sólo se pueden gestionar manualmente en la versión descargada por el sitio del programa (https://pixieditor.net).\nSteam y la tienda de Microsoft gestionan las actualizaciones por separado.",
+  "DEBUG": "Depuración",
+  "ENABLE_DEBUG_MODE": "Activar modo de depuración",
+  "OPEN_CRASH_REPORTS_DIR": "Abrir carpeta de reportes de colapsos",
   "DISCORD_RICH_PRESENCE": "Rich Presence",
   "ENABLED": "Activado",
-  "SHOW_IMAGE_NAME": "Mostrar nombre del archivo",
-  "SHOW_IMAGE_SIZE": "Mostrar tamaño del archivo",
-  "SHOW_LAYER_COUNT": "Mostrar recuento de capas",
+  "SHOW_IMAGE_NAME": "Mostrar nombre de la imagen",
+  "SHOW_IMAGE_SIZE": "Mostrar tamaño de la imagen",
+  "SHOW_LAYER_COUNT": "Mostrar cantidad de capas",
   "FILE": "Archivo",
   "RECENT": "Reciente",
   "OPEN": "Abrir",
@@ -38,119 +38,119 @@
   "EXIT": "Salir",
   "PERCENTAGE": "Porcentaje",
   "ABSOLUTE": "Absoluto",
-  "PRESERVE_ASPECT_RATIO": "Conservar la relación de aspecto",
-  "ANCHOR_POINT": "Punto de anclaje",
-  "RESIZE_IMAGE": "Cambiar el tamaño de la imagen",
-  "RESIZE": "Cambie el tamaño de",
-  "DOCUMENTATION": "Documentacion",
-  "WEBSITE": "Pagina Web",
-  "OPEN_WEBSITE": "Abrir Pagina Web",
+  "PRESERVE_ASPECT_RATIO": "Preservar la relación de aspecto",
+  "ANCHOR_POINT": "Anclaje",
+  "RESIZE_IMAGE": "Reescalar la imagen",
+  "RESIZE": "Confirmar",
+  "DOCUMENTATION": "Documentación",
+  "WEBSITE": "Pagina web",
+  "OPEN_WEBSITE": "Abrir Pagina web",
   "REPOSITORY": "Repositorio",
   "OPEN_REPOSITORY": "Abrir Repositorio",
   "LICENSE": "Licencia",
-  "OPEN_LICENSE": "Abrir Licencia",
+  "OPEN_LICENSE": "Abrir licencia",
   "THIRD_PARTY_LICENSES": "Licencias de terceros",
   "OPEN_THIRD_PARTY_LICENSES": "Abrir licencias de terceros",
   "APPLY_TRANSFORM": "Aplicar transformacion",
-  "INCREASE_TOOL_SIZE": "Aumentar el tamaño de la herramienta",
-  "DECREASE_TOOL_SIZE": "Disminuir el tamaño de la herramienta",
-  "DOWNLOADING_UPDATE": "Instalando actualizacion...",
-  "UPDATE_READY": "Actualización listo para instalar. ¿Desea instalarlo ahora?",
-  "NEW_UPDATE": "Nueva actualizacion",
-  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "No se ha podido actualizar sin privilegios de administrador. Por favor, ejecute PixiEditor como administrador.",
+  "INCREASE_TOOL_SIZE": "Agrandar la herramienta",
+  "DECREASE_TOOL_SIZE": "Desagrandar la herramienta",
+  "DOWNLOADING_UPDATE": "Descargando actualización...",
+  "UPDATE_READY": "La actualización está lista para instalar. ¿Quieres instalarla ahora?",
+  "NEW_UPDATE": "Nueva actualización",
+  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "No se puede actualizar sin privilegios de administrador. Ejecuta PixiEditor como administrador.",
   "INSUFFICIENT_PERMISSIONS": "Permisos insuficientes",
-  "UPDATE_CHECK_FAILED": "Error en la comprobación de la actualización",
-  "COULD_NOT_CHECK_FOR_UPDATES": "No se ha podido comprobar si hay una actualización disponible.",
-  "VERSION": "Version {0}",
+  "UPDATE_CHECK_FAILED": "Error en detección de actualizaciones",
+  "COULD_NOT_CHECK_FOR_UPDATES": "No se pudo detectar si hay actualizaciones.",
+  "VERSION": "Versión {0}",
   "OPEN_TEMP_DIR": "Abrir directorio temporal",
-  "OPEN_LOCAL_APPDATA_DIR": "Abra el directorio local AppData",
-  "OPEN_ROAMING_APPDATA_DIR": "Abrir el directorio Roaming AppData",
+  "OPEN_LOCAL_APPDATA_DIR": "Abra la carpeta Local AppData",
+  "OPEN_ROAMING_APPDATA_DIR": "Abrir la carpeta Roaming AppData",
   "OPEN_INSTALLATION_DIR": "Abrir el directorio de instalación",
-  "DUMP_ALL_COMMANDS": "Volcar todos los comandos",
-  "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Volcar todos los comandos a un archivo de texto",
-  "CRASH": "Error",
-  "CRASH_APP": "Error en la aplicacion",
-  "DELETE_USR_PREFS": "Eliminar las preferencias del usuario (Roaming AppData)",
+  "DUMP_ALL_COMMANDS": "Exportar todos los comandos",
+  "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Exportar todos los comandos a un archivo de texto",
+  "CRASH": "Forzar cierre",
+  "CRASH_APP": "Forzar el cierre de la aplicación",
+  "DELETE_USR_PREFS": "Eliminar las preferencias de usuario (Roaming AppData)",
   "DELETE_SHORTCUT_FILE": "Eliminar el archivo de acceso directo (Roaming AppData)",
   "DELETE_EDITOR_DATA": "Eliminar datos del editor (Local AppData)",
-  "GENERATE_KEY_BINDINGS_TEMPLATE": "Generar plantilla de enlaces de teclas",
-  "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Generar plantilla json de enlaces de claves",
-  "VALIDATE_SHORTCUT_MAP": "Validar dirección de accesos directos",
-  "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Validados dirección de accesos directos",
-  "VALIDATION_KEYS_NOTICE_DIALOG": "Llaves vacías: {0}\nComandos desconocidos: {1}",
+  "GENERATE_KEY_BINDINGS_TEMPLATE": "Generar plantilla de atajos del teclado",
+  "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Generar plantilla de atajos de teclado en formato JSON",
+  "VALIDATE_SHORTCUT_MAP": "Validar mapa de atajos del teclado",
+  "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Valida los atajos del teclado",
+  "VALIDATION_KEYS_NOTICE_DIALOG": "Claves vacías: {0}\nComandos desconocidos: {1}",
   "RESULT": "Resultado",
-  "CLEAR_RECENT_DOCUMENTS": "Limpiar archivos recientes",
-  "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Limpiar documentos recientemente abiertos",
-  "OPEN_CMD_DEBUG_WINDOW": "Abrir caja de comandos debug de windows",
+  "CLEAR_RECENT_DOCUMENTS": "Limpiar documentos recientes",
+  "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Limpiar documentos abiertos recientemente",
+  "OPEN_CMD_DEBUG_WINDOW": "Abrir ventana de comandos de depuración",
   "PATH_DOES_NOT_EXIST": "{0} no existe.",
   "LOCATION_DOES_NOT_EXIST": "La ubicación no existe.",
   "FILE_NOT_FOUND": "Archivo no encontrado.",
   "ARE_YOU_SURE": "¿Estas seguro?",
   "ARE_YOU_SURE_PATH_FULL_PATH": "¿Está seguro de que desea borrar {0}?\nEstos datos se perderán para todas las instalaciones.\n(Ruta completa: {1})",
-  "FAILED_TO_OPEN_FILE": "Error al abrir el archivo",
-  "OLD_FILE_FORMAT": "Formato de archivo viejo",
+  "FAILED_TO_OPEN_FILE": "No se pudo abrir el archivo",
+  "OLD_FILE_FORMAT": "Formato de archivo antiguo",
   "OLD_FILE_FORMAT_DESCRIPTION": "Este archivo .pixi utiliza el formato antiguo,\nque ya no es compatible y no puede abrirse.",
   "NOTHING_FOUND": "Nada encontrado",
   "EXPORT": "Exportar",
   "EXPORT_IMAGE": "Exportar imagen",
   "IMPORT": "Importar",
-  "SHORTCUT_TEMPLATES": "Plantillas de acceso directo",
-  "RESET_ALL": "Reiniciar todo",
+  "SHORTCUT_TEMPLATES": "Plantillas de atajos del teclado",
+  "RESET_ALL": "Restaurar todos",
   "LAYER": "Capa",
   "LAYER_DELETE_SELECTED": "Borrar capa/carpeta activa",
   "LAYER_DELETE_SELECTED_DESCRIPTIVE": "Borrar capa o carpeta activa",
   "LAYER_DELETE_ALL_SELECTED": "Eliminar todas las capas/carpetas seleccionadas",
   "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Eliminar todas las capas y/o carpetas seleccionadas",
-  "DELETE_SELECTED_PIXELS": "Borrar píxeles seleccionados",
+  "DELETE_SELECTED_PIXELS": "Eliminar pixeles seleccionados",
   "NEW_FOLDER": "Nueva carpeta",
   "CREATE_NEW_FOLDER": "Crear nueva carpeta",
   "NEW_LAYER": "Nueva capa",
   "CREATE_NEW_LAYER": "Crear nueva capa",
-  "NEW_IMAGE": "Imagen nueva",
-  "CREATE_NEW_IMAGE": "Crear imagen nueva",
+  "NEW_IMAGE": "Nueva imagen",
+  "CREATE_NEW_IMAGE": "Crear nueva imagen",
   "SAVE": "Guardar",
   "SAVE_AS": "Guardar como...",
   "IMAGE": "Imagen",
   "SAVE_IMAGE": "Guardar imagen",
-  "SAVE_IMAGE_AS": "Guardar imagen como nuevo",
+  "SAVE_IMAGE_AS": "Guardar imagen como nuevo archivo",
   "DUPLICATE": "Duplicar",
   "DUPLICATE_SELECTED_LAYER": "Duplicar capa selecionada",
-  "CREATE_MASK": "Crear mascara",
-  "DELETE_MASK": "Borrar mascara",
-  "TOGGLE_MASK": "Máscara de conmutación",
-  "APPLY_MASK": "Aplicar mascara",
+  "CREATE_MASK": "Crear máscara",
+  "DELETE_MASK": "Borrar máscara",
+  "TOGGLE_MASK": "Alternar máscara",
+  "APPLY_MASK": "Aplicar máscara",
   "TOGGLE_VISIBILITY": "Alternar visibilidad",
-  "MOVE_MEMBER_UP": "Desplazar al miembro hacia arriba",
-  "MOVE_MEMBER_UP_DESCRIPTIVE": "Mover hacia arriba la capa o carpeta seleccionada",
-  "MOVE_MEMBER_DOWN": "Desplazar el miembro hacia abajo",
-  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Mover hacia abajo la capa o carpeta seleccionada",
-  "MERGE_ALL_SELECTED_LAYERS": "Fusionar todas las capas seleccionadas",
-  "MERGE_WITH_ABOVE": "Fusionar la capa seleccionada con la anterior",
-  "MERGE_WITH_ABOVE_DESCRIPTIVE": "Fusionar la capa seleccionada con la superior",
-  "MERGE_WITH_BELOW": "Fusionar la capa seleccionada con la de abajo",
-  "MERGE_WITH_BELOW_DESCRIPTIVE": "Fusionar la capa seleccionada con la inferior",
+  "MOVE_MEMBER_UP": "Mover miembro hacia arriba",
+  "MOVE_MEMBER_UP_DESCRIPTIVE": "Mover la capa o carpeta seleccionada hacia arriba",
+  "MOVE_MEMBER_DOWN": "Desplazar miembro hacia abajo",
+  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Mover la capa o carpeta seleccionada hacia abajo",
+  "MERGE_ALL_SELECTED_LAYERS": "Combinar todas las capas seleccionadas",
+  "MERGE_WITH_ABOVE": "Combinar la capa seleccionada con la de arriba",
+  "MERGE_WITH_ABOVE_DESCRIPTIVE": "Combinar la capa seleccionada con la que esté por encima",
+  "MERGE_WITH_BELOW": "Combinar la capa seleccionada con la de abajo",
+  "MERGE_WITH_BELOW_DESCRIPTIVE": "Combinar la capa seleccionada con la inferior",
   "ADD_REFERENCE_LAYER": "Añadir capa de referencia",
   "DELETE_REFERENCE_LAYER": "Eliminar capa de referencia",
   "TRANSFORM_REFERENCE_LAYER": "Transformar capa de referencia",
-  "TOGGLE_REFERENCE_LAYER_POS": "Conmutar la posición de la capa de referencia",
-  "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Conmutar la capa de referencia entre la superior o la inferior",
+  "TOGGLE_REFERENCE_LAYER_POS": "Alternar la posición de la capa de referencia",
+  "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Configurar el si la capa de referencia se encuentra arriba o abajo de todo",
   "RESET_REFERENCE_LAYER_POS": "Restablecer la posición de la capa de referencia",
-  "CLIP_CANVAS": "Adjuntar Canvas",
-  "FLIP_IMG_VERTICALLY": "Voltear imagen verticalmente",
-  "FLIP_IMG_HORIZONTALLY": "Voltear imagen horizontalmente",
-  "FLIP_LAYERS_VERTICALLY": "Voltear verticalmente las capas seleccionadas",
-  "FLIP_LAYERS_HORIZONTALLY": "Voltear horizontalmente las capas seleccionadas",
-  "ROT_IMG_90": "Girar la imagen 90 grados",
-  "ROT_IMG_180": "Girar la imagen 180 grados",
-  "ROT_IMG_-90": "Girar la imagen -90 grados",
-  "ROT_LAYERS_90": "Girar 90 grados las capas seleccionadas",
-  "ROT_LAYERS_180": "Girar 180 grados las capas seleccionadas",
-  "ROT_LAYERS_-90": "Girar -90 grados las capas seleccionadas",
-  "TOGGLE_VERT_SYMMETRY_AXIS": "Conmutar el eje de simetría vertical",
-  "TOGGLE_HOR_SYMMETRY_AXIS": "Conmutar el eje de simetría horizontal",
-  "RESIZE_DOCUMENT": "Cambiar el tamaño del documento",
-  "RESIZE_CANVAS": "Cambiar el tamaño del canvas",
-  "CENTER_CONTENT": "Contenido central",
+  "CLIP_CANVAS": "Recortar lienzo",
+  "FLIP_IMG_VERTICALLY": "Espejar imagen verticalmente",
+  "FLIP_IMG_HORIZONTALLY": "Espejar imagen horizontalmente",
+  "FLIP_LAYERS_VERTICALLY": "Espejar verticalmente las capas seleccionadas",
+  "FLIP_LAYERS_HORIZONTALLY": "Espejar horizontalmente las capas seleccionadas",
+  "ROT_IMG_90": "Rotar la imagen 90 grados",
+  "ROT_IMG_180": "Rotar la imagen 180 grados",
+  "ROT_IMG_-90": "Rotar la imagen -90 grados",
+  "ROT_LAYERS_90": "Rotar las capas seleccionadas 90 grados",
+  "ROT_LAYERS_180": "Rotar las capas seleccionadas 180 grados",
+  "ROT_LAYERS_-90": "Rotar las capas seleccionadas -90 grados",
+  "TOGGLE_VERT_SYMMETRY_AXIS": "Alternar el eje de simetría vertical",
+  "TOGGLE_HOR_SYMMETRY_AXIS": "Alternar el eje de simetría horizontal",
+  "RESIZE_DOCUMENT": "Reescalar documento",
+  "RESIZE_CANVAS": "Reescalar lienzo",
+  "CENTER_CONTENT": "Centrar contenido",
   "CUT": "Cortar",
   "CUT_DESCRIPTIVE": "Cortar el área o las capas seleccionadas",
   "PASTE": "Pegar",
@@ -167,20 +167,20 @@
   "COPY": "Copiar",
   "COPY_DESCRIPTIVE": "Copiar al portapapeles",
   "COPY_COLOR_HEX": "Copiar color primario (HEX)",
-  "COPY_COLOR_HEX_DESCRIPTIVE": "Copiar color primario como código HEX",
+  "COPY_COLOR_HEX_DESCRIPTIVE": "Copiar color primario como código hexadecimal",
   "COPY_COLOR_RGB": "Copiar color primario (RGB)",
   "COPY_COLOR_RGB_DESCRIPTIVE": "Copiar color primario como código RGB",
   "COPY_COLOR_SECONDARY_HEX": "Copiar color secundario (HEX)",
-  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Copiar color secundario como código HEX",
+  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Copiar color secundario como código hexadecimal",
   "COPY_COLOR_SECONDARY_RGB": "Copiar color secundario (RGB)",
   "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "Copiar color secundario como código RGB",
   "PALETTE_COLORS": "Colores de la paleta",
-  "REPLACE_SECONDARY_BY_PRIMARY": "Sustituir el color secundario por el primario",
+  "REPLACE_SECONDARY_BY_PRIMARY": "Reemplazar el color secundario por el primario",
   "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "Sustituye el color secundario por el primario",
   "REPLACE_PRIMARY_BY_SECONDARY": "Sustituir el color primario por el secundario",
   "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "Sustituir el color primario por el secundario",
   "OPEN_PALETTE_BROWSER": "Abrir el navegador de paletas",
-  "OVERWRITE_PALETTE_CONSENT": "La paleta '{0}' ya existe, ¿desea sobrescribirla?",
+  "OVERWRITE_PALETTE_CONSENT": "La paleta '{0}' ya existe, ¿deseas sobrescribirla?",
   "PALETTE_EXISTS": "La paleta ya existe",
   "REPLACE_PALETTE_CONSENT": "¿Reemplazar la paleta actual por la seleccionada?",
   "REPLACE_PALETTE": "Sustituir la paleta actual",
@@ -194,11 +194,11 @@
   "SELECT_COLOR_8": "Seleccionar color  8",
   "SELECT_COLOR_9": "Seleccionar color 9",
   "SELECT_COLOR_10": "Seleccionar color 10",
-  "SELECT_TOOL": "Seleccionar {0} Herramienta",
-  "SELECT_COLOR_1_DESCRIPTIVE": "Seleccione el primer color de la paleta",
-  "SELECT_COLOR_2_DESCRIPTIVE": "Seleccione el segundo color de la paleta",
-  "SELECT_COLOR_3_DESCRIPTIVE": "Seleccione el tercero color de la paleta",
-  "SELECT_COLOR_4_DESCRIPTIVE": "Seleccione el cuarto color de la paleta",
+  "SELECT_TOOL": "Seleccionar la herramienta de {0}",
+  "SELECT_COLOR_1_DESCRIPTIVE": "Seleccione el primer color en la paleta",
+  "SELECT_COLOR_2_DESCRIPTIVE": "Seleccione el segundo color en la paleta",
+  "SELECT_COLOR_3_DESCRIPTIVE": "Seleccione el tercero color en la paleta",
+  "SELECT_COLOR_4_DESCRIPTIVE": "Seleccione el cuarto color en la paleta",
   "SELECT_COLOR_5_DESCRIPTIVE": "Seleccione el quinto color de la paleta",
   "SELECT_COLOR_6_DESCRIPTIVE": "Seleccione el sexto color de la paleta",
   "SELECT_COLOR_7_DESCRIPTIVE": "Seleccione el séptimo color de la paleta",
@@ -543,5 +543,11 @@
   "SOURCE_UNSET_OR_MISSING": "Fuente ausente/desactivada",
   "SOURCE_NEWER": "Fuente más reciente",
   "SOURCE_UP_TO_DATE": "La fuente está actualizada",
-  "SOURCE_OLDER": "Nube más reciente"
+  "SOURCE_OLDER": "Nube más reciente",
+  "NEWS": "Noticias",
+  "DISABLE_NEWS_PANEL": "No mostrar el panel de noticias en la ventana de bienvenida",
+  "FAILED_FETCH_NEWS": "No se pudieron cargar las noticias",
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE": "Pérdida de documentos tras colapso",
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED": "No todos los documentos pudieron ser recuperados tras un cierre inesperado. Metele ganas a guardar tu trabajo.",
+  "EXAMPLE_FILES": "Archivos de ejemplo"
 }

+ 131 - 1
src/PixiEditor/Data/Localization/Languages/ru.json

@@ -678,5 +678,135 @@
   "REMOVE_CLOSE_POINTS": "Удалить близкие точки",
   "RASTERIZE_SHAPE": "Растрировать фигуру",
   "MODE": "Режим",
-  "Factor": "Степень"
+  "Factor": "Степень",
+  "SEND": "Отправить отчет",
+  "INPUT": "Ввод",
+  "CHANNELS_DOCK_TITLE": "Каналы",
+  "COORDINATE": "Координата",
+  "NOISE": "Шум",
+  "PIXEL_COORDINATE": "Координаты пикселя",
+  "MODIFY_IMAGE_PAIR_NODE": "Редактировать изображение",
+  "WARMING_UP": "Разогреваемся",
+  "RENDERING_FRAME": "Генерация кадра {0}/{1}",
+  "RENDERING_VIDEO": "Рендеринг видео",
+  "GENERATING_SPRITE_SHEET": "Создание атласа спрайтов",
+  "PROBABILITY": "Вероятность",
+  "STARS_EXAMPLE": "Звёзды",
+  "ADD_EMPTY_FRAME": "Создать пустой кадр",
+  "DUPLICATE_FRAME": "Дублировать кадр",
+  "DELETE_FRAME": "Удалить кадр",
+  "NO_PARSER_FOUND": "Не найден парсер файлов для расширения '{0}'",
+  "SELECT_FILE_FORMAT": "Выберите формат файла",
+  "ISLAND_EXAMPLE": "Острова",
+  "SHAPE": "Фигура",
+  "STRUCTURE": "Структура",
+  "NUMBERS": "Числа",
+  "OPERATIONS": "Операции",
+  "GENERATION": "Генерация",
+  "NUMBER": "Число",
+  "ANIMATION": "Анимация",
+  "SAMPLE_IMAGE": "Цвет с изображения",
+  "POSITION": "Позиция",
+  "MATH_ADD": "Сложение",
+  "MATH_SUBTRACT": "Вычитание",
+  "MULTIPLY": "Умножение",
+  "DIVIDE": "Деление",
+  "SIN": "Синус",
+  "COS": "Косинус",
+  "TAN": "Тангенс",
+  "PIXEL_ART_TOOLSET": "Пиксель-арт",
+  "VECTOR_TOOLSET": "Векторная графика",
+  "VECTOR_LAYER": "Векторный слой",
+  "STROKE_COLOR_LABEL": "Контур",
+  "SYNC_WITH_PRIMARY_COLOR_LABEL": "Синхронизировать с основным цветом",
+  "RASTERIZE_ACTIVE_LAYER_DESCRIPTIVE": "Преобразовать/растеризовать выбранный слой в растровый слой (в изображение)",
+  "NEW_ELLIPSE_LAYER_NAME": "Овал",
+  "NEW_RECTANGLE_LAYER_NAME": "Прямоугольник",
+  "NEW_LINE_LAYER_NAME": "Линия",
+  "PAINT_TOOLSET": "Рисование",
+  "HARDNESS_SETTING": "Жесткость",
+  "SPACING_SETTING": "Промежуток",
+  "ANTI_ALIASING_SETTING": "Сглаживание",
+  "TOLERANCE_LABEL": "Допуск",
+  "HIGH_RES_PREVIEW": "Предварительный просмотр в высоком разрешении",
+  "LOW_RES_PREVIEW": "Предварительный просмотр в разрешении документа",
+  "TOGGLE_HIGH_RES_PREVIEW": "Вкл/выкл предварительный просмотр в высоком разрешении",
+  "PATH_TOOL": "Кривая",
+  "PATH_TOOL_TOOLTIP": "Создание векторных кривых ({0}).",
+  "PATH_TOOL_ACTION_DISPLAY": "Клик для добавления новой точки",
+  "PATH_TOOL_ACTION_DISPLAY_CTRL": "Потяните за точку для создания кривой. Кликните точку для выделения.",
+  "PATH_TOOL_ACTION_DISPLAY_SHIFT": "Кликните для создания нового слоя.",
+  "PATH_TOOL_ACTION_DISPLAY_CTRL_SHIFT": "Кликните точку чтобы добавить её в выделение",
+  "PATH_TOOL_ACTION_DISPLAY_ALT": "Потяните за контрольную точку для изменения кривой только с одной стороны.",
+  "DEFAULT_PATH_LAYER_NAME": "Кривая",
+  "DELETE_NODES": "Удалить ноды",
+  "DELETE_NODES_DESCRIPTIVE": "Удалить выбранные ноды",
+  "DELETE_CELS": "Удалить кадры",
+  "DELETE_CELS_DESCRIPTIVE": "Удалить выбранные кадры",
+  "COPY_COLOR_TO_CLIPBOARD": "Скопировать цвет в буфер обмена",
+  "VIEWPORT_ROTATION": "Поворот камеры",
+  "NEXT_TOOL_SET": "Следующий набор инструментов",
+  "PREVIOUS_TOOL_SET": "Предыдущий набор инструментов",
+  "FILL_MODE": "Режим заливки",
+  "USE_LINEAR_SRGB_PROCESSING": "Использовать линейный sRGB при смешивании цветов",
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Включить линейное смешивание цветов в этом документе. После включения цвета будут смешиваться более естественно, но внешний вид изображения может измениться.",
+  "STROKE_CAP": "Конец контура",
+  "STROKE_JOIN": "Стык контура",
+  "COPY_VISIBLE": "Копировать видимое",
+  "COPY_VISIBLE_DESCRIPTIVE": "Копировать видимые пиксели",
+  "CREATE_CEL": "Создать кадр",
+  "CREATE_CEL_DESCRIPTIVE": "Создать новый кадр",
+  "DUPLICATE_CEL": "Дублировать кадр",
+  "DUPLICATE_CEL_DESCRIPTIVE": "Дублировать выбранный кадр",
+  "RENDER_PREVIEW": "Предварительный просмотр",
+  "TOGGLE_PLAY": "Плей/пауза анимации",
+  "OPEN_PREVIEW_WINDOW": "Открыть окно предварительного просмотра",
+  "PREVIEW_TITLE": "Предварительный просмотр",
+  "GREATER_THAN": "Больше",
+  "LESS_THAN": "Меньше",
+  "LESS_THAN_OR_EQUAL": "Меньше или равно",
+  "COMPARE": "Сравнить",
+  "MATH_POWER": "Степень",
+  "LOGARITHM": "Логарифм",
+  "ROOT": "Корень",
+  "COPY_NODES_DESCRIPTIVE": "Копировать выбранные ноды",
+  "PASTE_NODES": "Вставить ноды",
+  "PASTE_NODES_DESCRIPTIVE": "Вставить скопированные ноды",
+  "COPY_CELS": "Копировать кадры",
+  "COPY_CELS_DESCRIPTIVE": "Копировать выбранные кадры",
+  "VALUE": "Значение",
+  "PRESERVE_ALPHA": "Не затрагивать alpha",
+  "BLUR_FILTER_NODE": "Размытие по гауссу",
+  "LENGTH": "Длина",
+  "GREATER_THAN_OR_EQUAL": "Больше или равно",
+  "WEBP_FILE": "Изображения WebP",
+  "COLOR_NODE": "Цвет",
+  "CONVERT_TO_CURVE": "Преобразовать в кривую",
+  "CONVERT_TO_CURVE_DESCRIPTIVE": "Преобразовать выбранный векторный слой в кривую",
+  "FONT_FILES": "Файлы шрифтов",
+  "UNIT_PT": "тч",
+  "FONT_LABEL": "Шрифт",
+  "FONT_SIZE_LABEL": "Размер",
+  "SPACING_LABEL": "Интервал",
+  "TEXT_TOOL": "Текст",
+  "MISSING_FONT": "Отсутствует шрифт",
+  "TEXT_LAYER_NAME": "Текст",
+  "TEXT_TOOL_TOOLTIP": "Добавить текст ({0}).",
+  "BOLD_TOOLTIP": "Жирный",
+  "ITALIC_TOOLTIP": "Курсив",
+  "STRING_OPEN_IN_FOLDER": "Показать в проводнике",
+  "DISCO_BALL_EXAMPLE": "Диско-шар",
+  "COLOR_SPACE": "Пространство цветов",
+  "PHOTO_EXAMPLES": "Фотография",
+  "MASK_EXAMPLE": "Маска",
+  "SHADOW_NODE": "Тень",
+  "INPUT_MATRIX": "Входная матрица",
+  "OUTPUT_MATRIX": "Выходная матрица",
+  "CENTER": "Центр",
+  "CONTENT_OFFSET": "Отступ содержимого",
+  "CANVAS_POSITION": "Позиция холста",
+  "ROTATION_NODE": "Поворот",
+  "SCALE_NODE": "Масштабирование",
+  "ROTATE_NODE": "Поворот",
+  "HUE_VALUE": "Тон"
 }

+ 2 - 5
src/PixiEditor/Data/Localization/Languages/tr.json

@@ -1123,8 +1123,5 @@
   "TRANSFORMED_POSITION": "Dönüştürülmüş Konum",
   "ACCOUNT_PROVIDER_NOT_AVAILABLE": "PixiEditor'ın bu yapısı hesapları desteklemiyor. Hesabınızı yönetmek için pixieditor.net'teki resmi yapıyı kullanın.",
   "STEAM_OFFLINE": "Hesap doğrulanamıyor. Steam çevrimdışı. Steam istemcisinin çalıştığından ve giriş yaptığınızdan emin olun.",
-  "ERROR_GPU_RESOURCES_CREATION": "Kaynaklar oluşturulamadı: GPU sürücülerinizi güncellemeyi deneyin veya ayarlarda farklı bir oluşturma API'si ayarlamayı deneyin. \nHata: '{0}'",
-  "ERROR_SAVING_PREFERENCES_DESC": "Tercihler şu hatayla kaydedilemedi: '{0}'. Lütfen PixiEditor veri klasörüne yazma izniniz olup olmadığını kontrol edin.",
-  "ERROR_SAVING_PREFERENCES": "Tercihler kaydedilemedi",
-  "PREFERRED_RENDERER": "Tercih Edilen Oluşturma Api"
-}
+  "OPEN_COMMAND_DEBUG_WINDOW": "Komut hata ayıklama penceresini aç"
+}

+ 5 - 5
src/PixiEditor/Data/Localization/LocalizationData.json

@@ -27,7 +27,7 @@
       "code": "es",
       "localeFileName": "es.json",
       "iconFileName": "es.png",
-      "lastUpdated": "2023-05-17 16:46:19"
+      "lastUpdated": "2025-09-07 02:23:00"
     },
     {
       "name": "中文",
@@ -42,7 +42,7 @@
       "code": "ru",
       "localeFileName": "ru.json",
       "iconFileName": "ru.png",
-      "lastUpdated": "2025-06-30 11:13:26"
+      "lastUpdated": "2025-07-28 08:51:44"
     },
     {
       "name": "Українська",
@@ -57,7 +57,7 @@
       "localeFileName": "ar.json",
       "iconFileName": "ar.png",
       "rightToLeft": true,
-      "lastUpdated": "2025-06-04 18:20:46"
+      "lastUpdated": "2025-09-06 22:22:52"
     },
     {
       "name": "Čeština",
@@ -85,7 +85,7 @@
       "code": "tr",
       "localeFileName": "tr.json",
       "iconFileName": "tr.png",
-      "lastUpdated": "2025-08-06 09:23:12"
+      "lastUpdated": "2025-08-07 18:24:00"
     }
   ]
-}
+}

+ 1 - 0
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -10,4 +10,5 @@ public static class ClipboardDataFormats
     public const string CelIdList = "PixiEditor.CelIdList";
     public const string PixiVectorData = "PixiEditor.VectorData";
     public const string UriList = "text/uri-list";
+    public const string HadSelectionFormat = "PixiEditor.HadSelection";
 }

+ 3 - 1
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -83,7 +83,9 @@ internal class SupportedFilesHelper
         if (file is null)
             return null;
 
-        string extension = Path.GetExtension(file.Path.LocalPath);
+        string? localPath = file.TryGetLocalPath();
+
+        string extension = Path.GetExtension(localPath ?? file.Name);
         return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
 

+ 2 - 0
src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs

@@ -10,6 +10,8 @@ internal partial class Command
         public Key Transient { get; set; }
         public bool TransientImmediate { get; set; } = false;
 
+        public string? CommonToolType { get; set; }
+
         public ToolAttribute() : base(null, null, null)
         {
         }

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

@@ -68,6 +68,7 @@ internal static class ClipboardController
 
         Surface surfaceToCopy = null;
         RectD copyArea = RectD.Empty;
+        bool hadSelection = false;
 
         if (!document.SelectionPathBindable.IsEmpty)
         {
@@ -83,6 +84,7 @@ internal static class ClipboardController
 
             surfaceToCopy = surface.AsT2.Item1;
             copyArea = (RectD)surface.AsT2.Item2;
+            hadSelection = true;
         }
         else if (document.TransformViewModel.TransformActive || lastTransform != null)
         {
@@ -122,6 +124,11 @@ internal static class ClipboardController
             data.SetVecD(ClipboardDataFormats.PositionFormat, copyArea.Pos);
         }
 
+        if (hadSelection)
+        {
+            data.Set(ClipboardDataFormats.HadSelectionFormat, true);
+        }
+
         string[] layerIds = document.GetSelectedMembers().Select(x => x.ToString()).ToArray();
         string layerIdsString = string.Join(";", layerIds);
 
@@ -235,14 +242,18 @@ internal static class ClipboardController
         Guid[] layerIds = await GetLayerIds(data);
 
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
+        bool hadSelection = data.Any(x => x.Contains(ClipboardDataFormats.HadSelectionFormat));
 
         IDocument? targetDoc = manager.Documents.FirstOrDefault(x => x.Id == sourceDocument);
 
-        if (targetDoc != null && pasteAsNew && layerIds is { Length: > 0 } &&
+        if (targetDoc != null && !hadSelection && pasteAsNew && layerIds is { Length: > 0 } &&
             (!hasPos || await AllMatchesPos(layerIds, data, targetDoc)))
         {
             foreach (var layerId in layerIds)
             {
+                if (targetDoc.StructureHelper.Find(layerId) == null)
+                    continue;
+
                 if (sourceDocument == document.Id)
                 {
                     document.Operations.DuplicateMember(layerId);
@@ -495,7 +506,7 @@ internal static class ClipboardController
         {
             text = await importObj.GetDataAsync(DataFormats.Text) as string;
         }
-        catch(InvalidCastException ex) // bug on x11
+        catch (InvalidCastException ex) // bug on x11
         {
         }
 
@@ -848,7 +859,7 @@ internal static class ClipboardController
         data.Set(ClipboardDataFormats.DocumentFormat, Encoding.UTF8.GetBytes(docId.ToString()));
 
         byte[] idsBytes = Encoding.UTF8.GetBytes(string.Join(";", ids.Select(x => x.ToString())));
-        
+
         data.Set(format, idsBytes);
 
         await Clipboard.SetDataObjectAsync(data);

+ 2 - 1
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -8,6 +8,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using PixiEditor.UI.Common.Localization;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.DocumentModels;
 #nullable enable
@@ -34,7 +35,7 @@ internal class DocumentStructureHelper
                     count++;
             }
 
-            return true;
+            return Traverse.Further;
         });
         return $"{name} {count}";
     }

+ 5 - 3
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -21,6 +21,7 @@ using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -234,7 +235,8 @@ internal class DocumentOperationsModule : IDocumentOperations
         {
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateFolder_Action(guidValue, newGuid, null),
-                new SetSelectedMember_PassthroughAction(newGuid));
+                new SetSelectedMember_PassthroughAction(newGuid),
+                new CreateAnimationDataFromFolder_Action(newGuid));
         }
 
         return newGuid;
@@ -617,10 +619,10 @@ internal class DocumentOperationsModule : IDocumentOperations
             if (!members.Contains(traversedNode.Id))
             {
                 parent = traversedNode;
-                return false;
+                return Traverse.Exit;
             }
 
-            return true;
+            return Traverse.Further;
         });
 
         if (parent is null)

+ 16 - 15
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -45,10 +46,10 @@ internal class DocumentStructureModule
             if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
             {
                 parent = traversedNode;
-                return false;
+                return Traverse.Exit;
             }
 
-            return true;
+            return Traverse.Further;
         });
 
         if (parent is null)
@@ -62,10 +63,10 @@ internal class DocumentStructureModule
                 if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
                 {
                     parent = traversedNode;
-                    return false;
+                    return Traverse.Exit;
                 }
 
-                return true;
+                return Traverse.Further;
             });
         }
 
@@ -110,7 +111,7 @@ internal class DocumentStructureModule
         {
             if (node is IStructureMemberHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
                 parents.Add(parent);
-            return true;
+            return Traverse.Further;
         });
 
         return parents;
@@ -187,7 +188,7 @@ internal class DocumentStructureModule
                 toFill.Add(strNode);
             }
 
-            return true;
+            return Traverse.Further;
         });
     }
 
@@ -197,10 +198,10 @@ internal class DocumentStructureModule
         startNode.TraverseForwards(node =>
         {
             if (node == startNode)
-                return true;
+                return Traverse.Further;
 
             result = node;
-            return false;
+            return Traverse.Exit;
         });
 
         return result;
@@ -218,13 +219,13 @@ internal class DocumentStructureModule
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             {
                 if (node is IFolderHandler && !includeFolders)
-                    return true;
+                    return Traverse.Further;
 
                 result = structureMemberNode;
-                return false;
+                return Traverse.Exit;
             }
 
-            return true;
+            return Traverse.Further;
         });
 
         return result;
@@ -242,13 +243,13 @@ internal class DocumentStructureModule
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             {
                 if (node is IFolderHandler && !includeFolders)
-                    return true;
+                    return Traverse.Further;
 
                 result = structureMemberNode;
-                return false;
+                return Traverse.Exit;
             }
 
-            return true;
+            return Traverse.Further;
         });
 
         return result;
@@ -268,7 +269,7 @@ internal class DocumentStructureModule
             if (node is IStructureMemberHandler structureMemberNode)
                 children.Add(structureMemberNode);
 
-            return true;
+            return Traverse.Further;
         });
 
         return children;

+ 7 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -14,6 +14,7 @@ using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.Vectors;
+using PixiEditor.Models.DocumentModels.Public;
 using PixiEditor.ViewModels.Document.TransformOverlays;
 using PixiEditor.Views.Overlays.TransformOverlay;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
@@ -314,7 +315,8 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         if (highlight)
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
+            HighlightSnapAxis(snapXAxis, snapYAxis,
+                string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
 
         if (AlignToPixels)
@@ -344,7 +346,8 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
 
         if (highlight)
         {
-            HighlightSnapAxis(snapXAxis, snapYAxis, string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
+            HighlightSnapAxis(snapXAxis, snapYAxis,
+                string.IsNullOrEmpty(snapXAxis) && string.IsNullOrEmpty(snapYAxis) ? null : snapped);
         }
 
         if (snapped != VecI.Zero)
@@ -422,9 +425,10 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
                 if (SelectLayerOnTap)
                 {
                     var layersUnderCursor = QueryLayers<ILayerHandler>(argsPositionOnCanvas);
-                    var firstValidLayer = layersUnderCursor.FirstOrDefault(x => CanSelectLayer(x));
+                    var firstValidLayer = layersUnderCursor.FirstOrDefault(x => CanSelectLayer(x) && x.Id != memberId);
                     if (firstValidLayer != null)
                     {
+                        document.Operations.SetSelectedMember(Guid.Empty); // fixes reselecting
                         document.Operations.SetSelectedMember(firstValidLayer.Id);
                     }
                 }

+ 12 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -84,11 +84,18 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
             toolbar.FillBrush = textData.FillPaintable.ToBrush();
             toolbar.StrokeBrush = textData.Stroke.ToBrush();
             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;
+            try
+            {
+                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;
+            }
+            catch (InvalidOperationException) // Native font likely disposed
+            {
+                
+            }
 
             onPath = textData.Path;
             lastText = textData.Text;

+ 9 - 0
src/PixiEditor/Models/EnumTranslations.cs

@@ -11,6 +11,9 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Models.Tools;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 [assembly: LocalizeEnum<StrokeCap>(StrokeCap.Butt, "BUTT_STROKE_CAP")]
@@ -96,6 +99,9 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 [assembly: LocalizeEnum<OutlineType>(OutlineType.Gaussian, "GAUSSIAN_OUTLINE_TYPE")]
 [assembly: LocalizeEnum<OutlineType>(OutlineType.PixelPerfect, "PIXEL_PERFECT_OUTLINE_TYPE")]
 
+[assembly: LocalizeEnum<PosterizationMode>(PosterizationMode.Rgb, "RGB_POSTERIZATION_MODE")]
+[assembly: LocalizeEnum<PosterizationMode>(PosterizationMode.Luminance, "LUMINANCE_POSTERIZATION_MODE")]
+
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F1, "F1_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2, "F2_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2MinusF1, "F2_MINUS_F1_VORONOI_FEATURE")]
@@ -118,3 +124,6 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Exclusion, "EXCLUSION_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Erase, "ERASE_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.LinearDodge, "LINEAR_DODGE_BLEND_MODE")]
+
+[assembly: LocalizeEnum<PaintBrushShape>(PaintBrushShape.Circle, "PAINT_BRUSH_SHAPE_CIRCLE")]
+[assembly: LocalizeEnum<PaintBrushShape>(PaintBrushShape.Square, "PAINT_BRUSH_SHAPE_SQUARE")]

+ 11 - 7
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.ComponentModel;
+using Avalonia;
 using Avalonia.Media;
 using ChunkyImageLib;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -8,6 +9,7 @@ using Drawie.Backend.Core;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.Models.Handlers;
 
@@ -22,15 +24,17 @@ public interface INodeHandler : INotifyPropertyChanged, IDisposable
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public PreviewPainter? ResultPainter { get; set; }
     public VecD PositionBindable { get; set; }
+    public Rect UiSize { get; set; }
     public bool IsNodeSelected { get; set; }
     public string Icon { get; }
-    public void TraverseBackwards(Func<INodeHandler, bool> func);
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func);
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func);
+    public void TraverseBackwards(Func<INodeHandler, Traverse> func);
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, Traverse> func);
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, Traverse> func);
+    public HashSet<NodeFrameViewModelBase> Frames { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap { get; }
 }

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

@@ -19,6 +19,7 @@ internal interface IToolsHandler : IHandler
     public ICollection<IToolSetHandler> AllToolSets { get; }
     public RightClickMode RightClickMode { get; set; }
     public bool EnableSharedToolbar { get; set; }
+    public bool SelectionTintingEnabled { get; set; }
     public event EventHandler<SelectedToolEventArgs> SelectedToolChanged;
     public void SetupTools(IServiceProvider services, ToolSetsConfig toolSetConfig);
     public void SetupToolsTooltipShortcuts();

+ 4 - 3
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -8,6 +8,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
 
 namespace PixiEditor.Models.Rendering;
@@ -288,16 +289,16 @@ internal class MemberPreviewUpdater
         nodeVm.TraverseForwards(next =>
         {
             if (next is not INodeHandler nextVm)
-                return true;
+                return Traverse.Further;
 
             var nextNode = allNodes.FirstOrDefault(x => x.Id == next.Id);
 
             if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
-                return true;
+                return Traverse.Further;
 
             RequestRepaintNode(nextNode, nextVm);
             actualRepaintedNodes.Add(next.Id);
-            return true;
+            return Traverse.Further;
         });
     }
 

+ 4 - 4
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -173,7 +173,7 @@ public class PreviewPainter : IDisposable
             VecI bounds = painterInstance.RequestRenderBounds?.Invoke() ?? VecI.Zero;
 
             ChunkResolution finalResolution = FindResolution(bounds);
-            SamplingOptions samplingOptions = FindSamplingOptions(bounds);
+            SamplingOptions samplingOptions = FindSamplingOptions(matrix);
 
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
             renderTexture.DrawingSurface.Canvas.Scale((float)finalResolution.InvertedMultiplier());
@@ -257,10 +257,10 @@ public class PreviewPainter : IDisposable
         return ChunkResolution.Full;
     }
 
-    private SamplingOptions FindSamplingOptions(VecI bounds)
+    private SamplingOptions FindSamplingOptions(Matrix3X3? matrix)
     {
-        var density = DocumentSize.X / (double)bounds.X;
-        return density > 1
+        Matrix3X3 mtx = matrix ?? Matrix3X3.Identity;
+        return mtx.ScaleX < 1f || mtx.ScaleY < 1f
             ? SamplingOptions.Bilinear
             : SamplingOptions.Default;
     }

+ 144 - 0
src/PixiEditor/Models/Structures/ObservableHashSet.cs

@@ -0,0 +1,144 @@
+using System.Collections;
+using System.Collections.Immutable;
+using System.Collections.Specialized;
+using System.Runtime.Serialization;
+
+namespace PixiEditor.Models.Structures;
+
+public class ObservableHashSet<T> : ISet<T>, IReadOnlySet<T>, IDeserializationCallback, ISerializable, INotifyCollectionChanged
+{
+    private readonly HashSet<T> setImplementation;
+
+    public ObservableHashSet()
+    {
+        setImplementation = new HashSet<T>();
+    }
+
+    public ObservableHashSet(IEnumerable<T> collection)
+    {
+        setImplementation = new HashSet<T>(collection);
+    }
+    
+    public bool Add(T item)
+    {
+        var isAdded = setImplementation.Add(item);
+
+        if (isAdded)
+        {
+            CallCollectionChanged(NotifyCollectionChangedAction.Add, item);
+        }
+        
+        return isAdded;
+    }
+
+    void ICollection<T>.Add(T item) => Add(item);
+
+    public void Clear()
+    {
+        setImplementation.Clear();
+        CallCollectionChanged(NotifyCollectionChangedAction.Reset, Array.Empty<T>(), setImplementation.ToList());
+    }
+
+    public bool Remove(T item)
+    {
+        var isRemoved = setImplementation.Remove(item);
+
+        if (isRemoved)
+        {
+            CallCollectionChanged(NotifyCollectionChangedAction.Remove, item);
+        }
+        
+        return isRemoved;
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void ExceptWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void IntersectWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void SymmetricExceptWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    public void UnionWith(IEnumerable<T> other)
+    {
+        var allOther = other.ToImmutableHashSet();
+        var addedOnly = allOther.Except(setImplementation);
+        
+        setImplementation.UnionWith(allOther);
+        CallCollectionChanged(
+            NotifyCollectionChangedAction.Reset, 
+            addedOnly.ToList());
+    }
+
+    public void ReplaceBy(IEnumerable<T> other)
+    {
+        var otherOriginal = other.ToHashSet();
+        var original = setImplementation.ToImmutableHashSet();
+        var removed = original.Except(otherOriginal);
+
+        setImplementation.Clear();
+        setImplementation.UnionWith(otherOriginal);
+        CallCollectionChanged(NotifyCollectionChangedAction.Replace, otherOriginal.ToList(), removed.ToList());
+    }
+
+    public bool IsProperSubsetOf(IEnumerable<T> other) => setImplementation.IsProperSubsetOf(other);
+
+    public bool IsProperSupersetOf(IEnumerable<T> other) => setImplementation.IsProperSupersetOf(other);
+
+    public bool IsSubsetOf(IEnumerable<T> other) => setImplementation.IsSubsetOf(other);
+
+    public bool IsSupersetOf(IEnumerable<T> other) => setImplementation.IsSupersetOf(other);
+
+    public bool Overlaps(IEnumerable<T> other) => setImplementation.Overlaps(other);
+
+    public bool SetEquals(IEnumerable<T> other) => setImplementation.SetEquals(other);
+
+    public bool Contains(T item) => setImplementation.Contains(item);
+
+    public void CopyTo(T[] array, int arrayIndex) => setImplementation.CopyTo(array, arrayIndex);
+
+    public IEnumerator<T> GetEnumerator() => setImplementation.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)setImplementation).GetEnumerator();
+
+    public int Count => setImplementation.Count;
+
+    bool ICollection<T>.IsReadOnly => ((ISet<T>)setImplementation).IsReadOnly;
+
+    void IDeserializationCallback.OnDeserialization(object? sender) => setImplementation.OnDeserialization(sender);
+
+    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) => setImplementation.GetObjectData(info, context);
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action));
+    
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, IList added, IList removed) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, added, removed));
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, IList changed) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, changed));
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, T item) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, item));
+
+    public event NotifyCollectionChangedEventHandler? CollectionChanged;
+}

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

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

+ 4 - 7
src/PixiEditor/Styles/Templates/NodeFrameView.axaml

@@ -7,13 +7,10 @@
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate>
-                    <Grid Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}">
-                        <Rectangle Fill="{TemplateBinding Background}"
-                                   Stroke="{TemplateBinding BorderBrush}"
-                                   StrokeThickness="2" RadiusX="10" RadiusY="10"
-                                   Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
-                                   Height="{Binding Size.Y, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}" />
-                    </Grid>
+                    <Path Data="{Binding Geometry, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
+                          Fill="{TemplateBinding Background}"
+                          Stroke="{TemplateBinding BorderBrush}"
+                          StrokeThickness="2" />
                 </ControlTemplate>
             </Setter.Value>
         </Setter>

+ 4 - 10
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -59,7 +59,8 @@
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                     SocketDropCommand="{Binding SocketDropCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                    ResultPreview="{Binding ResultPainter}" />
+                                    ResultPreview="{Binding ResultPainter}"
+                                    Bounds="{Binding UiSize, Mode=OneWayToSource}" />
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                         <ItemsControl.ItemContainerTheme>
@@ -109,9 +110,8 @@
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                                 <nodes:NodeFrameView
-                                    TopLeft="{Binding TopLeft}"
-                                    BottomRight="{Binding BottomRight}"
-                                    Size="{Binding Size}">
+                                    Geometry="{Binding Geometry}"
+                                    ClipToBounds="False">
                                     <nodes:NodeFrameView.Background>
                                         <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
                                             <Binding Path="InternalName"
@@ -131,12 +131,6 @@
                                 </nodes:NodeFrameView>
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
-                        <ItemsControl.ItemContainerTheme>
-                            <ControlTheme TargetType="ContentPresenter">
-                                <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
-                                <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
-                            </ControlTheme>
-                        </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
                 </Grid>
             </ControlTemplate>

+ 9 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -299,6 +299,12 @@ internal partial class DocumentViewModel
         float centerY = (float)rectangleData.Center.Y;
         float halfWidth = (float)rectangleData.Size.X / 2f;
         float halfHeight = (float)rectangleData.Size.Y / 2f;
+        float minHalf = Math.Min(halfWidth, halfHeight);
+        float clampedCorner = Math.Clamp((float)rectangleData.CornerRadius, 0f, 1f);
+        float radius = minHalf * clampedCorner;
+        float radiusX = Math.Min(radius, halfWidth);
+        float radiusY = Math.Min(radius, halfHeight);
+
 
         rect.X.Unit = SvgNumericUnit.FromUserUnits(centerX - halfWidth);
         rect.Y.Unit = SvgNumericUnit.FromUserUnits(centerY - halfHeight);
@@ -306,6 +312,9 @@ internal partial class DocumentViewModel
         rect.Width.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.X);
         rect.Height.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.Y);
 
+        rect.Rx.Unit = SvgNumericUnit.FromUserUnits(radiusX);
+        rect.Ry.Unit = SvgNumericUnit.FromUserUnits(radiusY);
+        
         return rect;
     }
 

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

@@ -392,6 +392,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             factory.ResourceLocator = null;
         }
 
+        viewModel.NodeGraph.FinalizeCreation();
+
         return viewModel;
 
 
@@ -950,7 +952,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (transformed.X < 0 || transformed.Y < 0 || transformed.X >= bitmap.Size.X || transformed.Y >= bitmap.Size.Y)
             return null;
 
-        return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
+        return bitmap.GetSrgbPixel(new VecI((int)transformed.X, (int)transformed.Y));
     }
 
     public void SuppressAllOverlayEvents(string suppressor)

+ 83 - 0
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -19,6 +19,8 @@ namespace PixiEditor.ViewModels.Document;
 
 internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposable
 {
+    private bool isFullyCreated;
+
     public DocumentViewModel DocumentViewModel { get; }
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
@@ -109,6 +111,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
 
         Connections.Add(connection);
 
+        UpdatesFramesPartOf(connection.InputNode);
+        UpdatesFramesPartOf(connection.OutputNode);
+
         StructureTree.Update(this);
     }
 
@@ -121,6 +126,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
             connection.InputProperty.ConnectedOutput = null;
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             Connections.Remove(connection);
+
+            UpdatesFramesPartOf(connection.InputNode);
+            UpdatesFramesPartOf(connection.OutputNode);
         }
 
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
@@ -136,6 +144,81 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         StructureTree.Update(this);
     }
 
+    public void UpdatesFramesPartOf(INodeHandler node)
+    {
+        if (!isFullyCreated)
+            return;
+
+        var lastKnownFramesPartOf = node.Frames.OfType<NodeZoneViewModel>().ToHashSet();
+        var startLookup = Frames.OfType<NodeZoneViewModel>().ToDictionary(x => x.Start);
+        var currentlyPartOf = new HashSet<NodeZoneViewModel>();
+
+        node.TraverseBackwards(x =>
+        {
+            if (x is IPairNodeEndViewModel)
+                return Traverse.NoFurther;
+
+            if (x is not IPairNodeStartViewModel)
+                return Traverse.Further;
+
+            if (startLookup != null && startLookup.TryGetValue(x, out var zone))
+            {
+                currentlyPartOf.Add(zone);
+            }
+
+            return Traverse.Further;
+        });
+
+        foreach (var frame in currentlyPartOf)
+        {
+            frame.Nodes.Add(node);
+            node.Frames.Add(frame);
+        }
+
+        lastKnownFramesPartOf.ExceptWith(currentlyPartOf);
+        foreach (var removedFrom in lastKnownFramesPartOf)
+        {
+            removedFrom.Nodes.Remove(node);
+            node.Frames.Remove(removedFrom);
+        }
+    }
+
+    public void FinalizeCreation()
+    {
+        if (isFullyCreated)
+            return;
+
+        isFullyCreated = true;
+
+        foreach (var nodeZoneViewModel in Frames.OfType<NodeZoneViewModel>())
+        {
+            UpdateNodesPartOf(nodeZoneViewModel);
+        }
+    }
+
+    private static void UpdateNodesPartOf(NodeZoneViewModel zone)
+    {
+        var currentlyPartOf = new HashSet<INodeHandler>([zone.Start, zone.End]);
+
+        foreach (var node in zone.Start
+                     .Outputs
+                     .SelectMany(x => x.ConnectedInputs)
+                     .Select(x => x.Node))
+        {
+            node.TraverseForwards((x) =>
+            {
+                if (x is IPairNodeEndViewModel)
+                    return Traverse.NoFurther;
+
+                currentlyPartOf.Add(x);
+
+                return Traverse.Further;
+            });
+        }
+
+        zone.Nodes.ReplaceBy(currentlyPartOf);
+    }
+
     public void RemoveConnections(Guid nodeId)
     {
         var connections = Connections

+ 11 - 0
src/PixiEditor/ViewModels/Document/Nodes/Effects/PosterizationNodeViewModel.cs

@@ -0,0 +1,11 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Effects;
+
+[NodeViewModel("POSTERIZATION_NODE", "EFFECTS", PixiPerfectIcons.Swatches)]
+internal class PosterizationNodeViewModel : NodeViewModel<PosterizationNode>
+{
+    
+}
+

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs

@@ -5,4 +5,4 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 [NodeViewModel("MODIFY_IMAGE_LEFT_NODE", "IMAGE", PixiPerfectIcons.PutImage)]
-internal class ModifyImageLeftNodeViewModel : NodeViewModel<ModifyImageLeftNode>;
+internal class ModifyImageLeftNodeViewModel : NodeViewModel<ModifyImageLeftNode>, IPairNodeStartViewModel;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ModifyImageRightNodeViewModel.cs

@@ -4,4 +4,4 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 [NodeViewModel("MODIFY_IMAGE_RIGHT_NODE", "IMAGE", null)]
-internal class ModifyImageRightNodeViewModel : NodeViewModel<ModifyImageRightNode>;
+internal class ModifyImageRightNodeViewModel : NodeViewModel<ModifyImageRightNode>, IPairNodeEndViewModel;

+ 2 - 2
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -60,10 +60,10 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
                 if (node is IFolderHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
                 {
                     visible = parent.IsVisibleBindable;
-                    return visible;
+                    return visible ? Traverse.Further : Traverse.Exit;
                 }
 
-                return true;
+                return Traverse.Further;
             });
 
             return visible;

+ 2 - 1
src/PixiEditor/ViewModels/Document/StructureTree.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document;
 
@@ -60,7 +61,7 @@ internal class StructureTree
 
             _memberMap.TryAdd(node, lastRoot);
 
-            return true;
+            return Traverse.Further;
         });
 
         List<IStructureMemberHandler> toRemove = new();

+ 6 - 0
src/PixiEditor/ViewModels/Nodes/IPairNodeEndViewModel.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public interface IPairNodeEndViewModel
+{
+    
+}

+ 6 - 0
src/PixiEditor/ViewModels/Nodes/IPairNodeStartViewModel.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public interface IPairNodeStartViewModel
+{
+    
+}

+ 2 - 28
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModel.cs

@@ -1,9 +1,4 @@
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using CommunityToolkit.Mvvm.ComponentModel;
-using PixiEditor.Models.Handlers;
-using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 
 namespace PixiEditor.ViewModels.Nodes;
 
@@ -16,27 +11,6 @@ internal sealed class NodeFrameViewModel : NodeFrameViewModelBase
 
     protected override void CalculateBounds()
     {
-        
-        // TODO: Use the GetBounds like in NodeZoneViewModel
-        if (Nodes.Count == 0)
-        {
-            if (TopLeft == BottomRight)
-            {
-                BottomRight = TopLeft + new VecD(100, 100);
-            }
-            
-            return;
-        }
-        
-        var minX = Nodes.Min(n => n.PositionBindable.X) - 30;
-        var minY = Nodes.Min(n => n.PositionBindable.Y) - 45;
-        
-        var maxX = Nodes.Max(n => n.PositionBindable.X) + 130;
-        var maxY = Nodes.Max(n => n.PositionBindable.Y) + 130;
-
-        TopLeft = new VecD(minX, minY);
-        BottomRight = new VecD(maxX, maxY);
-
-        Size = BottomRight - TopLeft;
+        throw new NotImplementedException();
     }
 }

+ 23 - 21
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModelBase.cs

@@ -1,20 +1,23 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
+using Avalonia.Media;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
+using PixiEditor.Models.Structures;
 
 namespace PixiEditor.ViewModels.Nodes;
 
 public abstract class NodeFrameViewModelBase : ObservableObject
 {
     private Guid id;
+    private StreamGeometry geometry;
     private VecD topLeft;
     private VecD bottomRight;
     private VecD size;
     
-    public ObservableCollection<INodeHandler> Nodes { get; }
+    public ObservableHashSet<INodeHandler> Nodes { get; }
 
     public string InternalName { get; init; }
     
@@ -24,28 +27,16 @@ public abstract class NodeFrameViewModelBase : ObservableObject
         set => SetProperty(ref id, value);
     }
     
-    public VecD TopLeft
+    public StreamGeometry Geometry
     {
-        get => topLeft;
-        set => SetProperty(ref topLeft, value);
-    }
-
-    public VecD BottomRight
-    {
-        get => bottomRight;
-        set => SetProperty(ref bottomRight, value);
-    }
-
-    public VecD Size
-    {
-        get => size;
-        set => SetProperty(ref size, value);
+        get => geometry;
+        set => SetProperty(ref geometry, value);
     }
 
     public NodeFrameViewModelBase(Guid id, IEnumerable<INodeHandler> nodes)
     {
         Id = id;
-        Nodes = new ObservableCollection<INodeHandler>(nodes);
+        Nodes = new(nodes);
 
         Nodes.CollectionChanged += OnCollectionChanged;
         AddHandlers(Nodes);
@@ -54,13 +45,22 @@ public abstract class NodeFrameViewModelBase : ObservableObject
     private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
         var action = e.Action;
-        if (action != NotifyCollectionChangedAction.Add && action != NotifyCollectionChangedAction.Remove && action != NotifyCollectionChangedAction.Replace && action != NotifyCollectionChangedAction.Reset)
+        if (action is
+            not NotifyCollectionChangedAction.Add and
+            not NotifyCollectionChangedAction.Remove and
+            not NotifyCollectionChangedAction.Replace and
+            not NotifyCollectionChangedAction.Reset)
         {
             return;
         }
+
+        CalculateBounds();
+        
+        if (e.NewItems != null)
+            AddHandlers(e.NewItems.Cast<INodeHandler>());
         
-        AddHandlers((IEnumerable<NodeViewModel>)e.NewItems);
-        RemoveHandlers((IEnumerable<NodeViewModel>)e.OldItems);
+        if (e.OldItems != null)
+            RemoveHandlers(e.OldItems.Cast<INodeHandler>());
     }
 
     private void AddHandlers(IEnumerable<INodeHandler> nodes)
@@ -81,7 +81,9 @@ public abstract class NodeFrameViewModelBase : ObservableObject
 
     private void NodePropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
-        if (e.PropertyName != nameof(INodeHandler.PositionBindable))
+        if (e.PropertyName is
+            not nameof(INodeHandler.PositionBindable) and
+            not nameof(INodeHandler.UiSize))
         {
             return;
         }

+ 99 - 24
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -1,6 +1,7 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using Avalonia;
 using Avalonia.Media;
@@ -25,6 +26,7 @@ namespace PixiEditor.ViewModels.Nodes;
 internal abstract class NodeViewModel : ObservableObject, INodeHandler
 {
     private LocalizedString displayName;
+    private Rect size;
     private IBrush? categoryBrush;
     private string? nodeNameBindable;
     private VecD position;
@@ -111,6 +113,12 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
+    public Rect UiSize
+    {
+        get => size;
+        set => SetProperty(ref size, value);
+    }
+
     public ObservableRangeCollection<INodePropertyHandler> Inputs
     {
         get => inputs;
@@ -218,7 +226,11 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
     public string Icon => icon ??= GetType().GetCustomAttribute<NodeViewModelAttribute>().Icon;
 
-    public void TraverseBackwards(Func<INodeHandler, bool> func)
+    [DoesNotReturn]
+    private void ThrowInvalidTraverseResult(Traverse traverse) =>
+        throw new IndexOutOfRangeException($"Invalid Traverse Option '{traverse}'");
+
+    public void TraverseBackwards(Func<INodeHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
@@ -233,9 +245,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
             }
 
-            if (!func(node))
+            var result = func(node);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var inputProperty in node.Inputs)
@@ -248,7 +269,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func)
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
@@ -263,9 +284,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
             }
 
-            if (!func(node.Item1, node.Item2))
+            var result = func(node.Item1, node.Item2);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var inputProperty in node.Item1.Inputs)
@@ -278,7 +308,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
@@ -292,10 +322,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-
-            if (!func(node.Item1, node.Item2, node.Item3))
+            var result = func(node.Item1, node.Item2, node.Item3);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var inputProperty in node.Item1.Inputs)
@@ -308,7 +346,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public void TraverseForwards(Func<INodeHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
@@ -322,10 +360,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-
-            if (!func(node))
+            
+            var result = func(node);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var outputProperty in node.Outputs)
@@ -338,7 +385,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
@@ -352,10 +399,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
                 continue;
             }
-
-            if (!func(node.Item1, node.Item2))
+            
+            var result = func(node.Item1, node.Item2);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var outputProperty in node.Item1.Outputs)
@@ -368,7 +424,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
@@ -383,9 +439,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
             }
 
-            if (!func(node.Item1, node.Item2, node.Item3))
+            var result = func(node.Item1, node.Item2, node.Item3);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var outputProperty in node.Item1.Outputs)
@@ -399,7 +464,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     }
 
     public void TraverseForwards(
-        Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func)
+        Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, Traverse> func)
     {
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
@@ -414,9 +479,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
             }
 
-            if (!func(node.Item1, node.Item2, node.Item3, node.Item4))
+            var result = func(node.Item1, node.Item2, node.Item3, node.Item4);
+            switch (result)
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
 
             foreach (var outputProperty in node.Item1.Outputs)
@@ -429,6 +503,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
     }
 
+    public HashSet<NodeFrameViewModelBase> Frames { get; } = [];
 
     public virtual void Dispose()
     {

+ 222 - 35
src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs

@@ -1,20 +1,26 @@
-using PixiEditor.Models.Handlers;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Media;
+using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 
 namespace PixiEditor.ViewModels.Nodes;
 
 public sealed class NodeZoneViewModel : NodeFrameViewModelBase
 {
-    private INodeHandler start;
-    private INodeHandler end;
+    public INodeHandler Start { get; }
     
-    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id, [start, end])
+    public INodeHandler End { get; }
+
+    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id,
+        [start, end])
     {
         InternalName = internalName;
-        
-        this.start = start.Metadata.IsPairNodeStart ? start : end;
-        this.end = start.Metadata.IsPairNodeStart ? end : start;
-        
+
+        this.Start = start.Metadata.IsPairNodeStart ? start : end;
+        this.End = start.Metadata.IsPairNodeStart ? end : start;
+
         CalculateBounds();
     }
 
@@ -22,53 +28,234 @@ public sealed class NodeZoneViewModel : NodeFrameViewModelBase
     {
         if (Nodes.Count == 0)
         {
-            if (TopLeft == BottomRight)
+            return;
+        }
+
+        var points = GetBoundPoints();
+
+        Geometry = BuildRoundedHullGeometry(points, 25);
+    }
+
+    private static StreamGeometry BuildRoundedHullGeometry(List<VecD> points, double cornerRadius)
+    {
+        const double startBoostDeg = 100;
+        const double maxBoost = 2.5;
+
+        var span = CollectionsMarshal.AsSpan(points);
+
+        var pool = ArrayPool<VecD>.Shared;
+        var hullBuf = pool.Rent(Math.Max(3, span.Length));
+
+        try
+        {
+            var hullCount = ConvexHull(span, hullBuf.AsSpan());
+            var hull = hullBuf.AsSpan(0, hullCount);
+
+            var geometry = new StreamGeometry();
+            if (hull.IsEmpty) return geometry;
+
+            using var ctx = geometry.Open();
+            
+            if (hull.Length <= 2 || cornerRadius <= 0)
             {
-                BottomRight = TopLeft + new VecD(100, 100);
+                ctx.BeginFigure(new Point(hull[0].X, hull[0].Y), isFilled: true);
+                for (var i = 1; i < hull.Length; i++)
+                    ctx.LineTo(new Point(hull[i].X, hull[i].Y));
+                ctx.EndFigure(isClosed: true);
+                return geometry;
             }
-            
-            return;
+
+            var n = hull.Length;
+
+            var enter = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var exit = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var rented = n > 256;
+
+            try
+            {
+                for (var i = 0; i < n; i++)
+                {
+                    var prev = hull[(i - 1 + n) % n];
+                    var current = hull[i];
+                    var next = hull[(i + 1) % n];
+
+                    var directionIn = (current - prev).Normalize();
+                    var directionOut = (next - current).Normalize();
+                    var lenIn = (prev - current).Length;
+                    var lenOut = (current - next).Length;
+
+                    var a = (prev - current).Normalize();
+                    var b = (next - current).Normalize();
+                    var dot = Math.Clamp(a.X * b.X + a.Y * b.Y, -1, 1);
+                    var theta = Math.Acos(dot); // radians
+
+                    // Boost wide angles a bit (same curve, fewer ops)
+                    var thetaDeg = theta * (180.0 / Math.PI);
+                    var tNorm = Math.Clamp((thetaDeg - startBoostDeg) / (180.0 - startBoostDeg), 0, 1);
+                    var s = tNorm * tNorm * (3 - 2 * tNorm); // smoothstep
+                    var radiusHere = cornerRadius * (1 + (maxBoost - 1) * s);
+
+                    var t = (theta > 1e-6) ? radiusHere / Math.Tan(theta / 2.0) : 0;
+                    var tMax = Math.Min(lenIn, lenOut) * 0.5;
+                    t = Math.Min(t, tMax);
+
+                    if (t <= 1e-6)
+                    {
+                        enter[i] = current;
+                        exit[i] = current;
+                    }
+                    else
+                    {
+                        enter[i] = current + directionIn * -t;
+                        exit[i] = current + directionOut * t;
+                    }
+                }
+
+                ctx.BeginFigure(new Point(enter[0].X, enter[0].Y), isFilled: true);
+
+                for (var i = 0; i < n; i++)
+                {
+                    ctx.QuadraticBezierTo(
+                        new Point(hull[i].X, hull[i].Y),
+                        new Point(exit[i].X, exit[i].Y));
+
+                    var nextEnter = enter[(i + 1) % n];
+                    ctx.LineTo(new Point(nextEnter.X, nextEnter.Y));
+                }
+
+                ctx.EndFigure(isClosed: true);
+            }
+            finally
+            {
+                if (rented)
+                {
+                    pool.Return(
+                        MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(enter), n).ToArray());
+
+                    pool.Return(MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(exit), n)
+                        .ToArray());
+                }
+            }
+
+            return geometry;
+        }
+        finally
+        {
+            pool.Return(hullBuf);
         }
+    }
+
+    private static int ConvexHull(ReadOnlySpan<VecD> input, Span<VecD> hull)
+    {
+        var n = input.Length;
+        if (n <= 1)
+        {
+            if (n == 1) hull[0] = input[0];
+            return n;
+        }
+
+        var pool = ArrayPool<VecD>.Shared;
+        var pts = pool.Rent(n);
+
+        try
+        {
+            input.CopyTo(pts);
+            Array.Sort(pts, 0, n, VecDComparer.Instance);
+
+            var m = 0;
+            for (var i = 0; i < n; i++)
+            {
+                if (m == 0 || !pts[i].Equals(pts[m - 1]))
+                    pts[m++] = pts[i];
+            }
+
+            if (m <= 1)
+            {
+                if (m == 1) hull[0] = pts[0];
+                return m;
+            }
+
+            var k = 0;
+
+            for (var i = 0; i < m; i++)
+            {
+                while (k >= 2 && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
+
+            var t = k + 1;
+            for (var i = m - 2; i >= 0; i--)
+            {
+                while (k >= t && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
 
-        var bounds = GetBounds();
-        
-        var minX = bounds.Min(n => n.X);
-        var minY = bounds.Min(n => n.Y);
-        
-        var maxX = bounds.Max(n => n.Right);
-        var maxY = bounds.Max(n => n.Bottom);
+            return k - 1;
+        }
+        finally
+        {
+            pool.Return(pts);
+        }
+    }
 
-        TopLeft = new VecD(minX, minY);
-        BottomRight = new VecD(maxX, maxY);
+    private sealed class VecDComparer : IComparer<VecD>
+    {
+        public static readonly VecDComparer Instance = new();
 
-        Size = BottomRight - TopLeft;
+        public int Compare(VecD a, VecD b)
+        {
+            var cx = a.X.CompareTo(b.X);
+            return cx != 0 ? cx : a.Y.CompareTo(b.Y);
+        }
     }
 
-    private List<RectD> GetBounds()
+    private List<VecD> GetBoundPoints()
     {
-        var list = new List<RectD>();
+        var list = new List<VecD>(Nodes.Count * 4);
+
+        const int defaultXOffset = 30;
+        const int defaultYOffset = 45;
 
-        const int defaultXOffset = -30;
-        const int defaultYOffset = -45;
-        
-        // TODO: Use the actual node height
         foreach (var node in Nodes)
         {
-            if (node == start)
+            var pos = node.PositionBindable;
+            var size = new VecD(node.UiSize.Size.Width, node.UiSize.Size.Height);
+
+            if (node == Start)
             {
-                list.Add(new RectD(node.PositionBindable + new VecD(100, defaultYOffset), new VecD(100, 400)));
+                var twoThirdsX = size.X * (2.0 / 3.0);
+
+                list.Add(pos + new VecD(twoThirdsX, -defaultYOffset));
+                list.Add(pos + new VecD(twoThirdsX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(size.X + defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(size.X + defaultXOffset, defaultYOffset + size.Y));
                 continue;
             }
 
-            if (node == end)
+            if (node == End)
             {
-                list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(100, 400)));
+                var oneThirdX = size.X / 3.0;
+
+                list.Add(pos + new VecD(oneThirdX, -defaultYOffset));
+                list.Add(pos + new VecD(oneThirdX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(-defaultXOffset, defaultYOffset + size.Y));
                 continue;
             }
-            
-            list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(200, 400)));
+
+            var right = defaultXOffset + size.X;
+            var bottom = defaultYOffset + size.Y;
+
+            list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+            list.Add(pos + new VecD(right, -defaultYOffset));
+            list.Add(pos + new VecD(-defaultXOffset, bottom));
+            list.Add(pos + new VecD(right, bottom));
         }
-        
+
         return list;
     }
 }

+ 19 - 0
src/PixiEditor/ViewModels/Nodes/Traverse.cs

@@ -0,0 +1,19 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public enum Traverse
+{
+    /// <summary>
+    /// Go further in this direction, meaning any further child connections will not be enqueued.
+    /// </summary>
+    Further,
+    
+    /// <summary>
+    /// Don't go further in this direction, meaning all further child connections will be enqueued.
+    /// </summary>
+    NoFurther,
+    
+    /// <summary>
+    /// Completely stop traversing in any direction, meaning this will drop all enqueued child connections.
+    /// </summary>
+    Exit
+}

+ 4 - 0
src/PixiEditor/ViewModels/PixiObservableObject.cs

@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 using PixiEditor.Models.Commands;
 
 namespace PixiEditor.ViewModels;
@@ -11,4 +12,7 @@ public class PixiObservableObject : ObservableObject
         base.OnPropertyChanged(e);
         CommandController.Current.NotifyPropertyChanged(e.PropertyName);
     }
+
+    protected void SubscribeSettingsValueChanged<T>(Setting<T> settingStore, string propertyName) =>
+        settingStore.ValueChanged += (_, _) => OnPropertyChanged(propertyName);
 }

+ 22 - 2
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -95,9 +95,21 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     [Command.Internal("PixiEditor.Shortcuts.Export")]
     public static async Task ExportShortcuts()
     {
+        IStorageFolder? suggestedStartLocation = null;
+        try
+        {
+            suggestedStartLocation =
+                await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
+        }
+        catch (Exception)
+        {
+            // If we can't get the documents folder, we will just use the default location
+            // This is not a critical error, so we can ignore it
+        }
+
         var file = await MainWindow.Current!.StorageProvider.SaveFilePickerAsync(new()
         {
-            SuggestedStartLocation = await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents),
+            SuggestedStartLocation = suggestedStartLocation,
             FileTypeChoices = new List<FilePickerFileType>()
             {
                 new FilePickerFileType("PixiShorts (*.pixisc)")
@@ -127,7 +139,15 @@ internal partial class SettingsWindowViewModel : ViewModelBase
         
         if (file is not null)
         {
-            File.Copy(CommandController.ShortcutsPath, file.Path.LocalPath, true);
+            try
+            {
+                File.Copy(CommandController.ShortcutsPath, file.Path.LocalPath, true);
+            }
+            catch (Exception ex)
+            {
+                string errMessageTrimmed = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
+                NoticeDialog.Show(title: "ERROR", message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
+            }
         }
         
         // Sometimes, focus was brought back to the last edited shortcut

+ 3 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -347,13 +347,15 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     private void MoveSelectedMember(bool upwards)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
+        var member = doc?.SelectedStructureMember;
         if (member is null)
             return;
         var path = doc!.StructureHelper.FindPath(member.Id);
         if (path.Count < 2 || path[1] is not FolderNodeViewModel folderVm)
             return;
         var parent = folderVm;
+        if(parent.Children.Count == 0)
+            return;
         int curIndex = parent.Children.IndexOf(path[0]);
         if (upwards)
         {

+ 53 - 1
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -2,6 +2,7 @@
 using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Linq;
+using System.Reflection;
 using Avalonia.Input;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.ChangeableDocument;
@@ -63,6 +64,19 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         }
     }
 
+    public bool SelectionTintingEnabled
+    {
+        get => PixiEditorSettings.Tools.SelectionTintingEnabled.Value;
+        set
+        {
+            if (SelectionTintingEnabled == value)
+                return;
+
+            PixiEditorSettings.Tools.SelectionTintingEnabled.Value = value;
+            OnPropertyChanged();
+        }
+    }
+
     private Cursor? toolCursor;
 
     public Cursor? ToolCursor
@@ -118,6 +132,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         owner.DocumentManagerSubViewModel.ActiveDocumentChanged += ActiveDocumentChanged;
         PixiEditorSettings.Tools.PrimaryToolset.ValueChanged += PrimaryToolsetOnValueChanged;
+        SubscribeSettingsValueChanged(PixiEditorSettings.Tools.SelectionTintingEnabled, nameof(SelectionTintingEnabled));
     }
 
     private void PrimaryToolsetOnValueChanged(Setting<string> setting, string? newPrimaryToolset)
@@ -166,14 +181,39 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         ActiveTool?.OnToolDeselected(false);
         ActiveToolSet = toolSetHandler;
+        if (ActiveTool != null && !ActiveToolSet.Tools.Contains(ActiveTool))
+        {
+            TrySelectCommonToolInNewToolSet();
+        }
         ActiveToolSet.ApplyToolSetSettings();
         UpdateEnabledState();
 
+
         ActiveTool?.OnToolSelected(false);
 
         OnPropertyChanged(nameof(NonSelectedToolSets));
     }
 
+    private void TrySelectCommonToolInNewToolSet()
+    {
+        var commonTool = ActiveToolSet.Tools.FirstOrDefault(tool =>
+        {
+            var attr = tool.GetType().GetCustomAttribute<Command.ToolAttribute>();
+            if (attr is null) return false;
+
+            return ActiveTool?.GetType().GetCustomAttribute<Command.ToolAttribute>()?.CommonToolType ==
+                   attr.CommonToolType;
+        });
+
+        if (commonTool is not null)
+        {
+            SetActiveTool(commonTool, false);
+        }
+    }
+
+    [Command.Basic("PixiEditor.Tools.ToggleSelectionTinting", "TOGGLE_TINTING_SELECTION", "TOGGLE_TINTING_SELECTION_DESCRIPTIVE", AnalyticsTrack = true)]
+    public void ToggleTintSelection() => SelectionTintingEnabled = !SelectionTintingEnabled;
+
     public void SetupToolsTooltipShortcuts()
     {
         foreach (IToolHandler tool in allTools)
@@ -372,7 +412,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         if (foundTool == null)
         {
             foundTool = allTools.FirstOrDefault(x => x.GetType().IsAssignableFrom(toolType));
-            if(foundTool == null)
+            if(foundTool == null || SimilarToolInActiveToolSetExists(toolType))
                 return;
 
             var toolset = AllToolSets.FirstOrDefault(x => x.Tools.Contains(foundTool));
@@ -409,6 +449,18 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         }
     }
 
+    private bool SimilarToolInActiveToolSetExists(Type toolType)
+    {
+        Command.ToolAttribute attr = toolType.GetCustomAttribute<Command.ToolAttribute>();
+        if (attr is null) return false;
+
+        return ActiveToolSet.Tools.Any(tool =>
+        {
+            var toolAttr = tool.GetType().GetCustomAttribute<Command.ToolAttribute>();
+            return toolAttr is not null && toolAttr.CommonToolType == attr.CommonToolType;
+        });
+    }
+
     public void HandleToolRepeatShortcutDown()
     {
         if (ActiveTool == null) return;

+ 19 - 2
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -54,12 +54,17 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     {
         get
         {
-            if (TimeToEndTimeout == null)
+            if (TimeToEndTimeout == null || !EmailEqualsLastSentMail)
             {
                 return string.Empty;
             }
 
             TimeSpan timeLeft = TimeToEndTimeout.Value - DateTime.Now;
+            if(timeLeft.TotalHours > 1)
+                return $"({timeLeft:hh\\:mm\\:ss})";
+            if(timeLeft.TotalMinutes > 1)
+                return $"({timeLeft:mm\\:ss})";
+
             return timeLeft.TotalSeconds > 0 ? $"({timeLeft:ss})" : string.Empty;
         }
     }
@@ -194,6 +199,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             LastError = null;
             try
             {
+                lastSentHash = EmailUtility.GetEmailHash(email);
                 await pixiAuthIdentityProvider.RequestLogin(email);
             }
             catch (Exception ex)
@@ -206,7 +212,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     public bool CanRequestLogin(string email)
     {
         return IdentityProvider is PixiAuthIdentityProvider && !string.IsNullOrEmpty(email) && email.Contains('@') &&
-               !HasTimeout();
+               !(HasTimeout() && EmailEqualsLastSentMail);
     }
 
     public async Task ResendActivation(string email)
@@ -240,6 +246,10 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         DispatcherTimer.RunOnce(
             () =>
             {
+                if (TimeToEndTimeout.HasValue && TimeToEndTimeout.Value > DateTime.Now)
+                {
+                    return;
+                }
                 TimeToEndTimeout = null;
                 LastError = null;
                 NotifyProperties();
@@ -376,6 +386,13 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
 
     private void OnError(string error, object? arg = null)
     {
+        if (error != "TOO_MANY_REQUESTS")
+        {
+            TimeToEndTimeout = null;
+            timerCancelable?.Dispose();
+            timerCancelable = null;
+            NotifyProperties();
+        }
         if (error == "SESSION_NOT_VALIDATED")
         {
             LastError = null;

+ 28 - 0
src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs

@@ -2,6 +2,7 @@
 using System.Drawing;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Controls;
 using Avalonia.Input;
 using CommunityToolkit.Mvvm.Input;
 using Drawie.Numerics;
@@ -303,4 +304,31 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>, IWindowHandler
         popup.Show();
         return popup;
     }
+
+    /// <summary>
+    /// Used to save the WindowState before toggling to FullScreen-Mode.
+    /// </summary>
+    private WindowState LastWindowState { get; set; }
+
+    /// <summary>
+    /// Method used to toggle to FullScreen-Mode.
+    /// </summary>
+    [Commands_Command.Basic("PixiEditor.Window.ToggleFullscreen", "TOGGLE_FULLSCREEN", "TOGGLE_FULLSCREEN_DESCRIPTIVE",
+        Key = Key.F11,
+        Icon = PixiPerfectIcons.Fullscreen,
+        AnalyticsTrack = true)]
+    public void ToggleFullscreen()
+    {
+        var window = Owner.AttachedWindow;
+
+        if (window is null) return;
+
+        if (window.WindowState != WindowState.FullScreen) LastWindowState = window.WindowState;
+
+        window.WindowState = window.WindowState switch
+        {
+            WindowState.FullScreen => LastWindowState,
+            _ => WindowState.FullScreen
+        };
+    }
 }

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

@@ -10,7 +10,7 @@ using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.C)]
+[Command.Tool(Key = Key.C, CommonToolType = "Ellipse")]
 internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
 {
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -12,7 +12,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.L)]
+[Command.Tool(Key = Key.L, CommonToolType = "Line")]
 internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 {
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -11,7 +11,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.R)]
+[Command.Tool(Key = Key.R, CommonToolType = "Rectangle")]
 internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHandler
 {
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -12,7 +12,7 @@ using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.C)]
+[Command.Tool(Key = Key.C, CommonToolType = "Ellipse")]
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 {
     public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME";

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

@@ -16,7 +16,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.L)]
+[Command.Tool(Key = Key.L, CommonToolType = "Line")]
 internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 {
     public const string NewLayerKey = "NEW_LINE_LAYER_NAME";

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

@@ -13,7 +13,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.R)]
+[Command.Tool(Key = Key.R, CommonToolType = "Rectangle")]
 internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHandler
 {
     public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";

+ 9 - 0
src/PixiEditor/ViewModels/UserPreferences/Settings/SceneSettings.cs

@@ -3,6 +3,7 @@ using Avalonia.Media;
 using CommunityToolkit.Mvvm.Input;
 using Drawie.Numerics;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers.Extensions;
 
 namespace PixiEditor.ViewModels.UserPreferences.Settings;
@@ -44,6 +45,12 @@ internal class SceneSettings : SettingsGroup
         set => RaiseAndUpdatePreference(ref _secondaryBackgroundColorHex, value, PreferencesConstants.SecondaryBackgroundColor);
     }
 
+    public bool SelectionTintingEnabled
+    {
+        get => PixiEditorSettings.Tools.SelectionTintingEnabled.Value;
+        set => RaiseAndUpdatePreference(PixiEditorSettings.Tools.SelectionTintingEnabled, value);
+    }
+
     public Color PrimaryBackgroundColor
     {
         get => Color.Parse(PrimaryBackgroundColorHex);
@@ -65,5 +72,7 @@ internal class SceneSettings : SettingsGroup
             PrimaryBackgroundColorHex = PreferencesConstants.PrimaryBackgroundColorDefault;
             SecondaryBackgroundColorHex = PreferencesConstants.SecondaryBackgroundColorDefault;
         });
+        
+        SubscribeValueChanged(PixiEditorSettings.Tools.SelectionTintingEnabled, nameof(SelectionTintingEnabled));
     }
 }

+ 16 - 1
src/PixiEditor/ViewModels/UserPreferences/SettingsGroup.cs

@@ -1,10 +1,11 @@
 using System.Runtime.CompilerServices;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 
 namespace PixiEditor.ViewModels.UserPreferences;
 
-internal class SettingsGroup : ObservableObject
+internal class SettingsGroup : PixiObservableObject
 {
     protected static T GetPreference<T>(string name)
     {
@@ -27,4 +28,18 @@ internal class SettingsGroup : ObservableObject
         SetProperty(ref backingStore, value, propertyName: name);
         IPreferences.Current.UpdatePreference(name, value);
     }
+    
+    protected void RaiseAndUpdatePreference<T>(Setting<T> settingStore, T value, [CallerMemberName] string name = "")
+    {
+        if (EqualityComparer<T>.Default.Equals(settingStore.Value, value))
+            return;
+
+        settingStore.Value = value;
+        OnPropertyChanged(name);
+    }
+
+    protected void SubscribeValueChanged<T>(Setting<T> settingStore, string propertyName)
+    {
+        settingStore.ValueChanged += (_, _) => OnPropertyChanged(propertyName);
+    }
 }

+ 1 - 1
src/PixiEditor/Views/Dock/DocumentTemplate.axaml

@@ -20,7 +20,7 @@
 
     <viewportControls:Viewport
         x:Name="Viewport"
-        CenterViewportTrigger="{Binding DockCenterViewportTrigger}"
+        CenterViewportTrigger="{Binding CenterViewportTrigger}"
         ZoomViewportTrigger="{Binding ZoomViewportTrigger}"
         MouseDownCommand="{Binding Path=IoSubViewModel.MouseDownCommand, Source={viewModels1:MainVM}}"
         MouseMoveCommand="{Binding Path=IoSubViewModel.MouseMoveCommand, Source={viewModels1:MainVM}}"

+ 1 - 1
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -47,7 +47,7 @@
                         </OnPlatform.Default>
                     </OnPlatform>
                 </Panel.Margin>
-                <ToggleButton Name="LogoButton" Padding="0" BorderThickness="0">
+                <ToggleButton Name="LogoButton" Padding="0" BorderThickness="0" FlowDirection="LeftToRight">
                     <Interaction.Behaviors>
                         <behaviours:ShowFlyoutOnTrigger Trigger="{Binding OpenPixiEditorMenuTrigger}" />
                     </Interaction.Behaviors>

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

@@ -174,7 +174,7 @@
             </Grid.RowDefinitions>
             <tools:Toolbar
                 DataContext="{Binding Source={viewModels:MainVM}, Path=.}" />
-            <StackPanel Margin="0, 5, 0, 0" Background="Transparent" Grid.Row="1" Orientation="Horizontal" Name="toolsetsPanel">
+            <StackPanel Margin="0, 5, 0, 0" Grid.Row="1" Orientation="Horizontal" Name="toolsetsPanel">
                 <Border ClipToBounds="False"
                         HorizontalAlignment="Left"
                         Padding="5 0"

+ 9 - 1
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -155,6 +155,13 @@ internal class ViewportOverlays
             Mode = BindingMode.OneWay
         };
 
+        Binding selectionTintingEnabled = new()
+        {
+            Source = ViewModelMain.Current,
+            Path = "ToolsSubViewModel.SelectionTintingEnabled",
+            Mode = BindingMode.OneWay
+        };
+
         MultiBinding showFillBinding = new()
         {
             Converter = new AllTrueConverter(),
@@ -162,7 +169,8 @@ internal class ViewportOverlays
             Bindings = new List<IBinding>()
             {
                 toolIsSelectionBinding,
-                isTransformingBinding
+                isTransformingBinding,
+                selectionTintingEnabled
             }
         };
 

+ 6 - 21
src/PixiEditor/Views/Nodes/NodeFrameView.cs

@@ -1,32 +1,17 @@
 using Avalonia;
 using Avalonia.Controls.Primitives;
+using Avalonia.Media;
 using Drawie.Numerics;
 
 namespace PixiEditor.Views.Nodes;
 
 public class NodeFrameView : TemplatedControl
 {
-    public static readonly StyledProperty<VecD> TopLeftProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(TopLeft));
-    
-    public VecD TopLeft
-    {
-        get => GetValue(TopLeftProperty);
-        set => SetValue(TopLeftProperty, value);
-    }
-    
-    public static readonly StyledProperty<VecD> BottomRightProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(BottomRight));
-    
-    public VecD BottomRight
-    {
-        get => GetValue(BottomRightProperty);
-        set => SetValue(BottomRightProperty, value);
-    }
-    
-    public static readonly StyledProperty<VecD> SizeProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(Size));
-    
-    public VecD Size
+    public static readonly StyledProperty<StreamGeometry> GeometryProperty = AvaloniaProperty.Register<NodeFrameView, StreamGeometry>(nameof(Geometry));
+
+    public StreamGeometry Geometry
     {
-        get => GetValue(SizeProperty);
-        set => SetValue(SizeProperty, value);
+        get => GetValue(GeometryProperty);
+        set => SetValue(GeometryProperty, value);
     }
 }

+ 0 - 4
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -773,10 +773,6 @@ internal class NodeGraphView : Zoombox.Zoombox
                 {
                     connection = (endConnectionProperty, startConnectionProperty, null);
                 }
-                else
-                {
-                    return;
-                }
             }
         }
 

+ 7 - 0
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -421,6 +421,13 @@
                                 d:Content="Reset"
                                 Background="{DynamicResource ThemeAccentBrush}"
                                 ui:Translator.Key="RESET" />
+                            
+                            <TextBlock ui:Translator.Key="SELECTION" Classes="h5" />
+
+                            <CheckBox Classes="leftOffset" Width="200" HorizontalAlignment="Left"
+                                      ui:Translator.Key="TINT_SELECTION"
+                                      IsChecked="{Binding SettingsSubViewModel.Scene.SelectionTintingEnabled}" />
+
                         </controls:FixedSizeStackPanel>
                     </ScrollViewer>
                     <ScrollViewer>

+ 0 - 20
src/stylecop.json

@@ -1,20 +0,0 @@
-{
-  "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
-  "settings": {
-    "indentation": {
-      "indentationSize": 4
-    },
-    "maintainabilityRules": {
-      "topLevelTypes": [ "class", "interface", "enum", "struct" ]
-    },
-    "readabilityRules": {
-      "allowBuiltInTypeAliases": false
-    },
-    "documentationRules": {
-      "xmlHeader": false,
-      "documentInterfaces": false,
-      "documentExposedElements": false,
-      "documentInternalElements": false
-    }
-  }
-}

+ 1 - 1
tests/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.3.0</AvaloniaVersion>
+		<AvaloniaVersion>11.3.5</AvaloniaVersion>
     </PropertyGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />