Browse Source

Merge pull request #761 from PixiEditor/more-effects

More effects
Krzysztof Krysiński 5 months ago
parent
commit
5fc211afc5
35 changed files with 624 additions and 64 deletions
  1. 12 3
      src/ChunkyImageLib/Operations/ImageOperation.cs
  2. 1 1
      src/Drawie
  3. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  4. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs
  5. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs
  6. 3 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs
  7. 30 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ShadowNode.cs
  8. 37 7
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  9. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs
  10. 6 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/MatrixNode.cs
  11. 26 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/OffsetNode.cs
  12. 43 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/RotateNode.cs
  13. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ScaleNode.cs
  14. 26 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/SkewNode.cs
  15. 50 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs
  16. 14 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs
  17. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  18. 83 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/TileNode.cs
  19. 4 0
      src/PixiEditor.UI.Common/Accents/Base.axaml
  20. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  21. 1 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  22. 1 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  23. 21 1
      src/PixiEditor/Data/Localization/Languages/en.json
  24. 1 1
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  25. 2 3
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  26. 56 37
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  27. 0 1
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  28. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/DocumentInfoNodeViewModel.cs
  29. 10 0
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ShadowNodeViewModel.cs
  30. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/OffsetNodeViewModel.cs
  31. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/RotateNodeViewModel.cs
  32. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/ScaleNodeViewModel.cs
  33. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/SkewNodeViewModel.cs
  34. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/Matrix/TransformNodeViewModel.cs
  35. 8 0
      src/PixiEditor/ViewModels/Document/Nodes/TileNodeViewModel.cs

+ 12 - 3
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -87,15 +87,20 @@ internal class ImageOperation : IMirroredDrawOperation
         var scaleTrans = Matrix3X3.CreateScaleTranslation(scaleMult, scaleMult, (float)trans.X * scaleMult, (float)trans.Y * scaleMult);
         var scaleTrans = Matrix3X3.CreateScaleTranslation(scaleMult, scaleMult, (float)trans.X * scaleMult, (float)trans.Y * scaleMult);
         var finalMatrix = Matrix3X3.Concat(scaleTrans, transformMatrix);
         var finalMatrix = Matrix3X3.Concat(scaleTrans, transformMatrix);
 
 
+        using var snapshot = toPaint.DrawingSurface.Snapshot();
+        ShapeCorners chunkCorners = new ShapeCorners(new RectD(VecD.Zero, targetChunk.PixelSize));
+        RectD rect = chunkCorners.WithMatrix(finalMatrix.Invert()).AABBBounds;
+
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.Save();
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
         targetChunk.Surface.DrawingSurface.Canvas.SetMatrix(finalMatrix);
-        targetChunk.Surface.DrawingSurface.Canvas.DrawSurface(toPaint.DrawingSurface, 0, 0, customPaint);
+        targetChunk.Surface.DrawingSurface.Canvas.DrawImage(snapshot, rect, rect, customPaint);
         targetChunk.Surface.DrawingSurface.Canvas.Restore();
         targetChunk.Surface.DrawingSurface.Canvas.Restore();
     }
     }
 
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
     {
-        return new AffectedArea(OperationHelper.FindChunksTouchingQuadrilateral(corners, ChunkPool.FullChunkSize), (RectI)corners.AABBBounds.RoundOutwards());
+        return new AffectedArea(OperationHelper.FindChunksTouchingQuadrilateral(corners, ChunkPool.FullChunkSize),
+            (RectI)corners.AABBBounds.RoundOutwards());
     }
     }
 
 
     public void Dispose()
     public void Dispose()
@@ -110,18 +115,22 @@ internal class ImageOperation : IMirroredDrawOperation
         if (verAxisX is not null && horAxisY is not null)
         if (verAxisX is not null && horAxisY is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((double)verAxisX).AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
+            (corners.AsMirroredAcrossVerAxis((double)verAxisX).AsMirroredAcrossHorAxis((double)horAxisY), toPaint,
+                customPaint, imageWasCopied);
         }
         }
+
         if (verAxisX is not null)
         if (verAxisX is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
                 (corners.AsMirroredAcrossVerAxis((double)verAxisX), toPaint, customPaint, imageWasCopied);
                 (corners.AsMirroredAcrossVerAxis((double)verAxisX), toPaint, customPaint, imageWasCopied);
         }
         }
+
         if (horAxisY is not null)
         if (horAxisY is not null)
         {
         {
             return new ImageOperation
             return new ImageOperation
                 (corners.AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
                 (corners.AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
         }
         }
+
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
     }
     }
 }
 }

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit d23a32dd0499ba3f3b9881f48aa7a69395bba329
+Subproject commit 157b999633b2c08dd5bd9bd09e48adea59075481

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

@@ -364,7 +364,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// <param name="guid">The <see cref="StructureNode.Id"/> of the member</param>
     /// <param name="guid">The <see cref="StructureNode.Id"/> of the member</param>
     public List<StructureNode> FindMemberPath(Guid guid)
     public List<StructureNode> FindMemberPath(Guid guid)
     {
     {
-        if (NodeGraph.OutputNode == null) return [];
+        //if (NodeGraph.OutputNode == null) return [];
 
 
         var list = new List<StructureNode>();
         var list = new List<StructureNode>();
         var targetNode = FindNode(guid);
         var targetNode = FindNode(guid);

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CreateImageNode.cs

@@ -20,6 +20,8 @@ public class CreateImageNode : Node, IPreviewRenderable
 
 
     public RenderInputProperty Content { get; }
     public RenderInputProperty Content { get; }
 
 
+    public InputProperty<VecD> ContentOffset { get; }
+
     public RenderOutputProperty RenderOutput { get; }
     public RenderOutputProperty RenderOutput { get; }
 
 
     private TextureCache textureCache = new();
     private TextureCache textureCache = new();
@@ -30,6 +32,7 @@ public class CreateImageNode : Node, IPreviewRenderable
         Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32)).WithRules(v => v.Min(VecI.One));
         Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32)).WithRules(v => v.Min(VecI.One));
         Fill = CreateInput(nameof(Fill), "FILL", Colors.Transparent);
         Fill = CreateInput(nameof(Fill), "FILL", Colors.Transparent);
         Content = CreateRenderInput(nameof(Content), "CONTENT");
         Content = CreateRenderInput(nameof(Content), "CONTENT");
+        ContentOffset = CreateInput(nameof(ContentOffset), "CONTENT_OFFSET", VecD.Zero);
         RenderOutput = CreateRenderOutput("RenderOutput", "RENDER_OUTPUT", () => new Painter(OnPaint));
         RenderOutput = CreateRenderOutput("RenderOutput", "RENDER_OUTPUT", () => new Painter(OnPaint));
     }
     }
 
 
@@ -58,6 +61,8 @@ public class CreateImageNode : Node, IPreviewRenderable
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
         RenderContext ctx = new RenderContext(surface.DrawingSurface, context.FrameTime, context.ChunkResolution,
             context.DocumentSize, context.ProcessingColorSpace);
             context.DocumentSize, context.ProcessingColorSpace);
 
 
+        surface.DrawingSurface.Canvas.Translate((float)-ContentOffset.Value.X, (float)-ContentOffset.Value.Y);
+
         Content.Value?.Paint(ctx, surface.DrawingSurface);
         Content.Value?.Paint(ctx, surface.DrawingSurface);
 
 
         surface.DrawingSurface.Canvas.RestoreToCount(saved);
         surface.DrawingSurface.Canvas.RestoreToCount(saved);

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/DocumentInfoNode.cs

@@ -0,0 +1,28 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("DocumentInfo")]
+public class DocumentInfoNode : Node
+{
+    public OutputProperty<VecI> Size { get; }
+    public OutputProperty<VecD> Center { get; }
+
+    public DocumentInfoNode()
+    {
+        Size = CreateOutput("Size", "SIZE", new VecI(0, 0));
+        Center = CreateOutput("Center", "CENTER", new VecD(0, 0));
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        Size.Value = context.DocumentSize;
+        Center.Value = new VecD(context.DocumentSize.X / 2.0, context.DocumentSize.Y / 2.0);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new DocumentInfoNode();
+    }
+}

+ 3 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ApplyFilterNode.cs

@@ -25,12 +25,12 @@ public class ApplyFilterNode : RenderNode, IRenderInput
 
 
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
     {
     {
-        if (Background.Value == null || Filter.Value == null || _paint == null)
+        if (_paint == null)
             return;
             return;
 
 
         _paint.SetFilters(Filter.Value);
         _paint.SetFilters(Filter.Value);
         int layer = surface.Canvas.SaveLayer(_paint);
         int layer = surface.Canvas.SaveLayer(_paint);
-        Background.Value.Paint(context, surface);
+        Background.Value?.Paint(context, surface);
 
 
         surface.Canvas.RestoreToCount(layer);
         surface.Canvas.RestoreToCount(layer);
     }
     }
@@ -43,11 +43,8 @@ public class ApplyFilterNode : RenderNode, IRenderInput
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
     public override bool RenderPreview(DrawingSurface renderOn, RenderContext context,
         string elementToRenderName)
         string elementToRenderName)
     {
     {
-        if (Background.Value == null)
-            return false;
-
         int layer = renderOn.Canvas.SaveLayer(_paint);
         int layer = renderOn.Canvas.SaveLayer(_paint);
-        Background.Value.Paint(context, renderOn);
+        Background.Value?.Paint(context, renderOn);
         renderOn.Canvas.RestoreToCount(layer);
         renderOn.Canvas.RestoreToCount(layer);
 
 
         return true;
         return true;

+ 30 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FilterNodes/ShadowNode.cs

@@ -0,0 +1,30 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+
+[NodeInfo("Shadow")]
+public class ShadowNode : FilterNode
+{
+    public InputProperty<VecD> Offset { get; }
+    public InputProperty<VecD> Sigma { get; }
+    public InputProperty<Color> Color { get; }
+
+    public ShadowNode()
+    {
+        Offset = CreateInput("Offset", "OFFSET", new VecD(5, 5));
+        Sigma = CreateInput("Radius", "RADIUS", new VecD(5, 5));
+        Color = CreateInput("Color", "COLOR", Colors.Black);
+    }
+
+    protected override ImageFilter? GetImageFilter()
+    {
+        return ImageFilter.CreateDropShadow((float)Offset.Value.X, (float)Offset.Value.Y, (float)Sigma.Value.X, (float)Sigma.Value.Y, Color.Value, null);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ShadowNode();
+    }
+}

+ 37 - 7
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -34,7 +34,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     {
     {
         if (keyFrames.Count == 0)
         if (keyFrames.Count == 0)
         {
         {
-            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size, colorSpace) });
+            keyFrames.Add(
+                new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size, colorSpace) });
         }
         }
 
 
         this.startSize = size;
         this.startSize = size;
@@ -125,13 +126,39 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
             if (keyFrame != null)
             if (keyFrame != null)
             {
             {
-                return (RectD?)GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid).FindTightCommittedBounds();
+                var kf = GetLayerImageByKeyFrameGuid(keyFrame.KeyFrameGuid);
+                if (kf == null)
+                {
+                    return null;
+                }
+
+                RectI? bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+                if (bounds.HasValue)
+                {
+                    return new RectD(bounds.Value.X, bounds.Value.Y,
+                        Math.Min(bounds.Value.Width, kf.CommittedSize.X),
+                        Math.Min(bounds.Value.Height, kf.CommittedSize.Y));
+                }
             }
             }
         }
         }
 
 
         try
         try
         {
         {
-            return (RectD?)GetLayerImageAtFrame(frame).FindTightCommittedBounds();
+            var kf = GetLayerImageAtFrame(frame);
+            if (kf == null)
+            {
+                return null;
+            }
+
+            var bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+            if (bounds.HasValue)
+            {
+                return new RectD(bounds.Value.X, bounds.Value.Y,
+                    Math.Min(bounds.Value.Width, kf.CommittedSize.X),
+                    Math.Min(bounds.Value.Height, kf.CommittedSize.Y));
+            }
+
+            return null;
         }
         }
         catch (ObjectDisposedException)
         catch (ObjectDisposedException)
         {
         {
@@ -197,7 +224,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
         {
         {
             return keyFrames[0];
             return keyFrames[0];
         }
         }
-        
+
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         if (imageFrame?.Data is not ChunkyImage)
         if (imageFrame?.Data is not ChunkyImage)
         {
         {
@@ -229,8 +256,10 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     {
     {
         var image = new ImageLayerNode(startSize, colorSpace)
         var image = new ImageLayerNode(startSize, colorSpace)
         {
         {
-            MemberName = this.MemberName, LockTransparency = this.LockTransparency,
-            ClipToPreviousMember = this.ClipToPreviousMember, EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
+            MemberName = this.MemberName,
+            LockTransparency = this.LockTransparency,
+            ClipToPreviousMember = this.ClipToPreviousMember,
+            EmbeddedMask = this.EmbeddedMask?.CloneFromCommitted()
         };
         };
 
 
         image.keyFrames.Clear();
         image.keyFrames.Clear();
@@ -258,7 +287,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
     void IReadOnlyImageNode.ForEveryFrame(Action<IReadOnlyChunkyImage> action) => ForEveryFrame(action);
 
 
-    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime, ColorSpace processColorSpace)
+    public override void RenderChunk(VecI chunkPos, ChunkResolution resolution, KeyFrameTime frameTime,
+        ColorSpace processColorSpace)
     {
     {
         base.RenderChunk(chunkPos, resolution, frameTime, processColorSpace);
         base.RenderChunk(chunkPos, resolution, frameTime, processColorSpace);
 
 

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

@@ -0,0 +1,51 @@
+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 PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
+{
+    public RenderInputProperty Background { get; }
+    public InputProperty<Matrix3X3> Input { get; }
+    public OutputProperty<Matrix3X3> Matrix { get; }
+
+    private Paint? paint;
+
+    public Matrix3X3BaseNode()
+    {
+        Background = CreateRenderInput("Background", "IMAGE");
+        Input = CreateInput("Input", "INPUT_MATRIX", Matrix3X3.Identity);
+        Matrix = CreateOutput("Matrix", "OUTPUT_MATRIX", Matrix3X3.Identity);
+        Output.FirstInChain = null;
+        AllowHighDpiRendering = true;
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        Matrix.Value = CalculateMatrix(Input.Value);
+        if (Background.Value == null)
+            return;
+
+        paint ??= new();
+        base.OnExecute(context);
+    }
+
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (paint == null)
+            return;
+
+        int layer = surface.Canvas.Save();
+
+        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(Matrix.Value));
+        Background.Value?.Paint(context, surface);
+
+        surface.Canvas.RestoreToCount(layer);
+    }
+
+    protected abstract Matrix3X3 CalculateMatrix(Matrix3X3 input);
+}

+ 6 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/MatrixNode.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+public class MatrixNode
+{
+
+}

+ 26 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/OffsetNode.cs

@@ -0,0 +1,26 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("Offset")]
+public class OffsetNode : Matrix3X3BaseNode
+{
+    public InputProperty<VecD> Translation { get; }
+
+    public OffsetNode()
+    {
+        Translation = CreateInput("Offset", "OFFSET", VecD.Zero);
+    }
+
+    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    {
+        Matrix3X3 matrix = Matrix3X3.CreateTranslation((float)(Translation.Value.X), (float)(Translation.Value.Y));
+        return input.PostConcat(matrix);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new OffsetNode();
+    }
+}

+ 43 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/RotateNode.cs

@@ -0,0 +1,43 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("Rotate")]
+public class RotateNode : Matrix3X3BaseNode
+{
+    public InputProperty<RotationType> RotationType { get; }
+    public InputProperty<double> Angle { get; }
+    public InputProperty<VecD> Center { get; }
+
+    public RotateNode()
+    {
+        RotationType = CreateInput("RotationType", "UNIT", Nodes.Matrix.RotationType.Degrees);
+        Angle = CreateInput("Angle", "ANGLE", 0.0);
+        Center = CreateInput("Center", "CENTER", new VecD(0, 0));
+    }
+
+    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    {
+        VecD scaledCenter = new VecD(Center.Value.X, Center.Value.Y);
+        Matrix3X3 rotated = RotationType.Value switch
+        {
+            Nodes.Matrix.RotationType.Degrees => Matrix3X3.CreateRotationDegrees((float)Angle.Value, (float)scaledCenter.X, (float)scaledCenter.Y),
+            Nodes.Matrix.RotationType.Radians => Matrix3X3.CreateRotation((float)Angle.Value, (float)scaledCenter.X, (float)scaledCenter.Y),
+            _ => throw new ArgumentOutOfRangeException()
+        };
+
+        return input.PostConcat(rotated);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new RotateNode();
+    }
+}
+
+public enum RotationType
+{
+    Degrees,
+    Radians
+}

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/ScaleNode.cs

@@ -0,0 +1,28 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("Scale")]
+public class ScaleNode : Matrix3X3BaseNode
+{
+    public InputProperty<VecD> Scale { get; }
+    public InputProperty<VecD> Center { get; }
+
+    public ScaleNode()
+    {
+        Scale = CreateInput("Scale", "SCALE", new VecD(1, 1));
+        Center = CreateInput("Center", "CENTER", new VecD(0, 0));
+    }
+
+    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    {
+        Matrix3X3 scaled = Matrix3X3.CreateScale((float)Scale.Value.X, (float)Scale.Value.Y, (float)Center.Value.X, (float)Center.Value.Y);
+        return input.PostConcat(scaled);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ScaleNode();
+    }
+}

+ 26 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/SkewNode.cs

@@ -0,0 +1,26 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("Skew")]
+public class SkewNode : Matrix3X3BaseNode
+{
+    public InputProperty<VecD> Skew { get; }
+
+    public SkewNode()
+    {
+        Skew = CreateInput("Skew", "SKEW", VecD.Zero);
+    }
+
+    protected override Matrix3X3 CalculateMatrix(Matrix3X3 input)
+    {
+        Matrix3X3 matrix = Matrix3X3.CreateSkew((float)Skew.Value.X, (float)Skew.Value.Y);
+        return input.PostConcat(matrix);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new SkewNode();
+    }
+}

+ 50 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/TransformNode.cs

@@ -0,0 +1,50 @@
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+
+[NodeInfo("Transform")]
+public class TransformNode : RenderNode, IRenderInput
+{
+    public RenderInputProperty Background { get; }
+    public InputProperty<Matrix3X3> Matrix { get; }
+
+    public TransformNode()
+    {
+        Background = CreateRenderInput("Background", "IMAGE");
+        Matrix = CreateInput("Matrix", "INPUT_MATRIX", Matrix3X3.Identity);
+
+        Output.FirstInChain = null;
+    }
+
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (Background.Value == null)
+            return;
+
+        int layer = surface.Canvas.Save();
+
+        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.PostConcat(Matrix.Value));
+        Background.Value?.Paint(context, surface);
+
+        surface.Canvas.RestoreToCount(layer);
+    }
+
+    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        return null;
+    }
+
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        return false;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TransformNode();
+    }
+}

+ 14 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/RenderNode.cs

@@ -17,6 +17,8 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
 
     private TextureCache textureCache = new();
     private TextureCache textureCache = new();
 
 
+    private VecI lastDocumentSize = VecI.Zero;
+
     public RenderNode()
     public RenderNode()
     {
     {
         Painter painter = new Painter(Paint);
         Painter painter = new Painter(Paint);
@@ -34,6 +36,8 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
                 output.ChainToPainterValue();
                 output.ChainToPainterValue();
             }
             }
         }
         }
+
+        lastDocumentSize = context.DocumentSize;
     }
     }
 
 
     private void Paint(RenderContext context, DrawingSurface surface)
     private void Paint(RenderContext context, DrawingSurface surface)
@@ -58,10 +62,17 @@ public abstract class RenderNode : Node, IPreviewRenderable, IHighDpiRenderNode
 
 
     protected abstract void OnPaint(RenderContext context, DrawingSurface surface);
     protected abstract void OnPaint(RenderContext context, DrawingSurface surface);
 
 
-    public abstract RectD? GetPreviewBounds(int frame, string elementToRenderName = "");
+    public virtual RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+    }
 
 
-    public abstract bool RenderPreview(DrawingSurface renderOn, RenderContext context,
-        string elementToRenderName);
+    public virtual bool RenderPreview(DrawingSurface renderOn, RenderContext context,
+        string elementToRenderName)
+    {
+        OnPaint(context, renderOn);
+        return true;
+    }
 
 
     protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     protected Texture RequestTexture(int id, VecI size, ColorSpace processingCs, bool clear = true)
     {
     {

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -39,6 +39,10 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
     public RenderOutputProperty FilterlessOutput { get; }
     public RenderOutputProperty FilterlessOutput { get; }
     public RenderOutputProperty RawOutput { get; }
     public RenderOutputProperty RawOutput { get; }
 
 
+    public OutputProperty<VecD> TightSize { get; }
+    public OutputProperty<VecD> CanvasPosition { get; }
+    public OutputProperty<VecD> CenterPosition { get; }
+
     public ChunkyImage? EmbeddedMask { get; set; }
     public ChunkyImage? EmbeddedMask { get; set; }
 
 
     protected Texture renderedMask;
     protected Texture renderedMask;
@@ -94,9 +98,33 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
 
         RawOutput = CreateRenderOutput(RawOutputPropertyName, "RAW_LAYER_OUTPUT", () => rawPainter);
         RawOutput = CreateRenderOutput(RawOutputPropertyName, "RAW_LAYER_OUTPUT", () => rawPainter);
 
 
+        CanvasPosition = CreateOutput<VecD>("CanvasPosition", "CANVAS_POSITION", VecD.Zero);
+        CenterPosition = CreateOutput<VecD>("CenterPosition", "CENTER_POSITION", VecD.Zero);
+        TightSize = CreateOutput<VecD>("Size", "SIZE", VecD.Zero);
+
         MemberName = DefaultMemberName;
         MemberName = DefaultMemberName;
     }
     }
 
 
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+
+        if (TightSize.Connections.Count > 0)
+        {
+            TightSize.Value = GetTightBounds(context.FrameTime)?.Size ?? VecD.Zero;
+        }
+
+        if (CanvasPosition.Connections.Count > 0)
+        {
+            CanvasPosition.Value = GetTightBounds(context.FrameTime)?.TopLeft ?? VecD.Zero;
+        }
+
+        if (CenterPosition.Connections.Count > 0)
+        {
+            CenterPosition.Value = GetTightBounds(context.FrameTime)?.Center ?? VecD.Zero;
+        }
+    }
+
     protected override void OnPaint(RenderContext context, DrawingSurface renderTarget)
     protected override void OnPaint(RenderContext context, DrawingSurface renderTarget)
     {
     {
         if (Output.Connections.Count > 0)
         if (Output.Connections.Count > 0)

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

@@ -0,0 +1,83 @@
+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.Interfaces;
+using PixiEditor.ChangeableDocument.Helpers;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("Tile")]
+public class TileNode : RenderNode
+{
+    public InputProperty<Texture> Image { get; }
+    public InputProperty<ShaderTileMode> TileModeX { get; }
+    public InputProperty<ShaderTileMode> TileModeY { get; }
+    public InputProperty<Matrix3X3> Matrix { get; }
+
+    private Image lastImage;
+    private Shader tileShader;
+    private Paint paint;
+
+    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);
+        Matrix = CreateInput<Matrix3X3>("Matrix", "MATRIX", Matrix3X3.Identity);
+
+        Output.FirstInChain = null;
+    }
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+    protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.Inputs;
+
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+
+        lastImage?.Dispose();
+        tileShader?.Dispose();
+        if (paint != null)
+        {
+            paint.Shader = null;
+        }
+
+        if (Image.Value == null)
+            return;
+
+        lastImage = Image.Value.DrawingSurface.Snapshot();
+        tileShader = Shader.CreateImage(lastImage, TileModeX.Value, TileModeY.Value, Matrix.Value);
+
+        paint ??= new();
+        paint.Shader = tileShader;
+    }
+
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (paint == null)
+            return;
+
+        surface.Canvas.DrawRect(0, 0, context.DocumentSize.X, context.DocumentSize.Y, paint);
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TileNode();
+    }
+
+    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        return null;
+    }
+
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        return false;
+    }
+
+}

+ 4 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -60,6 +60,7 @@
             <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
             <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
             <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
             <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
             <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
             <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
+            <Color x:Key="Matrix3X3SocketColor">#ffea4f</Color>
             <GradientStops x:Key="ShapeDataSocketGradient">
             <GradientStops x:Key="ShapeDataSocketGradient">
                 <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
                 <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
                 <GradientStop Offset="0.33" Color="{StaticResource EllipseDataSocketColor}"/>
                 <GradientStop Offset="0.33" Color="{StaticResource EllipseDataSocketColor}"/>
@@ -88,6 +89,7 @@
             <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
             <Color x:Key="ColorCategoryBackgroundColor">#3B665D</Color>
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
             <Color x:Key="EffectsCategoryBackgroundColor">#e36262</Color>
             <Color x:Key="EffectsCategoryBackgroundColor">#e36262</Color>
+            <Color x:Key="MatrixCategoryBackgroundColor">#9169ff</Color>
             
             
             <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
             <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
             <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
             <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
@@ -152,6 +154,7 @@
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
             <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}"/>
             <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}"/>
+            <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}"/>
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
             <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
             <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
             <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
             <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
@@ -180,6 +183,7 @@
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="EffectsCategoryBackgroundBrush" Color="{StaticResource EffectsCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="EffectsCategoryBackgroundBrush" Color="{StaticResource EffectsCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="MatrixCategoryBackgroundBrush" Color="{StaticResource MatrixCategoryBackgroundColor}" />
 
 
             <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}"/>
             <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}"/>
             <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}"/>
             <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}"/>

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


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

@@ -156,6 +156,7 @@
             <system:String x:Key="icon-fullscreen">&#xE98c;</system:String>
             <system:String x:Key="icon-fullscreen">&#xE98c;</system:String>
             <system:String x:Key="icon-outline">&#xE99a;</system:String>
             <system:String x:Key="icon-outline">&#xE99a;</system:String>
             <system:String x:Key="icon-terminal">&#xE99b;</system:String>
             <system:String x:Key="icon-terminal">&#xE99b;</system:String>
+            <system:String x:Key="icon-cone">&#xE99c;</system:String>
 
 
         </ResourceDictionary>
         </ResourceDictionary>
     </Styles.Resources>
     </Styles.Resources>

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

@@ -151,6 +151,7 @@ public static class PixiPerfectIcons
     public const string LinkedPipette = "\uE997";
     public const string LinkedPipette = "\uE997";
     public const string TextUnderline = "\uE998";
     public const string TextUnderline = "\uE998";
     public const string TextRound = "\uE999";
     public const string TextRound = "\uE999";
+    public const string Cone = "\uE99c";
 
 
     public static Stream GetFontStream()
     public static Stream GetFontStream()
     {
     {

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

@@ -868,5 +868,25 @@
   "DISCO_BALL_EXAMPLE": "Disco Ball",
   "DISCO_BALL_EXAMPLE": "Disco Ball",
   "COLOR_SPACE": "Color Space",
   "COLOR_SPACE": "Color Space",
   "PHOTO_EXAMPLES": "Photo",
   "PHOTO_EXAMPLES": "Photo",
-  "MASK_EXAMPLE": "Mask"
+  "MASK_EXAMPLE": "Mask",
+  "SHADOW_NODE": "Shadow Filter",
+  "INPUT_MATRIX": "Input Matrix",
+  "OUTPUT_MATRIX": "Output Matrix",
+  "CENTER": "Center",
+  "CONTENT_OFFSET": "Content Offset",
+  "CANVAS_POSITION": "Canvas Position",
+  "CENTER_POSITION": "Center Position",
+  "TILE_MODE_X": "Tile Mode X",
+  "TILE_MODE_Y": "Tile Mode Y",
+  "TILE_NODE": "Tile",
+  "SKEW": "Skew",
+  "OFFSET_NODE": "Offset",
+  "SKEW_NODE": "Skew",
+  "ROTATION_NODE": "Rotation",
+  "SCALE_NODE": "Scale",
+  "ROTATE_NODE": "Rotate",
+  "TRANSFORM_NODE": "Transform",
+  "UNIT": "Unit",
+  "ANGLE": "Angle",
+  "DOCUMENT_INFO_NODE": "Document Info"
 }
 }

+ 1 - 1
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -143,7 +143,7 @@ internal class ActionAccumulator
                         undoBoundaryPassed || viewportRefreshRequest);
                         undoBoundaryPassed || viewportRefreshRequest);
                 }*/
                 }*/
 
 
-                previewUpdater.UpdatePreviews(undoBoundaryPassed || changeFrameRequest || viewportRefreshRequest,
+                previewUpdater.UpdatePreviews(
                     affectedAreas.ImagePreviewAreas.Keys,
                     affectedAreas.ImagePreviewAreas.Keys,
                     affectedAreas.MaskPreviewAreas.Keys,
                     affectedAreas.MaskPreviewAreas.Keys,
                     affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);
                     affectedAreas.ChangedNodes, affectedAreas.ChangedKeyFrames);

+ 2 - 3
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -209,7 +209,9 @@ internal class AffectedAreasGatherer
     {
     {
         ChangedNodes ??= new List<Guid>();
         ChangedNodes ??= new List<Guid>();
         if (!ChangedNodes.Contains(nodeId))
         if (!ChangedNodes.Contains(nodeId))
+        {
             ChangedNodes.Add(nodeId);
             ChangedNodes.Add(nodeId);
+        }
     }
     }
     
     
     private void AddAllNodesToImagePreviews()
     private void AddAllNodesToImagePreviews()
@@ -331,9 +333,6 @@ internal class AffectedAreasGatherer
     private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     private void AddToImagePreviews(Guid memberGuid, AffectedArea area, bool ignoreSelf = false)
     {
     {
         var path = tracker.Document.FindMemberPath(memberGuid);
         var path = tracker.Document.FindMemberPath(memberGuid);
-        int minCount = ignoreSelf ? 2 : 1;
-        if (path.Count < minCount)
-            return;
         for (int i = ignoreSelf ? 1 : 0; i < path.Count; i++)
         for (int i = ignoreSelf ? 1 : 0; i < path.Count; i++)
         {
         {
             var member = path[i];
             var member = path[i];

+ 56 - 37
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -1,21 +1,13 @@
 #nullable enable
 #nullable enable
 
 
-using System.Diagnostics.CodeAnalysis;
-using ChunkyImageLib;
-using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-using PixiEditor.ChangeableDocument.Rendering;
-using Drawie.Backend.Core;
-using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.ImageData;
-using Drawie.Backend.Core.Surfaces.PaintImpl;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using Drawie.Numerics;
-using PixiEditor.Parser;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
 
 
@@ -33,13 +25,12 @@ internal class MemberPreviewUpdater
         AnimationKeyFramePreviewRenderer = new AnimationKeyFramePreviewRenderer(internals);
         AnimationKeyFramePreviewRenderer = new AnimationKeyFramePreviewRenderer(internals);
     }
     }
 
 
-    public void UpdatePreviews(bool rerenderPreviews, IEnumerable<Guid> membersToUpdate,
+    public void UpdatePreviews(IEnumerable<Guid> membersToUpdate,
         IEnumerable<Guid> masksToUpdate, IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate)
         IEnumerable<Guid> masksToUpdate, IEnumerable<Guid> nodesToUpdate, IEnumerable<Guid> keyFramesToUpdate)
     {
     {
-        if (!rerenderPreviews)
-        {
+        if (!membersToUpdate.Any() && !masksToUpdate.Any() && !nodesToUpdate.Any() &&
+            !keyFramesToUpdate.Any())
             return;
             return;
-        }
 
 
         UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate);
         UpdatePreviewPainters(membersToUpdate, masksToUpdate, nodesToUpdate, keyFramesToUpdate);
     }
     }
@@ -243,45 +234,73 @@ internal class MemberPreviewUpdater
         if (outputNode is null)
         if (outputNode is null)
             return;
             return;
 
 
-        var executionQueue =
+        var allNodes =
             internals.Tracker.Document.NodeGraph
             internals.Tracker.Document.NodeGraph
                 .AllNodes; //internals.Tracker.Document.NodeGraph.CalculateExecutionQueue(outputNode);
                 .AllNodes; //internals.Tracker.Document.NodeGraph.CalculateExecutionQueue(outputNode);
 
 
         if (nodesGuids.Length == 0)
         if (nodesGuids.Length == 0)
             return;
             return;
 
 
-        foreach (var node in executionQueue)
+        List<Guid> actualRepaintedNodes = new();
+        foreach (var guid in nodesGuids)
+        {
+            QueueRepaintNode(actualRepaintedNodes, guid, allNodes);
+        }
+    }
+
+    private void QueueRepaintNode(List<Guid> actualRepaintedNodes, Guid guid,
+        IReadOnlyCollection<IReadOnlyNode> allNodes)
+    {
+        if (actualRepaintedNodes.Contains(guid))
+            return;
+
+        var nodeVm = doc.StructureHelper.FindNode<INodeHandler>(guid);
+        if (nodeVm == null)
+        {
+            return;
+        }
+
+        actualRepaintedNodes.Add(guid);
+        IReadOnlyNode node = allNodes.FirstOrDefault(x => x.Id == guid);
+        if (node is null)
+            return;
+
+        RequestRepaintNode(node, nodeVm);
+
+        nodeVm.TraverseForwards(next =>
         {
         {
-            if (node is null)
-                continue;
+            if (next is not INodeHandler nextVm)
+                return true;
 
 
-            if (!nodesGuids.Contains(node.Id))
-                continue;
+            var nextNode = allNodes.FirstOrDefault(x => x.Id == next.Id);
 
 
-            var nodeVm = doc.StructureHelper.FindNode<INodeHandler>(node.Id);
+            if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
+                return true;
+
+            RequestRepaintNode(nextNode, nextVm);
+            actualRepaintedNodes.Add(next.Id);
+            return true;
+        });
+    }
 
 
-            if (nodeVm == null)
+    private void RequestRepaintNode(IReadOnlyNode node, INodeHandler nodeVm)
+    {
+        if (node is IPreviewRenderable renderable)
+        {
+            if (nodeVm.ResultPainter == null)
             {
             {
-                continue;
+                nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable,
+                    doc.AnimationHandler.ActiveFrameTime,
+                    doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
+                nodeVm.ResultPainter.Repaint();
             }
             }
-
-            if (node is IPreviewRenderable renderable)
+            else
             {
             {
-                if (nodeVm.ResultPainter == null)
-                {
-                    nodeVm.ResultPainter = new PreviewPainter(doc.Renderer, renderable,
-                        doc.AnimationHandler.ActiveFrameTime,
-                        doc.SizeBindable, internals.Tracker.Document.ProcessingColorSpace);
-                    nodeVm.ResultPainter.Repaint();
-                }
-                else
-                {
-                    nodeVm.ResultPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
-                    nodeVm.ResultPainter.DocumentSize = doc.SizeBindable;
-                    nodeVm.ResultPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
+                nodeVm.ResultPainter.FrameTime = doc.AnimationHandler.ActiveFrameTime;
+                nodeVm.ResultPainter.DocumentSize = doc.SizeBindable;
+                nodeVm.ResultPainter.ProcessingColorSpace = internals.Tracker.Document.ProcessingColorSpace;
 
 
-                    nodeVm.ResultPainter?.Repaint();
-                }
+                nodeVm.ResultPainter?.Repaint();
             }
             }
         }
         }
     }
     }

+ 0 - 1
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -7,7 +7,6 @@
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
                 <Grid Background="Transparent">
                 <Grid Background="Transparent">
-
                     <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left"
                     <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left"
                                VerticalAlignment="Top"
                                VerticalAlignment="Top"
                                IsVisible="False" ZIndex="100"
                                IsVisible="False" ZIndex="100"

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/DocumentInfoNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes;
+
+[NodeViewModel("DOCUMENT_INFO_NODE", "", PixiPerfectIcons.Info)]
+internal class DocumentInfoNodeViewModel : NodeViewModel<DocumentInfoNode>;

+ 10 - 0
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ShadowNodeViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
+
+[NodeViewModel("SHADOW_NODE", "FILTERS", PixiPerfectIcons.Cone)]
+internal class ShadowNodeViewModel : NodeViewModel<ShadowNode>
+{
+}

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/OffsetNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("OFFSET_NODE", "MATRIX", PixiPerfectIcons.MoveView)]
+internal class OffsetNodeViewModel : NodeViewModel<OffsetNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/RotateNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("ROTATE_NODE", "MATRIX", PixiPerfectIcons.RotateView)]
+internal class RotateNodeViewModel : NodeViewModel<RotateNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/ScaleNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("SCALE_NODE", "MATRIX", PixiPerfectIcons.Minimize)]
+internal class ScaleNodeViewModel : NodeViewModel<ScaleNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/SkewNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("SKEW_NODE", "MATRIX", PixiPerfectIcons.Italic)]
+internal class SkewNodeViewModel : NodeViewModel<SkewNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/Matrix/TransformNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Matrix;
+
+[NodeViewModel("TRANSFORM_NODE", "MATRIX", PixiPerfectIcons.CanvasResize)]
+internal class TransformNodeViewModel : NodeViewModel<TransformNode>;

+ 8 - 0
src/PixiEditor/ViewModels/Document/Nodes/TileNodeViewModel.cs

@@ -0,0 +1,8 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes;
+
+[NodeViewModel("TILE_NODE", "IMAGE", PixiPerfectIcons.Grid)]
+internal class TileNodeViewModel : NodeViewModel<TileNode>;