Răsfoiți Sursa

Merge pull request #865 from PixiEditor/fixes/26.03.2025

Fixes/26.03.2025
Krzysztof Krysiński 4 luni în urmă
părinte
comite
413cb8e33d
38 a modificat fișierele cu 453 adăugiri și 63 ștergeri
  1. 1 1
      src/Drawie
  2. 11 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/ChangeError_Info.cs
  3. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IScalable.cs
  4. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/LineVectorData.cs
  5. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  6. 21 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs
  7. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  8. 20 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  9. 58 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromLayer_Change.cs
  10. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Change.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectProperties_Change.cs
  12. 11 0
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  13. 58 12
      src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs
  14. 4 3
      src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs
  15. 14 1
      src/PixiEditor.Extensions/Helpers/EnumHelpers.cs
  16. 61 3
      src/PixiEditor/Data/Localization/Languages/en.json
  17. 1 1
      src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json
  18. 18 1
      src/PixiEditor/Helpers/Converters/EnumToLocalizedStringConverter.cs
  19. 32 0
      src/PixiEditor/Helpers/StringHelpers.cs
  20. 13 0
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  21. 4 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  22. 1 1
      src/PixiEditor/Models/Files/VideoFileType.cs
  23. 18 0
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  24. 5 0
      src/PixiEditor/Models/Serialization/Factories/FontFamilySerializationFactory.cs
  25. 2 2
      src/PixiEditor/Styles/Templates/KeyFrame.axaml
  26. 15 0
      src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs
  27. 14 6
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  28. 7 7
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  29. 7 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/KernelFilterNodeViewModel.cs
  30. 1 1
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  31. 3 2
      src/PixiEditor/Views/Animations/KeyFrame.cs
  32. 10 6
      src/PixiEditor/Views/Animations/Timeline.cs
  33. 2 0
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml
  34. 1 1
      src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs
  35. 15 0
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  36. 5 2
      src/PixiEditor/Views/Overlays/TextOverlay/Caret.cs
  37. 1 1
      src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs
  38. 2 1
      src/PixiEditor/Views/Rendering/Scene.cs

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 1f966ffb17a8f2140c4b0d95e5e318880a4b53e5
+Subproject commit 7e4de2359281d40b99049748d7056f6cd836fbde

+ 11 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/ChangeError_Info.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos;
+
+public struct ChangeError_Info : IChangeInfo
+{
+    public string Message { get; }
+
+    public ChangeError_Info(string message)
+    {
+        Message = message;
+    }
+}

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IScalable.cs

@@ -0,0 +1,8 @@
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface IScalable
+{
+    public void Resize(VecD multiplier);
+}

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

@@ -120,4 +120,5 @@ public class LineVectorData : ShapeVectorData, IReadOnlyLineData
 
         return path;
     }
+
 }

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

@@ -10,8 +10,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 {
-    public VecD Center { get; }
-    public VecD Size { get; }
+    public VecD Center { get; set; }
+    public VecD Size { get; set; }
 
     public override RectD GeometryAABB => RectD.FromCenterAndSize(Center, Size);
 

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

@@ -5,11 +5,12 @@ using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public class TextVectorData : ShapeVectorData, IReadOnlyTextData
+public class TextVectorData : ShapeVectorData, IReadOnlyTextData, IScalable
 {
     private string text;
     private Font font = Font.CreateDefault();
@@ -237,4 +238,23 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
 
         return hash.ToHashCode();
     }
+
+    public void Resize(VecD multiplier)
+    {
+        // TODO: Resize font size
+        /*Position = Position.Multiply(multiplier);
+        if(Font != null)
+        {
+            Font.Size *= multiplier.Y;
+        }
+
+        if (Spacing.HasValue)
+        {
+            Spacing *= multiplier.Y;
+        }*/
+
+        TransformationMatrix = TransformationMatrix.PostConcat(Matrix3X3.CreateScale((float)multiplier.X, (float)multiplier.Y));
+
+        lastBounds = richText.MeasureBounds(Font);
+    }
 }

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs

@@ -15,8 +15,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 public class TileNode : RenderNode
 {
     public InputProperty<Texture> Image { get; }
-    public InputProperty<ShaderTileMode> TileModeX { get; }
-    public InputProperty<ShaderTileMode> TileModeY { get; }
+    public InputProperty<TileMode> TileModeX { get; }
+    public InputProperty<TileMode> TileModeY { get; }
     public InputProperty<Matrix3X3> Matrix { get; }
 
     private Drawie.Backend.Core.Surfaces.ImageData.Image lastImage;
@@ -26,8 +26,8 @@ public class TileNode : RenderNode
     public TileNode()
     {
         Image = CreateInput<Texture>("Image", "IMAGE", null);
-        TileModeX = CreateInput<ShaderTileMode>("TileModeX", "TILE_MODE_X", ShaderTileMode.Repeat);
-        TileModeY = CreateInput<ShaderTileMode>("TileModeY", "TILE_MODE_Y", ShaderTileMode.Repeat);
+        TileModeX = CreateInput<TileMode>("TileModeX", "TILE_MODE_X", TileMode.Repeat);
+        TileModeY = CreateInput<TileMode>("TileModeY", "TILE_MODE_Y", TileMode.Repeat);
         Matrix = CreateInput<Matrix3X3>("Matrix", "MATRIX", Matrix3X3.Identity);
 
         Output.FirstInChain = null;

+ 20 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -16,7 +16,7 @@ using Drawie.Numerics;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 [NodeInfo("VectorLayer")]
-public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable
+public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorNode, IRasterizable, IScalable
 {
     public OutputProperty<ShapeVectorData> Shape { get; }
 
@@ -190,4 +190,23 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
             AllowHighDpiRendering = this.AllowHighDpiRendering
         };
     }
+
+    public void Resize(VecD multiplier)
+    {
+        if (ShapeData == null)
+        {
+            return;
+        }
+
+        if(ShapeData is IScalable resizable)
+        {
+            resizable.Resize(multiplier);
+        }
+        else
+        {
+            ShapeData.TransformationMatrix =
+                ShapeData.TransformationMatrix.PostConcat(Matrix3X3.CreateScale((float)multiplier.X,
+                    (float)multiplier.Y));
+        }
+    }
 }

+ 58 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromLayer_Change.cs

@@ -0,0 +1,58 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class CreateAnimationDataFromLayer_Change : Change
+{
+    private readonly Guid layerGuid;
+
+    [GenerateMakeChangeAction]
+    public CreateAnimationDataFromLayer_Change(Guid layerGuid)
+    {
+        this.layerGuid = layerGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        return target.TryFindMember<LayerNode>(layerGuid, out LayerNode? layer) && layer.KeyFrames != null &&
+               layer.KeyFrames.Count != 0;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        LayerNode layer = target.FindNode(layerGuid) as LayerNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+        foreach (var frame in layer.KeyFrames)
+        {
+            Guid keyFrameId = Guid.NewGuid();
+            target.AnimationData.AddKeyFrame(new RasterKeyFrame(keyFrameId, layer.Id, frame.StartFrame, target)
+            {
+                Duration = frame.Duration
+            });
+            infos.Add(new CreateRasterKeyFrame_ChangeInfo(layer.Id, frame.StartFrame, keyFrameId, true));
+        }
+
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var layer = target.FindNode(layerGuid) as LayerNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+
+        var keyFrame = target.AnimationData.KeyFrames;
+        var ids = keyFrame.Where(x => x.NodeId == layer.Id).Select(x => x.Id).ToList();
+
+        foreach (var id in ids)
+        {
+            target.AnimationData.RemoveKeyFrame(id);
+            infos.Add(new DeleteKeyFrame_ChangeInfo(id));
+        }
+
+        return infos;
+    }
+}

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Change.cs

@@ -2,6 +2,7 @@
 
 internal abstract class Change : IDisposable
 {
+    public string FailedMessage { get; protected set; }
     public Guid ChangeGuid { get; } = Guid.NewGuid();
 
     /// <summary>

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/ConnectProperties_Change.cs

@@ -169,7 +169,7 @@ internal class ConnectProperties_Change : Change
         return changes;
     }
 
-    private bool IsLoop(InputProperty input, OutputProperty output)
+    private static bool IsLoop(InputProperty input, OutputProperty output)
     {
         if (input.Node == output.Node)
         {

+ 11 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -6,6 +6,7 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using BlendMode = Drawie.Backend.Core.Surfaces.BlendMode;
 
 namespace PixiEditor.ChangeableDocument.Changes.Root;
@@ -98,6 +99,11 @@ internal class ResizeImage_Change : Change
                     img.CommitChanges();
                 });
             }
+            else if (member is IScalable scalableLayer)
+            {
+                VecD multiplier = new VecD(newSize.X / (double)originalSize.X, newSize.Y / (double)originalSize.Y);
+                scalableLayer.Resize(multiplier);
+            }
 
             // Add support for different Layer types
 
@@ -129,6 +135,11 @@ internal class ResizeImage_Change : Change
                     layerImage.CommitChanges();
                 });
             }
+            else if (member is IScalable scalableLayer)
+            {
+                VecD multiplier = new VecD(originalSize.X / (double)newSize.X, originalSize.Y / (double)newSize.Y);
+                scalableLayer.Resize(multiplier);
+            }
 
             if (member.EmbeddedMask is not null)
             {

+ 58 - 12
src/PixiEditor.ChangeableDocument/Changes/Structure/MoveStructureMember_Change.cs

@@ -33,10 +33,16 @@ internal class MoveStructureMember_Change : Change
     public override bool InitializeAndValidate(Document document)
     {
         var member = document.FindMember(memberGuid);
-        var targetFolder = document.FindNode(targetNodeGuid);
-        if (member is null || targetFolder is null)
+        var targetNode = document.FindNode(targetNodeGuid);
+        if (member is null || targetNode is null)
             return false;
 
+        if (WillCreateLoop(member, targetNode))
+        {
+            FailedMessage = "ERROR_LOOP_DETECTED_MESSAGE";
+            return false;
+        }
+
         originalConnections = NodeOperations.CreateConnectionsData(member);
 
         return true;
@@ -67,7 +73,7 @@ internal class MoveStructureMember_Change : Change
         var previouslyConnected = inputProperty.Connection;
 
         bool isMovingBelow = false;
-        
+
         inputProperty.Node.TraverseForwards(x =>
         {
             if (x.Id == sourceNodeGuid)
@@ -75,13 +81,14 @@ internal class MoveStructureMember_Change : Change
                 isMovingBelow = true;
                 return false;
             }
-            
+
             return true;
         });
 
         if (isMovingBelow)
         {
-            changes.AddRange(NodeOperations.AdjustPositionsBeforeAppend(sourceNode, inputProperty.Node, out originalPositions));
+            changes.AddRange(
+                NodeOperations.AdjustPositionsBeforeAppend(sourceNode, inputProperty.Node, out originalPositions));
         }
 
         changes.AddRange(NodeOperations.DetachStructureNode(sourceNode));
@@ -129,8 +136,9 @@ internal class MoveStructureMember_Change : Change
 
         return changes;
     }
-    
-    private static List<IChangeInfo> AdjustPutIntoFolderPositions(Node targetNode, Dictionary<Guid, VecD> originalPositions)
+
+    private static List<IChangeInfo> AdjustPutIntoFolderPositions(Node targetNode,
+        Dictionary<Guid, VecD> originalPositions)
     {
         List<IChangeInfo> changes = new();
 
@@ -144,14 +152,14 @@ internal class MoveStructureMember_Change : Change
                     {
                         originalPositions[node.Id] = node.Position;
                     }
-                    
+
                     node.Position = new VecD(node.Position.X, folder.Position.Y + 250);
                     changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
                 }
-                
+
                 return true;
             });
-            
+
             folder.Background.Connection?.Node.TraverseBackwards(bgNode =>
             {
                 if (bgNode is Node node)
@@ -167,15 +175,53 @@ internal class MoveStructureMember_Change : Change
                     {
                         pos -= 250;
                     }
-                    
+
                     node.Position = new VecD(node.Position.X, pos);
                     changes.Add(new NodePosition_ChangeInfo(node.Id, node.Position));
                 }
-                
+
                 return true;
             });
         }
 
         return changes;
     }
+
+    private bool WillCreateLoop(StructureNode member, Node targetNode)
+    {
+        InputProperty? input = targetNode.GetInputProperty("Background");
+        OutputProperty output = member.Output;
+
+        if (input is null)
+            return false;
+
+        return IsLoop(input, output);
+    }
+
+    private static bool IsLoop(InputProperty input, OutputProperty output)
+    {
+        if (input.Node == output.Node)
+        {
+            return true;
+        }
+
+        if (input.Node.OutputProperties.Any(x => x.InternalPropertyName != "Output" && x.Connections.Any(y => y.Node == output.Node)))
+        {
+            return true;
+        }
+
+        bool isLoop = false;
+        input.Node.TraverseForwards((node, inputProp) =>
+        {
+            if (node == output.Node && inputProp.InternalPropertyName != "Background")
+            {
+                isLoop = true;
+                return false;
+            }
+
+            return true;
+        });
+
+        return isLoop;
+    }
 }

+ 4 - 3
src/PixiEditor.ChangeableDocument/DocumentChangeTracker.cs

@@ -210,7 +210,7 @@ public class DocumentChangeTracker : IDisposable
         }
 
         bool ignoreInUndo = false;
-        List<IChangeInfo> changeInfos = new();
+        System.Collections.Generic.List<IChangeInfo> changeInfos = new();
 
         if (activeUpdateableChange is InterruptableUpdateableChange interruptable)
         {
@@ -232,9 +232,10 @@ public class DocumentChangeTracker : IDisposable
         var validationResult = change.InitializeAndValidate(document);
         if (!validationResult)
         {
-            Trace.WriteLine($"Change {change} failed validation");
+            string? failedMessage = change.FailedMessage;
+            Trace.WriteLine($"Change {change} failed validation. Reason: {failedMessage}");
             change.Dispose();
-            return new None();
+            return string.IsNullOrEmpty(failedMessage) ? new None() : new ChangeError_Info(failedMessage);
         }
 
         var info = change.Apply(document, true, out ignoreInUndo);

+ 14 - 1
src/PixiEditor.Extensions/Helpers/EnumHelpers.cs

@@ -41,7 +41,7 @@ public static class EnumHelpers
         if (fieldInfo != null)
         {
             var attrs = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), true);
-            if (attrs != null && attrs.Length > 0)
+            if (attrs is { Length: > 0 })
             {
                 description = ((DescriptionAttribute)attrs[0]).Description;
             }
@@ -49,4 +49,17 @@ public static class EnumHelpers
 
         return description;
     }
+
+    public static bool HasDescription(Enum enumValue)
+    {
+        var fieldInfo = enumValue.GetType().GetField(enumValue.ToString());
+
+        if (fieldInfo != null)
+        {
+            var attrs = fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), true);
+            return attrs is { Length: > 0 };
+        }
+
+        return false;
+    }
 }

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

@@ -152,8 +152,8 @@
   "ROT_LAYERS_-90": "Rotate Selected Layers -90 degrees",
   "TOGGLE_VERT_SYMMETRY_AXIS": "Toggle vertical symmetry axis",
   "TOGGLE_HOR_SYMMETRY_AXIS": "Toggle horizontal symmetry axis",
-  "DELETE_PIXELS": "Delete pixels",
-  "DELETE_PIXELS_DESCRIPTIVE": "Delete selected pixels",
+  "DELETE_SELECTED": "Delete selected",
+  "DELETE_SELECTED_DESCRIPTIVE": "Delete selected element (layer, pixels, etc.)",
   "RESIZE_DOCUMENT": "Resize document",
   "RESIZE_CANVAS": "Resize canvas",
   "CENTER_CONTENT": "Center content",
@@ -923,5 +923,63 @@
   "LOAD_LAZY_FILE_MESSAGE": "To improve startup time, PixiEditor didn't load this file. Click the button below to load it.",
   "EASING_NODE": "Easing",
   "EASING_TYPE": "Easing Type",
-  "OPEN_DIRECTORY_ON_EXPORT": "Open directory on export"
+  "OPEN_DIRECTORY_ON_EXPORT": "Open directory on export",
+  "ERROR_LOOP_DETECTED_MESSAGE": "Moving this layer will create a loop. Fix it in the Node Graph.",
+  "LINEAR_EASING_TYPE": "Linear",
+  "IN_SINE_EASING_TYPE": "In Sine",
+  "OUT_SINE_EASING_TYPE": "Out Sine",
+  "IN_OUT_SINE_EASING_TYPE": "In Out Sine",
+  "IN_QUAD_EASING_TYPE": "In Quad",
+  "OUT_QUAD_EASING_TYPE": "Out Quad",
+  "IN_OUT_QUAD_EASING_TYPE": "In Out Quad",
+  "IN_CUBIC_EASING_TYPE": "In Cubic",
+  "OUT_CUBIC_EASING_TYPE": "Out Cubic",
+  "IN_OUT_CUBIC_EASING_TYPE": "In Out Cubic",
+  "IN_QUART_EASING_TYPE": "In Quart",
+  "OUT_QUART_EASING_TYPE": "Out Quart",
+  "IN_OUT_QUART_EASING_TYPE": "In Out Quart",
+  "IN_QUINT_EASING_TYPE": "In Quint",
+  "OUT_QUINT_EASING_TYPE": "Out Quint",
+  "IN_OUT_QUINT_EASING_TYPE": "In Out Quint",
+  "IN_EXPO_EASING_TYPE": "In Expo",
+  "OUT_EXPO_EASING_TYPE": "Out Expo",
+  "IN_OUT_EXPO_EASING_TYPE": "In Out Expo",
+  "IN_CIRC_EASING_TYPE": "In Circ",
+  "OUT_CIRC_EASING_TYPE": "Out Circ",
+  "IN_OUT_CIRC_EASING_TYPE": "In Out Circ",
+  "IN_BACK_EASING_TYPE": "In Back",
+  "OUT_BACK_EASING_TYPE": "Out Back",
+  "IN_OUT_BACK_EASING_TYPE": "In Out Back",
+  "IN_ELASTIC_EASING_TYPE": "In Elastic",
+  "OUT_ELASTIC_EASING_TYPE": "Out Elastic",
+  "IN_OUT_ELASTIC_EASING_TYPE": "In Out Elastic",
+  "IN_BOUNCE_EASING_TYPE": "In Bounce",
+  "OUT_BOUNCE_EASING_TYPE": "Out Bounce",
+  "IN_OUT_BOUNCE_EASING_TYPE": "In Out Bounce",
+  "CLAMP_SHADER_TILE_NODE": "Clamp",
+  "REPEAT_SHADER_TILE_NODE": "Repeat",
+  "MIRROR_SHADER_TILE_NODE": "Mirror",
+  "DECAL_SHADER_TILE_NODE": "Decal",
+  "R_G_B_COMBINE_SEPARATE_COLOR_MODE": "RGB",
+  "H_S_V_COMBINE_SEPARATE_COLOR_MODE": "HSV",
+  "H_S_L_COMBINE_SEPARATE_COLOR_MODE": "HSL",
+  "COLOR_MANAGED_COLOR_SAMPLE_MODE": "Color Managed",
+  "RAW_COLOR_SAMPLE_MODE": "Raw",
+  "FRACTAL_PERLIN_NOISE_TYPE": "Perlin",
+  "TURBULENCE_PERLIN_NOISE_TYPE": "Turbulence",
+  "INHERIT_COLOR_SPACE_TYPE": "Inherit",
+  "SRGB_COLOR_SPACE_TYPE": "sRGB",
+  "LINEAR_SRGB_COLOR_SPACE_TYPE": "Linear sRGB",
+  "SIMPLE_OUTLINE_TYPE": "Simple",
+  "GAUSSIAN_OUTLINE_TYPE": "Gaussian",
+  "PIXEL_PERFECT_OUTLINE_TYPE": "Pixel Perfect",
+  "DEGREES_ROTATION_TYPE": "Degrees",
+  "RADIANS_ROTATION_TYPE": "Radians",
+  "WEIGHTED_GRAYSCALE_MODE": "Weighted",
+  "AVERAGE_GRAYSCALE_MODE": "Average",
+  "CUSTOM_GRAYSCALE_MODE": "Custom",
+  "CLAMP_TILE_MODE": "Clamp",
+  "REPEAT_TILE_MODE": "Repeat",
+  "MIRROR_TILE_MODE": "Mirror",
+  "DECAL_TILE_MODE": "Decal"
 }

+ 1 - 1
src/PixiEditor/Data/ShortcutActionMaps/AsepriteShortcutMap.json

@@ -460,7 +460,7 @@
       "Parameters": []
     },
     "Clear": {
-      "Command": "PixiEditor.Document.DeletePixels",
+      "Command": "PixiEditor.Document.DeleteSelected",
       "DefaultShortcut": {
         "key": "Delete",
         "modifiers": null

+ 18 - 1
src/PixiEditor/Helpers/Converters/EnumToLocalizedStringConverter.cs

@@ -9,9 +9,26 @@ internal class EnumToLocalizedStringConverter : SingleInstanceConverter<EnumToLo
     {
         if (value is Enum enumValue)
         {
-            return EnumHelpers.GetDescription(enumValue);
+            if (EnumHelpers.HasDescription(enumValue))
+            {
+                return EnumHelpers.GetDescription(enumValue);
+            }
+
+            return ToLocalizedStringFormat(enumValue);
         }
 
         return value;
     }
+
+    private string ToLocalizedStringFormat(Enum enumValue)
+    {
+        // VALUE_ENUMTYPE
+        // for example BlendMode.Normal becomes NORMAL_BLEND_MODE
+
+        string enumType = enumValue.GetType().Name;
+
+        string value = enumValue.ToString();
+
+        return $"{value.ToSnakeCase()}_{enumType.ToSnakeCase()}".ToUpper();
+    }
 }

+ 32 - 0
src/PixiEditor/Helpers/StringHelpers.cs

@@ -17,6 +17,7 @@ internal static class StringHelpers
                 newText.Append(' ');
             newText.Append(text[i]);
         }
+
         return newText.ToString();
     }
 
@@ -24,4 +25,35 @@ internal static class StringHelpers
     {
         return value.Length > maxLenght ? value.Substring(0, maxLenght) : value;
     }
+
+    public static string ToSnakeCase(this string text)
+    {
+        if (text == null)
+        {
+            throw new ArgumentNullException(nameof(text));
+        }
+
+        if (text.Length < 2)
+        {
+            return text.ToLowerInvariant();
+        }
+
+        var sb = new StringBuilder();
+        sb.Append(char.ToLowerInvariant(text[0]));
+        for (int i = 1; i < text.Length; ++i)
+        {
+            char c = text[i];
+            if (char.IsUpper(c))
+            {
+                sb.Append('_');
+                sb.Append(char.ToLowerInvariant(c));
+            }
+            else
+            {
+                sb.Append(c);
+            }
+        }
+
+        return sb.ToString();
+    }
 }

+ 13 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -1,5 +1,6 @@
 using System.Collections.Immutable;
 using System.Reflection;
+using Avalonia.Threading;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Exceptions;
@@ -22,6 +23,7 @@ using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using Drawie.Numerics;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document.Nodes;
 using PixiEditor.ViewModels.Nodes;
@@ -59,6 +61,9 @@ internal class DocumentUpdater
         //TODO: Find a more elegant way to do this
         switch (arbitraryInfo)
         {
+            case ChangeError_Info error:
+                ProcessError(error);
+                break;
             case InvokeAction_PassthroughAction info:
                 ProcessInvokeAction(info);
                 break;
@@ -220,6 +225,14 @@ internal class DocumentUpdater
         }
     }
 
+    private void ProcessError(ChangeError_Info info)
+    {
+        Dispatcher.UIThread.Post(() =>
+        {
+            NoticeDialog.Show(info.Message, "ERROR");
+        });
+    }
+
     private void ProcessInvokeAction(InvokeAction_PassthroughAction info)
     {
         info.Action.Invoke();

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

@@ -211,7 +211,10 @@ internal class DocumentOperationsModule : IDocumentOperations
         bool isFolder = Document.StructureHelper.Find(guidValue) is IFolderHandler;
         if (!isFolder)
         {
-            Internals.ActionAccumulator.AddFinishedActions(new DuplicateLayer_Action(guidValue, Guid.NewGuid()));
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new DuplicateLayer_Action(guidValue, newGuid),
+                new CreateAnimationDataFromLayer_Action(newGuid));
         }
         else
         {

+ 1 - 1
src/PixiEditor/Models/Files/VideoFileType.cs

@@ -21,7 +21,7 @@ internal abstract class VideoFileType : IoFileType
         job?.Report(0, new LocalizedString("WARMING_UP"));
 
         int frameRendered = 0;
-        int totalFrames = document.AnimationDataViewModel.FramesCount;
+        int totalFrames = document.AnimationDataViewModel.GetVisibleFramesCount();
 
         document.RenderFrames(frames, surface =>
         {

+ 18 - 0
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -25,6 +25,7 @@ internal class SceneRenderer
     private bool lastHighResRendering = true;
     private int lastGraphCacheHash = -1;
     private KeyFrameTime lastFrameTime;
+    private Dictionary<Guid, bool> lastFramesVisibility = new();
 
     public SceneRenderer(IReadOnlyDocument trackerDocument, IDocument documentViewModel)
     {
@@ -138,6 +139,23 @@ internal class SceneRenderer
             return true;
         }
 
+        foreach (var frame in DocumentViewModel.AnimationHandler.KeyFrames)
+        {
+            if (lastFramesVisibility.TryGetValue(frame.Id, out var lastVisibility))
+            {
+                if (frame.IsVisible != lastVisibility)
+                {
+                    lastFramesVisibility[frame.Id] = frame.IsVisible;
+                    return true;
+                }
+            }
+            else
+            {
+                lastFramesVisibility[frame.Id] = frame.IsVisible;
+                return true;
+            }
+        }
+
         if (!renderInDocumentSize)
         {
             double lengthDiff = target.LocalClipBounds.Size.Length - cachedTexture.DrawingSurface.LocalClipBounds.Size.Length;

+ 5 - 0
src/PixiEditor/Models/Serialization/Factories/FontFamilySerializationFactory.cs

@@ -11,6 +11,11 @@ public class FontFamilySerializationFactory : SerializationFactory<byte[], FontF
     {
         ByteBuilder builder = new ByteBuilder();
 
+        if (original.Name == null)
+        {
+            original = new FontFamilyName(FontLibrary.DefaultFontFamily.Name) { FontUri = original.FontUri };
+        }
+
         builder.AddString(original.Name);
         builder.AddBool(original.FontUri?.IsFile ?? false);
         if (original.FontUri?.IsFile ?? false)

+ 2 - 2
src/PixiEditor/Styles/Templates/KeyFrame.axaml

@@ -15,10 +15,10 @@
                             Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 15"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
-                            <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5"
+                            <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="15"
                                    Cursor="SizeWestEast" Background="Transparent" ZIndex="1" />
                             <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft"
-                                   Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1" />
+                                   Width="15" Cursor="SizeWestEast" Background="Transparent" ZIndex="1" />
                         </Grid>
                     </Border>
 

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

@@ -421,4 +421,19 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
         this.keyFrames = new KeyFrameCollection(layerKeyFrames);
         OnPropertyChanged(nameof(KeyFrames));
     }
+
+    public int GetFirstVisibleFrame()
+    {
+        return keyFrames.Count > 0 ? keyFrames.Where(x => x.IsVisible).Min(x => x.StartFrameBindable) : 0;
+    }
+
+    public int GetLastVisibleFrame()
+    {
+        return keyFrames.Count > 0 ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable) : 0;
+    }
+
+    public int GetVisibleFramesCount()
+    {
+        return GetLastVisibleFrame() - GetFirstVisibleFrame();
+    }
 }

+ 14 - 6
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -185,15 +185,23 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
     }
 
-    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE",
-        CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
+    [Command.Basic("PixiEditor.Document.DeleteSelected", "DELETE_SELECTED", "DELETE_SELECTED_DESCRIPTIVE",
+        Key = Key.Delete,
         ShortcutContexts = [typeof(ViewportWindowViewModel)],
         Icon = PixiPerfectIcons.Eraser,
-        MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
-    public void DeletePixels()
+        MenuItemPath = "EDIT/DELETE_SELECTED", MenuItemOrder = 6, AnalyticsTrack = true)]
+    public void DeleteSelected()
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument
-            .AnimationDataViewModel.ActiveFrameBindable);
+        if (ActiveDocument.SelectionPathBindable is { IsEmpty: false })
+        {
+            Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument
+                .AnimationDataViewModel.ActiveFrameBindable);
+        }
+        else
+        {
+            var selectedMembers = ActiveDocument?.GetSelectedMembers();
+            Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteStructureMembers(selectedMembers);
+        }
     }
 
 

+ 7 - 7
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -978,8 +978,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (token.IsCancellationRequested)
             return [];
 
-        int firstFrame = AnimationDataViewModel.FirstFrame;
-        int lastFrame = AnimationDataViewModel.LastFrame;
+        int firstFrame = AnimationDataViewModel.GetFirstVisibleFrame();
+        int lastFrame = AnimationDataViewModel.GetLastVisibleFrame();
 
         int framesCount = lastFrame - firstFrame;
 
@@ -1021,8 +1021,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return;
 
-        int firstFrame = AnimationDataViewModel.FirstFrame;
-        int framesCount = AnimationDataViewModel.FramesCount;
+        int firstFrame = AnimationDataViewModel.GetFirstVisibleFrame();
+        int framesCount = AnimationDataViewModel.GetLastVisibleFrame();
         int lastFrame = firstFrame + framesCount;
 
         int activeFrame = AnimationDataViewModel.ActiveFrameBindable;
@@ -1049,12 +1049,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     {
         var keyFrames = AnimationDataViewModel.KeyFrames;
         int firstFrame = 0;
-        int lastFrame = AnimationDataViewModel.FramesCount;
+        int lastFrame = AnimationDataViewModel.GetVisibleFramesCount();
 
         if (keyFrames.Count > 0)
         {
-            firstFrame = keyFrames.Min(x => x.StartFrameBindable);
-            lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
+            firstFrame = AnimationDataViewModel.GetFirstVisibleFrame();
+            lastFrame = AnimationDataViewModel.GetLastVisibleFrame();
         }
 
         for (int i = firstFrame; i < lastFrame; i++)

+ 7 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/KernelFilterNodeViewModel.cs

@@ -4,4 +4,10 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
 [NodeViewModel("KERNEL_FILTER_NODE", "FILTERS", "\uE80F")]
-internal class KernelFilterNodeViewModel : NodeViewModel<KernelFilterNode>;
+internal class KernelFilterNodeViewModel : NodeViewModel<KernelFilterNode>
+{
+    public override void OnInitialized()
+    {
+
+    }
+}

+ 1 - 1
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -52,7 +52,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
     public string NodeNameBindable
     {
-        get => nodeNameBindable ?? DisplayName;
+        get => nodeNameBindable ?? DisplayName.Key;
         set
         {
             if (!Document.BlockingUpdateableChangeActive)

+ 3 - 2
src/PixiEditor/Views/Animations/KeyFrame.cs

@@ -109,7 +109,8 @@ internal class KeyFrame : TemplatedControl
         {
             return;
         }
-        
+
+        e.PreventGestureRecognition();
         e.Pointer.Capture(sender as IInputElement);
         e.Handled = true;
     }
@@ -146,7 +147,7 @@ internal class KeyFrame : TemplatedControl
             }
             
             int oldStartFrame = Item.StartFrameBindable;
-            Item.ChangeFrameLength(frame, Item.DurationBindable + oldStartFrame - frame + 1);
+            Item.ChangeFrameLength(frame, Item.DurationBindable + oldStartFrame - frame);
         }
         
         e.Handled = true;

+ 10 - 6
src/PixiEditor/Views/Animations/Timeline.cs

@@ -378,6 +378,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
     private void KeyFramePressed(PointerPressedEventArgs? e)
     {
+        e.PreventGestureRecognition(); // Prevents digital pen losing capture when dragging
         shouldShiftSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
         shouldClearNextSelection = !shouldShiftSelect && !e.KeyModifiers.HasFlag(KeyModifiers.Control);
         KeyFrame target = null;
@@ -473,7 +474,6 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             () =>
             {
                 newOffsetX = Math.Clamp(newOffsetX, 0, _timelineKeyFramesScroll.ScrollBarMaximum.X);
-
                 ScrollOffset = new Vector(newOffsetX, 0);
             }, DispatcherPriority.Render);
 
@@ -488,14 +488,16 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         }
 
         var mouseButton = e.GetMouseButton(content);
+        e.PreventGestureRecognition();
 
-        if (mouseButton == MouseButton.Left)
+        if (mouseButton == MouseButton.Left && !e.KeyModifiers.HasFlag(KeyModifiers.Control))
         {
             _selectionRectangle.IsVisible = true;
             _selectionRectangle.Width = 0;
             _selectionRectangle.Height = 0;
         }
-        else if (mouseButton == MouseButton.Middle)
+        else if (mouseButton == MouseButton.Middle ||
+                 (mouseButton == MouseButton.Left && e.KeyModifiers.HasFlag(KeyModifiers.Control)))
         {
             Cursor = new Cursor(StandardCursorType.SizeAll);
             e.Pointer.Capture(content);
@@ -517,11 +519,13 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             return;
         }
 
-        if (e.GetCurrentPoint(content).Properties.IsLeftButtonPressed)
+        if (e.GetCurrentPoint(content).Properties.IsLeftButtonPressed && !e.KeyModifiers.HasFlag(KeyModifiers.Control))
         {
             HandleMoveSelection(e, content);
         }
-        else if (e.GetCurrentPoint(content).Properties.IsMiddleButtonPressed)
+        else if (e.GetCurrentPoint(content).Properties.IsMiddleButtonPressed ||
+                 (e.GetCurrentPoint(content).Properties.IsLeftButtonPressed &&
+                  e.KeyModifiers.HasFlag(KeyModifiers.Control)))
         {
             HandleTimelinePan(e, content);
         }
@@ -563,7 +567,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             var translated = frame.TranslatePoint(new Point(0, 0), _contentGrid);
             Rect frameBounds = new Rect(translated.Value.X, translated.Value.Y, frame.Bounds.Width,
                 frame.Bounds.Height);
-            if (bounds.Contains(frameBounds))
+            if (bounds.Intersects(frameBounds))
             {
                 SelectKeyFrame(frame.Item, false);
             }

+ 2 - 0
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml

@@ -44,9 +44,11 @@
                             </Grid.RowDefinitions>
                             <TextBlock ui1:Translator.Key="ROWS" Grid.Row="0" Grid.Column="0"/>
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="0"
+                                               Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetRows, Mode=TwoWay}" />
                             <TextBlock ui1:Translator.Key="COLUMNS" Grid.Row="1" Grid.Column="0"/>
                             <input:NumberInput Min="0" Width="50" Grid.Column="1" Grid.Row="1"
+                                               Decimals="0"
                                                Value="{Binding ElementName=saveFilePopup, Path=SpriteSheetColumns, Mode=TwoWay}" />
                         </Grid>
                     </TabItem>

+ 1 - 1
src/PixiEditor/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -178,7 +178,7 @@ internal partial class ExportFilePopup : PixiEditorPopup
         };
         videoPreviewTimer.Tick += OnVideoPreviewTimerOnTick;
 
-        int framesCount = document.AnimationDataViewModel.FramesCount;
+        int framesCount = document.AnimationDataViewModel.GetVisibleFramesCount();
 
         var (rows, columns) = SpriteSheetUtility.CalculateGridDimensionsAuto(framesCount);
         SpriteSheetColumns = columns;

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

@@ -418,12 +418,27 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
 
         DocumentViewModel? oldDoc = e.OldValue.Value;
 
+        if (oldDoc != null)
+        {
+            oldDoc.SizeChanged -= viewport.OnDocumentSizeChanged;
+        }
+
         DocumentViewModel? newDoc = e.NewValue.Value;
 
+        if (newDoc != null)
+        {
+            newDoc.SizeChanged += viewport.OnDocumentSizeChanged;
+        }
+
         oldDoc?.Operations.RemoveViewport(viewport.GuidValue);
         newDoc?.Operations.AddOrUpdateViewport(viewport.GetLocation());
     }
 
+    private void OnDocumentSizeChanged(object? sender, DocumentSizeChangedEventArgs documentSizeChangedEventArgs)
+    {
+        scene.CenterContent(documentSizeChangedEventArgs.NewSize);
+    }
+
     private ChunkResolution CalculateResolution()
     {
         VecD densityVec = Dimensions.Divide(RealDimensions);

+ 5 - 2
src/PixiEditor/Views/Overlays/TextOverlay/Caret.cs

@@ -26,7 +26,7 @@ internal class Caret : IDisposable
     public VecF[] GlyphPositions { get; set; }
     public VecD Offset { get; set; }
     public float[] GlyphWidths { get; set; }
-    public float CaretWidth { get; set; } = 1;
+    public float CaretWidth { get; set; } = 0.5f;
 
     private Paint paint = new Paint() { Color = Colors.White, Style = PaintStyle.StrokeAndFill, StrokeWidth = 3 };
 
@@ -63,7 +63,10 @@ internal class Caret : IDisposable
 
         paint.Color = new Color(Colors.White.R, Colors.White.G, Colors.White.B, (byte)(visible ? 255 : 0));
 
-        canvas.DrawLine(from, to, paint);
+        VecD strokeOffset = new VecD(CaretWidth / 2, 0);
+        canvas.DrawLine(from - strokeOffset, to - strokeOffset, paint);
+        paint.Color = new Color(Colors.Black.R, Colors.Black.G, Colors.Black.B, (byte)(visible ? 255 : 0));
+        canvas.DrawLine(from + strokeOffset, to + strokeOffset, paint);
     }
 
     public void Dispose()

+ 1 - 1
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -237,7 +237,7 @@ internal class TextOverlay : Overlay
         caret.GlyphWidths = glyphWidths;
         caret.Offset = Position;
 
-        caret.CaretWidth = 3f / (float)ZoomScale;
+        caret.CaretWidth = 2f / (float)ZoomScale;
         caret.Render(context);
     }
 

+ 2 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -34,6 +34,7 @@ using PixiEditor.Views.Visuals;
 using Bitmap = Drawie.Backend.Core.Surfaces.Bitmap;
 using Color = Drawie.Backend.Core.ColorsImpl.Color;
 using Point = Avalonia.Point;
+using TileMode = Drawie.Backend.Core.Surfaces.TileMode;
 
 namespace PixiEditor.Views.Rendering;
 
@@ -281,7 +282,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         {
             Shader = Shader.CreateBitmap(
                 checkerBitmap,
-                ShaderTileMode.Repeat, ShaderTileMode.Repeat,
+                TileMode.Repeat, TileMode.Repeat,
                 Matrix3X3.CreateScale(checkerScale, checkerScale)),
             FilterQuality = FilterQuality.None
         };