Browse Source

Vector layers wip

flabbet 11 months ago
parent
commit
47b27e67aa
30 changed files with 431 additions and 117 deletions
  1. 5 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/Objects/TransformObject_ChangeInfo.cs
  2. 0 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs
  3. 21 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ITransformableObject.cs
  4. 26 12
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs
  5. 6 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs
  6. 11 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  7. 4 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs
  8. 3 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs
  9. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  10. 5 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs
  11. 6 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs
  12. 67 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  14. 159 48
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  15. 1 0
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  16. 5 0
      src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs
  17. 2 1
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs
  18. 6 2
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  19. 1 2
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  20. 23 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs
  21. 7 2
      src/PixiEditor/Models/Handlers/IDocumentOperations.cs
  22. 0 1
      src/PixiEditor/Models/Handlers/ILayerHandler.cs
  23. 6 0
      src/PixiEditor/Models/Handlers/ITransparencyLockableMember.cs
  24. 5 0
      src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs
  25. 5 5
      src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs
  26. 4 4
      src/PixiEditor/Models/Serialization/Factories/PointsDataSerializationFactory.cs
  27. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ImageLayerNodeViewModel.cs
  28. 39 0
      src/PixiEditor/ViewModels/Document/Nodes/VectorLayerNodeViewModel.cs
  29. 6 4
      src/PixiEditor/Views/Layers/LayerControl.axaml.cs
  30. 2 1
      src/PixiEditor/Views/Layers/LayersManager.axaml

+ 5 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Objects/TransformObject_ChangeInfo.cs

@@ -0,0 +1,5 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Objects;
+
+public record TransformObject_ChangeInfo(Guid NodeGuid, AffectedArea Area) : IChangeInfo;

+ 0 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyStructureNode.cs

@@ -15,6 +15,5 @@ public interface IReadOnlyStructureNode : IReadOnlyNode
     public InputProperty<bool> MaskIsVisible { get; }
     public string MemberName { get; set; }
     public RectI? GetTightBounds(KeyFrameTime frameTime);
-    
     public ChunkyImage? EmbeddedMask { get; }
 }

+ 21 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/ITransformableObject.cs

@@ -0,0 +1,21 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public interface ITransformableObject
+{
+    /// <summary>
+    ///     Position in x and y.
+    /// </summary>
+    public VecD Position { get; set; }
+    
+    /// <summary>
+    ///     Scale in x and y.
+    /// </summary>
+    public VecD Size { get; set; }
+    
+    /// <summary>
+    ///     Rotation in radians.
+    /// </summary>
+    public double RotationRadians { get; set; }
+}

+ 26 - 12
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseData.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/EllipseVectorData.cs

@@ -3,29 +3,43 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public class EllipseData : ShapeData
+public class EllipseVectorData : ShapeVectorData
 {
-    public VecD Center { get; set; }
-    public VecD Radius { get; set; }
+    public VecD Radius
+    {
+        get => Size / 2;
+        set => Size = value * 2;
+    } 
 
-    public EllipseData(VecD center, VecD radius)
+    public override RectD AABB =>
+        new ShapeCorners(Position, Size)
+            .AABBBounds;
+    
+    public EllipseVectorData(VecD center, VecD radius)
     {
-        Center = center;
+        Position = center;
         Radius = radius;
     }
-    
+
     public override void Rasterize(DrawingSurface drawingSurface)
     {
-        var imageSize = new VecI((int)Radius.X * 2, (int)Radius.Y * 2);
+        var imageSize = (VecI)Size; 
 
         using ChunkyImage img = new ChunkyImage(imageSize);
-        RectI rect = new RectI(0, 0, (int)Radius.X * 2, (int)Radius.Y * 2);
+        RectI rect = new RectI(0, 0, imageSize.X, imageSize.Y); 
         
         img.EnqueueDrawEllipse(rect, StrokeColor, FillColor, StrokeWidth);
         img.CommitChanges();
         
-        VecI pos = new VecI((int)(Center.X - Radius.X), (int)(Center.Y - Radius.Y));
-        img.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, drawingSurface, pos);
+        VecI topLeft = new VecI((int)(Position.X - Radius.X), (int)(Position.Y - Radius.Y));
+
+        drawingSurface.Canvas.Save();
+        
+        drawingSurface.Canvas.RotateRadians((float)RotationRadians, (float)Position.X, (float)Position.Y);
+        
+        img.DrawMostUpToDateRegionOn(rect, ChunkResolution.Full, drawingSurface, topLeft);
+        
+        drawingSurface.Canvas.Restore();
     }
 
     public override bool IsValid()
@@ -35,7 +49,7 @@ public class EllipseData : ShapeData
 
     public override int CalculateHash()
     {
-        return HashCode.Combine(Center, Radius);
+        return HashCode.Combine(Position, Radius);
     }
 
     public override int GetCacheHash()
@@ -45,7 +59,7 @@ public class EllipseData : ShapeData
 
     public override object Clone()
     {
-        return new EllipseData(Center, Radius)
+        return new EllipseVectorData(Position, Radius)
         {
             StrokeColor = StrokeColor,
             FillColor = FillColor,

+ 6 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsData.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PointsVectorData.cs

@@ -4,15 +4,17 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public class PointsData : ShapeData
+public class PointsVectorData : ShapeVectorData
 {
     public List<VecD> Points { get; set; } = new();
     
-    public PointsData(IEnumerable<VecD> points)
+    public PointsVectorData(IEnumerable<VecD> points)
     {
         Points = new List<VecD>(points);
     }
-    
+
+    public override RectD AABB => new RectD(Points.Min(p => p.X), Points.Min(p => p.Y), Points.Max(p => p.X), Points.Max(p => p.Y));
+
     public override void Rasterize(DrawingSurface drawingSurface)
     {
         using Paint paint = new Paint();
@@ -39,7 +41,7 @@ public class PointsData : ShapeData
 
     public override object Clone()
     {
-        return new PointsData(Points)
+        return new PointsVectorData(Points)
         {
             StrokeColor = StrokeColor,
             FillColor = FillColor,

+ 11 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeData.cs → src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -1,18 +1,27 @@
 using PixiEditor.Common;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
-public abstract class ShapeData : ICacheable, ICloneable
+public abstract class ShapeVectorData : ICacheable, ICloneable
 {
+    public VecD Position { get; set; }
+    public VecD Size { get; set; } 
+    
+    /// <summary>
+    ///     Rotation in radians.
+    /// </summary>
+    public double RotationRadians { get; set; }
+    
     public Color StrokeColor { get; set; } = Colors.White;
     public Color FillColor { get; set; } = Colors.White;
     public int StrokeWidth { get; set; } = 1;
+    public abstract RectD AABB { get; }
 
     public abstract void Rasterize(DrawingSurface drawingSurface);
     public abstract bool IsValid();
-
     public abstract int GetCacheHash();
     public abstract int CalculateHash();
     public abstract object Clone();

+ 4 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/DistributePointsNode.cs

@@ -1,12 +1,11 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("DistributePoints")]
-public class DistributePointsNode : ShapeNode<PointsData>
+public class DistributePointsNode : ShapeNode<PointsVectorData>
 {
     public InputProperty<int> MaxPointCount { get; }
 
@@ -19,12 +18,12 @@ public class DistributePointsNode : ShapeNode<PointsData>
         Seed = CreateInput("Seed", "SEED", 0);
     }
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
         return GetPointsRandomly(context.DocumentSize);
     }
 
-    private PointsData GetPointsRandomly(VecI size)
+    private PointsVectorData GetPointsRandomly(VecI size)
     {
         var seed = Seed.Value;
         var random = new Random(seed);
@@ -36,7 +35,7 @@ public class DistributePointsNode : ShapeNode<PointsData>
             finalPoints.Add(new VecD(random.NextDouble() * size.X, random.NextDouble() * size.Y));
         }
         
-        var shapeData = new PointsData(finalPoints);
+        var shapeData = new PointsVectorData(finalPoints);
         return shapeData;
     }
 

+ 3 - 4
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/EllipseNode.cs

@@ -2,12 +2,11 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("Ellipse")]
-public class EllipseNode : ShapeNode<EllipseData>
+public class EllipseNode : ShapeNode<EllipseVectorData>
 {
     public InputProperty<VecD> Position { get; }
     public InputProperty<VecD> Radius { get; }
@@ -25,9 +24,9 @@ public class EllipseNode : ShapeNode<EllipseData>
         StrokeWidth = CreateInput<int>("StrokeWidth", "STROKE_WIDTH", 1);
     }
 
-    protected override EllipseData? GetShapeData(RenderingContext context)
+    protected override EllipseVectorData? GetShapeData(RenderingContext context)
     {
-        return new EllipseData(Position.Value, Radius.Value)
+        return new EllipseVectorData(Position.Value, Radius.Value)
             { StrokeColor = StrokeColor.Value, FillColor = FillColor.Value, StrokeWidth = StrokeWidth.Value };
     }
 

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

@@ -1,10 +1,10 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
@@ -13,13 +13,13 @@ public class RasterizeShapeNode : Node
 {
     public OutputProperty<Texture> Image { get; }
 
-    public InputProperty<ShapeData> Data { get; }
+    public InputProperty<ShapeVectorData> Data { get; }
 
 
     public RasterizeShapeNode()
     {
         Image = CreateOutput<Texture>("Image", "IMAGE", null);
-        Data = CreateInput<ShapeData>("Points", "SHAPE", null);
+        Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
     }
 
     protected override Texture? OnExecute(RenderingContext context)

+ 5 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RemoveClosePointsNode.cs

@@ -2,14 +2,13 @@
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
 [NodeInfo("RemoveClosePoints")]
-public class RemoveClosePointsNode : ShapeNode<PointsData>
+public class RemoveClosePointsNode : ShapeNode<PointsVectorData>
 {
-    public InputProperty<PointsData> Input { get; }
+    public InputProperty<PointsVectorData> Input { get; }
 
     public InputProperty<double> MinDistance { get; }
 
@@ -17,12 +16,12 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
 
     public RemoveClosePointsNode()
     {
-        Input = CreateInput<PointsData>("Input", "POINTS", null);
+        Input = CreateInput<PointsVectorData>("Input", "POINTS", null);
         MinDistance = CreateInput("MinDistance", "MIN_DISTANCE", 0d);
         Seed = CreateInput("Seed", "SEED", 0);
     }
 
-    protected override PointsData? GetShapeData(RenderingContext context)
+    protected override PointsVectorData? GetShapeData(RenderingContext context)
     {
         var data = Input.Value;
 
@@ -64,7 +63,7 @@ public class RemoveClosePointsNode : ShapeNode<PointsData>
             newPoints.Add(availablePoints[0]);
         }
 
-        var finalData = new PointsData(newPoints);
+        var finalData = new PointsVectorData(newPoints);
 
         return finalData;
     }

+ 6 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/ShapeNode.cs

@@ -1,11 +1,12 @@
-using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Numerics;
-using ShapeData = PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data.ShapeData;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 
-public abstract class ShapeNode<T> : Node where T : ShapeData
+public abstract class ShapeNode<T> : Node where T : ShapeVectorData
 {
     public OutputProperty<T> Output { get; }
     
@@ -28,11 +29,11 @@ public abstract class ShapeNode<T> : Node where T : ShapeData
     
     protected abstract T? GetShapeData(RenderingContext context);
 
-    public Texture RasterizePreview(ShapeData data, VecI size)
+    public Texture RasterizePreview(ShapeVectorData vectorData, VecI size)
     {
         Texture texture = RequestTexture(0, size);
         
-        data.Rasterize(texture.DrawingSurface);
+        vectorData.Rasterize(texture.DrawingSurface);
         
         return texture;
     }

+ 67 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs

@@ -0,0 +1,67 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("VectorLayer")]
+public class VectorLayerNode : LayerNode, ITransformableObject
+{
+    public VecD Position
+    {
+        get => ShapeData.Position;
+        set => ShapeData.Position = value;
+    }
+
+    public VecD Size
+    {
+        get => ShapeData.Size;
+        set => ShapeData.Size = value;
+    }
+
+    public double RotationRadians
+    {
+        get => ShapeData.RotationRadians;
+        set => ShapeData.RotationRadians = value;
+    }
+
+    public ShapeVectorData ShapeData { get; } = new EllipseVectorData(new VecI(32), new VecI(32));
+    
+    private int lastCacheHash;
+    
+    protected override Texture? OnExecute(RenderingContext context)
+    {
+        Texture texture = RequestTexture(0, context.DocumentSize);
+        ShapeData.Rasterize(texture.DrawingSurface);
+        
+        Output.Value = texture;
+        
+        return texture;
+    }
+
+    protected override bool CacheChanged(RenderingContext context)
+    {
+        return base.CacheChanged(context) || ShapeData.GetCacheHash() != lastCacheHash;
+    }
+
+    protected override void UpdateCache(RenderingContext context)
+    {
+        base.UpdateCache(context);
+        lastCacheHash = ShapeData.GetCacheHash();
+    }
+
+    public override RectI? GetTightBounds(KeyFrameTime frameTime)
+    {
+        return (RectI)ShapeData.AABB;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new VectorLayerNode();
+    }
+
+}

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

@@ -111,7 +111,7 @@ internal static class DrawingChangeHelper
             // If it should draw on the mask, the mask can't be null
             true when member.EmbeddedMask is null => false,
             // If it should not draw on the mask, the member can't be a folder
-            false when member is FolderNode => false,
+            false when member is not ImageLayerNode => false,
             _ => true
         };
     }

+ 159 - 48
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -1,4 +1,7 @@
 using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Objects;
 using PixiEditor.ChangeableDocument.Changes.Selection;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -8,6 +11,7 @@ using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
 internal class TransformSelectedArea_UpdateableChange : UpdateableChange
 {
     private readonly Guid[] membersToTransform;
@@ -16,6 +20,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
     private ShapeCorners corners;
 
     private Dictionary<Guid, (Surface surface, VecI pos)>? images;
+    private Dictionary<Guid, (ITransformableObject, ShapeCorners original)>? transformableObjectMembers;
     private Matrix3X3 globalMatrix;
     private Dictionary<Guid, CommittedChunkStorage>? savedChunks;
 
@@ -26,7 +31,7 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
     private bool hasEnqueudImages = false;
     private int frame;
 
-    private static Paint RegularPaint { get; } = new () { BlendMode = BlendMode.SrcOver };
+    private static Paint RegularPaint { get; } = new() { BlendMode = BlendMode.SrcOver };
 
     [GenerateUpdateableChangeActions]
     public TransformSelectedArea_UpdateableChange(
@@ -45,36 +50,54 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (membersToTransform.Length == 0 || target.Selection.SelectionPath.IsEmpty)
+        if (membersToTransform.Length == 0)
             return false;
 
-        foreach (var guid in membersToTransform)
-        {
-            if (!DrawingChangeHelper.IsValidForDrawing(target, guid, drawOnMask))
-                return false;
-        }
+        VectorPath path = !target.Selection.SelectionPath.IsEmpty
+            ? target.Selection.SelectionPath
+            : GetSelectionFromMembers(target, membersToTransform);
+
+        if (path.IsEmpty)
+            return false;
+
+        originalPath = new VectorPath(path) { FillType = PathFillType.EvenOdd };
 
-        originalPath = new VectorPath(target.Selection.SelectionPath) { FillType = PathFillType.EvenOdd };
-        
         originalTightBounds = originalPath.TightBounds;
         roundedTightBounds = (RectI)originalTightBounds.RoundOutwards();
         //boundsRoundingOffset = bounds.TopLeft - roundedBounds.TopLeft;
 
-        images = new();
         foreach (var guid in membersToTransform)
         {
-            ChunkyImage image = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            var extracted = ExtractArea(image, originalPath, roundedTightBounds);
-            if (extracted.IsT0)
-                continue;
-            images.Add(guid, (extracted.AsT1.image, extracted.AsT1.extractedRect.Pos));
+            StructureNode layer = target.FindMemberOrThrow(guid);
+
+            if (layer is ImageLayerNode)
+            {
+                ChunkyImage image = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
+                var extracted = ExtractArea(image, originalPath, roundedTightBounds);
+                if (extracted.IsT0)
+                    continue;
+                
+                if (images is null)
+                    images = new();
+                
+                images.Add(guid, (extracted.AsT1.image, extracted.AsT1.extractedRect.Pos));
+            }
+            else if (layer is ITransformableObject transformable)
+            {
+                transformableObjectMembers ??= new();
+                transformableObjectMembers.Add(guid, (transformable, new ShapeCorners(transformable.Position, transformable.Size).AsRotated(transformable.RotationRadians, transformable.Position)));
+            } 
         }
+        
+        if (images is null && transformableObjectMembers is null)
+            return false;
 
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
         return true;
     }
 
-    public OneOf<None, (Surface image, RectI extractedRect)> ExtractArea(ChunkyImage image, VectorPath path, RectI pathBounds)
+    public OneOf<None, (Surface image, RectI extractedRect)> ExtractArea(ChunkyImage image, VectorPath path,
+        RectI pathBounds)
     {
         // get rid of transparent areas on edges
         var memberImageBounds = image.FindChunkAlignedMostUpToDateBounds();
@@ -107,42 +130,41 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
     }
 
-    private AffectedArea DrawImage(Document doc, Guid memberGuid, Surface image, VecI originalPos, ChunkyImage memberImage)
-    {
-        var prevAffArea = memberImage.FindAffectedArea();
-
-        memberImage.CancelChanges();
-
-        if (!keepOriginal)
-            memberImage.EnqueueClearPath(originalPath!, roundedTightBounds);
-        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - (float)originalTightBounds.Left, originalPos.Y - (float)originalTightBounds.Top);
-        localMatrix = localMatrix.PostConcat(globalMatrix);
-        memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
-        hasEnqueudImages = true;
-
-        var affectedArea = memberImage.FindAffectedArea();
-        affectedArea.UnionWith(prevAffArea);
-        return affectedArea;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         if (savedChunks is not null)
             throw new InvalidOperationException("Apply called twice");
         savedChunks = new();
 
         List<IChangeInfo> infos = new();
-        foreach (var (guid, (image, pos)) in images!)
+        if (images != null)
         {
-            ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            var area = DrawImage(target, guid, image, pos, memberImage);
-            savedChunks[guid] = new(memberImage, memberImage.FindAffectedArea().Chunks);
-            memberImage.CommitChanges();
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, area, drawOnMask).AsT1);
+            foreach (var (guid, (image, pos)) in images)
+            {
+                ChunkyImage memberImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
+                var area = DrawImage(image, pos, memberImage);
+                savedChunks[guid] = new(memberImage, memberImage.FindAffectedArea().Chunks);
+                memberImage.CommitChanges();
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, area, drawOnMask).AsT1);
+            }
         }
 
-        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
+        if (transformableObjectMembers != null)
+        {
+            foreach (var (guid, (transformable, pos)) in transformableObjectMembers!)
+            {
+                transformable.Position = corners.RectCenter;
+                transformable.Size = corners.RectSize;
+                transformable.RotationRadians = corners.RectRotation;
+                
+                AffectedArea area = GetTranslationAffectedArea();
+                infos.Add(new TransformObject_ChangeInfo(guid, area));
+            }
+        }
 
+        infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
+        
         hasEnqueudImages = false;
         ignoreInUndo = false;
         return infos;
@@ -151,11 +173,30 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
     {
         List<IChangeInfo> infos = new();
-        foreach (var (guid, (image, pos)) in images!)
+        if (images != null)
         {
-            ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, DrawImage(target, guid, image, pos, targetImage), drawOnMask).AsT1);
+            foreach (var (guid, (image, pos)) in images)
+            {
+                ChunkyImage targetImage = DrawingChangeHelper.GetTargetImageOrThrow(target, guid, drawOnMask, frame);
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, DrawImage(image, pos, targetImage), drawOnMask)
+                    .AsT1);
+            }
         }
+
+        if (transformableObjectMembers != null)
+        {
+            foreach (var (guid, (transformable, pos)) in transformableObjectMembers)
+            {
+                VecD translated = corners.RectCenter; 
+                transformable.Position = translated;
+                transformable.Size = corners.RectSize; 
+                transformable.RotationRadians = corners.RectRotation;
+                
+                AffectedArea translationAffectedArea = GetTranslationAffectedArea();
+                infos.Add(new TransformObject_ChangeInfo(guid, translationAffectedArea));
+            }
+        }
+
         infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalTightBounds, corners));
         return infos;
     }
@@ -166,11 +207,27 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
         foreach (var (guid, storage) in savedChunks!)
         {
             var storageCopy = storage;
-            var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, frame, ref storageCopy);
+            var chunks =
+                DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, frame,
+                    ref storageCopy);
             infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, chunks, drawOnMask).AsT1);
         }
 
-        (var toDispose, target.Selection.SelectionPath) = (target.Selection.SelectionPath, new VectorPath(originalPath!));
+        if (transformableObjectMembers != null)
+        {
+            foreach (var (guid, (transformable, original)) in transformableObjectMembers)
+            {
+                transformable.Position = original.RectCenter;
+                transformable.Size = original.RectSize;
+                transformable.RotationRadians = original.RectRotation;
+                
+                AffectedArea area = GetTranslationAffectedArea();
+                infos.Add(new TransformObject_ChangeInfo(guid, area));
+            }
+        }
+
+        (var toDispose, target.Selection.SelectionPath) =
+            (target.Selection.SelectionPath, new VectorPath(originalPath!));
         toDispose.Dispose();
         infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
 
@@ -181,7 +238,8 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
     public override void Dispose()
     {
         if (hasEnqueudImages)
-            throw new InvalidOperationException("Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
+            throw new InvalidOperationException(
+                "Attempted to dispose the change while it's internally stored image is still used enqueued in some ChunkyImage. Most likely someone tried to dispose a change after ApplyTemporarily was called but before the subsequent call to Apply. Don't do that.");
 
         if (images is not null)
         {
@@ -199,4 +257,57 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
             }
         }
     }
+    
+    private AffectedArea GetTranslationAffectedArea()
+    {
+        RectD oldBounds = originalTightBounds;
+        
+        HashSet<VecI> chunks = new();
+        VecI topLeftChunk = new VecI((int)oldBounds.Left / ChunkyImage.FullChunkSize, (int)oldBounds.Top / ChunkyImage.FullChunkSize);
+        VecI bottomRightChunk = new VecI((int)oldBounds.Right / ChunkyImage.FullChunkSize, (int)oldBounds.Bottom / ChunkyImage.FullChunkSize);
+        
+        for (int x = topLeftChunk.X; x <= bottomRightChunk.X; x++)
+        {
+            for (int y = topLeftChunk.Y; y <= bottomRightChunk.Y; y++)
+            {
+                chunks.Add(new VecI(x, y));
+            }
+        }
+
+        return new AffectedArea(chunks);
+    }
+
+    private AffectedArea DrawImage(Surface image, VecI originalPos, ChunkyImage memberImage)
+    {
+        var prevAffArea = memberImage.FindAffectedArea();
+
+        memberImage.CancelChanges();
+
+        if (!keepOriginal)
+            memberImage.EnqueueClearPath(originalPath!, roundedTightBounds);
+        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - (float)originalTightBounds.Left,
+            originalPos.Y - (float)originalTightBounds.Top);
+        localMatrix = localMatrix.PostConcat(globalMatrix);
+        memberImage.EnqueueDrawImage(localMatrix, image, RegularPaint, false);
+        hasEnqueudImages = true;
+
+        var affectedArea = memberImage.FindAffectedArea();
+        affectedArea.UnionWith(prevAffArea);
+        return affectedArea;
+    }
+
+    private VectorPath GetSelectionFromMembers(Document target, IEnumerable<Guid> members)
+    {
+        VectorPath path = new VectorPath();
+        foreach (var guid in members)
+        {
+            var bounds = target.FindMember(guid).GetTightBounds(frame);
+            if (bounds.HasValue)
+            {
+                path.AddRect(bounds.Value);
+            }
+        }
+
+        return path;
+    }
 }

+ 1 - 0
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -42,6 +42,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <Folder Include="Models\" />
     <Folder Include="Utils\" />
   </ItemGroup>
 

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs

@@ -54,6 +54,11 @@ public struct Matrix3X3 : IEquatable<Matrix3X3>
     /// <summary>Gets or sets the z-perspective.</summary>
     /// <value />
     public float Persp2 { get; set; }
+    
+    public VecD Position => new VecD(TransX, TransY);
+    public VecD Scale => new VecD(ScaleX, ScaleY);
+    public VecD Skew => new VecD(SkewX, SkewY);
+    public VecD Perspective => new VecD(Persp0, Persp1);
 
     public readonly bool Equals(Matrix3X3 obj)
     {

+ 2 - 1
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaCanvasImplementation.cs

@@ -112,9 +112,10 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
 
         public void DrawPoints(IntPtr objPtr, PointMode pointMode, Point[] points, Paint paint)
         {
+            SKPoint[] skPoints = CastUtility.UnsafeArrayCast<Point, SKPoint>(points);
             ManagedInstances[objPtr].DrawPoints(
                 (SKPointMode)pointMode,
-                CastUtility.UnsafeArrayCast<Point, SKPoint>(points),
+                skPoints,
                 _paintImpl[paint.ObjectPointer]);
         }
 

+ 6 - 2
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -316,7 +316,8 @@ internal class DocumentUpdater
     private void ProcessLayerLockTransparency(LayerLockTransparency_ChangeInfo info)
     {
         ILayerHandler? layer = (ILayerHandler)doc.StructureHelper.FindOrThrow(info.Id);
-        layer.SetLockTransparency(info.LockTransparency);
+        if (layer is ITransparencyLockableMember transparencyLockableLayer)
+            transparencyLockableLayer.SetLockTransparency(info.LockTransparency);
     }
 
     private void ProcessStructureMemberBlendMode(StructureMemberBlendMode_ChangeInfo info)
@@ -372,7 +373,10 @@ internal class DocumentUpdater
         if (info is CreateLayer_ChangeInfo layerInfo)
         {
             memberVM = doc.NodeGraphHandler.AllNodes.FirstOrDefault(x => x.Id == info.Id) as ILayerHandler;
-            ((ILayerHandler)memberVM).SetLockTransparency(layerInfo.LockTransparency);
+            if (memberVM is ITransparencyLockableMember transparencyLockableMember)
+            {
+                transparencyLockableMember.SetLockTransparency(layerInfo.LockTransparency);        
+            }
         }
         else if (info is CreateFolder_ChangeInfo)
         {

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

@@ -482,8 +482,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     public void TransformSelectedArea(bool toolLinked)
     {
         if (Document.SelectedStructureMember is null ||
-            Internals.ChangeController.IsChangeActive && !toolLinked ||
-            Document.SelectionPathBindable.IsEmpty)
+            Internals.ChangeController.IsChangeActive && !toolLinked)
             return;
         Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
     }

+ 23 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs

@@ -24,7 +24,7 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
     public override ExecutionState Start()
     {
         tool = GetHandler<IMoveToolHandler>();
-        if (tool is null || document!.SelectedStructureMember is null || document!.SelectionPathBindable.IsEmpty)
+        if (tool is null || document!.SelectedStructureMember is null)
             return ExecutionState.Error;
 
         tool.TransformingSelectedArea = true;
@@ -36,8 +36,13 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
         
         if (!members.Any())
             return ExecutionState.Error;
+        
+        RectD rect = !document.SelectionPathBindable.IsEmpty ? document.SelectionPathBindable.TightBounds : GetMembersTightBounds(members);
+        
+        if (rect.IsZeroOrNegativeArea)
+            return ExecutionState.Error;
 
-        ShapeCorners corners = new(document.SelectionPathBindable.TightBounds);
+        ShapeCorners corners = new(rect);
         document.TransformHandler.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, corners, Type == ExecutorType.Regular);
         membersToTransform = members.Select(static a => a.Id).ToArray();
         internals!.ActionAccumulator.AddActions(
@@ -45,6 +50,22 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
         return ExecutionState.Success;
     }
 
+    private RectD GetMembersTightBounds(List<IStructureMemberHandler> members)
+    {
+        RectI rect = members[0].TightBounds ?? RectI.Empty;
+        
+        for (int i = 1; i < members.Count; i++)
+        {
+            RectI? memberRect = members[i].TightBounds;
+            if (memberRect is not null)
+            {
+                rect = rect.Union(memberRect.Value);
+            } 
+        }
+
+        return (RectD)rect;
+    }
+
     public override void OnTransformMoved(ShapeCorners corners)
     {
         internals!.ActionAccumulator.AddActions(

+ 7 - 2
src/PixiEditor/Models/Handlers/IDocumentOperations.cs

@@ -1,8 +1,13 @@
-namespace PixiEditor.Models.Handlers;
+using PixiEditor.Models.Layers;
 
-public interface IDocumentOperations
+namespace PixiEditor.Models.Handlers;
+
+internal interface IDocumentOperations
 {
     public void DeleteStructureMember(Guid memberGuidValue);
     public void DuplicateLayer(Guid memberGuidValue);
     public void AddSoftSelectedMember(Guid memberGuidValue);
+    public void MoveStructureMember(Guid memberGuidValue, Guid target, StructureMemberPlacement placement);
+    public void SetSelectedMember(Guid memberId);
+    public void ClearSoftSelectedMembers();
 }

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

@@ -3,5 +3,4 @@
 internal interface ILayerHandler : IStructureMemberHandler
 {
     public bool ShouldDrawOnMask { get; set; }
-    public void SetLockTransparency(bool infoLockTransparency);
 }

+ 6 - 0
src/PixiEditor/Models/Handlers/ITransparencyLockableMember.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Handlers;
+
+public interface ITransparencyLockableMember
+{
+    public void SetLockTransparency(bool value);
+}

+ 5 - 0
src/PixiEditor/Models/Rendering/AffectedAreasGatherer.cs

@@ -11,6 +11,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
 using PixiEditor.ChangeableDocument.ChangeInfos.Drawing;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Objects;
 using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root;
 using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
@@ -67,6 +68,10 @@ internal class AffectedAreasGatherer
                     AddToMainImage(info.Area);
                     AddToImagePreviews(info.Id, info.Area);
                     break;
+                case TransformObject_ChangeInfo info:
+                    AddToMainImage(info.Area);
+                    AddToImagePreviews(info.NodeGuid, info.Area);
+                    break;
                 case CreateStructureMember_ChangeInfo info:
                     AddAllToMainImage(info.Id, 0);
                     AddAllToImagePreviews(info.Id, 0);

+ 5 - 5
src/PixiEditor/Models/Serialization/Factories/EllipseSerializationFactory.cs

@@ -4,13 +4,13 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.Serialization.Factories;
 
-public class EllipseSerializationFactory : SerializationFactory<byte[], EllipseData>
+public class EllipseSerializationFactory : SerializationFactory<byte[], EllipseVectorData>
 {
     public override string DeserializationId { get; } = "PixiEditor.EllipseData";
-    public override byte[] Serialize(EllipseData original)
+    public override byte[] Serialize(EllipseVectorData original)
     {
         ByteBuilder builder = new ByteBuilder();
-        builder.AddVecD(original.Center);
+        builder.AddVecD(original.Position);
         builder.AddVecD(original.Radius);
         builder.AddColor(original.StrokeColor);
         builder.AddColor(original.FillColor);
@@ -19,7 +19,7 @@ public class EllipseSerializationFactory : SerializationFactory<byte[], EllipseD
         return builder.Build();
     }
 
-    public override bool TryDeserialize(object serialized, out EllipseData original)
+    public override bool TryDeserialize(object serialized, out EllipseVectorData original)
     {
         if (serialized is not byte[] data)
         {
@@ -35,7 +35,7 @@ public class EllipseSerializationFactory : SerializationFactory<byte[], EllipseD
         Color fillColor = extractor.GetColor();
         int strokeWidth = extractor.GetInt();
         
-        original = new EllipseData(center, radius)
+        original = new EllipseVectorData(center, radius)
         {
             StrokeColor = strokeColor,
             FillColor = fillColor,

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

@@ -4,10 +4,10 @@ using PixiEditor.Numerics;
 
 namespace PixiEditor.Models.Serialization.Factories;
 
-public class PointsDataSerializationFactory : SerializationFactory<byte[], PointsData>
+public class PointsDataSerializationFactory : SerializationFactory<byte[], PointsVectorData>
 {
     public override string DeserializationId { get; } = "PixiEditor.PointsData";
-    public override byte[] Serialize(PointsData original)
+    public override byte[] Serialize(PointsVectorData original)
     {
         ByteBuilder builder = new ByteBuilder();
         builder.AddVecDList(original.Points);
@@ -17,7 +17,7 @@ public class PointsDataSerializationFactory : SerializationFactory<byte[], Point
         return builder.Build();
     }
 
-    public override bool TryDeserialize(object serialized, out PointsData original)
+    public override bool TryDeserialize(object serialized, out PointsVectorData original)
     {
         if (serialized is not byte[] data)
         {
@@ -31,7 +31,7 @@ public class PointsDataSerializationFactory : SerializationFactory<byte[], Point
         Color fillColor = extractor.GetColor();
         int strokeWidth = extractor.GetInt();
         
-        original = new PointsData(points)
+        original = new PointsVectorData(points)
         {
             FillColor = fillColor,
             StrokeWidth = strokeWidth

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

@@ -6,7 +6,7 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 [NodeViewModel("IMAGE_LAYER_NODE", "STRUCTURE", "\ue905")]
-internal class ImageLayerNodeViewModel : StructureMemberViewModel<ImageLayerNode>, ILayerHandler
+internal class ImageLayerNodeViewModel : StructureMemberViewModel<ImageLayerNode>, ILayerHandler, ITransparencyLockableMember
 {
     bool lockTransparency;
     public void SetLockTransparency(bool lockTransparency)

+ 39 - 0
src/PixiEditor/ViewModels/Document/Nodes/VectorLayerNodeViewModel.cs

@@ -0,0 +1,39 @@
+using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes;
+
+[NodeViewModel("VECTOR_LAYER", "STRUCTURE", "\ue916")]
+internal class VectorLayerNodeViewModel : StructureMemberViewModel<VectorLayerNode>, ILayerHandler
+{
+    bool lockTransparency;
+    public void SetLockTransparency(bool lockTransparency)
+    {
+        this.lockTransparency = lockTransparency;
+        OnPropertyChanged(nameof(LockTransparencyBindable));
+    }
+    public bool LockTransparencyBindable
+    {
+        get => lockTransparency;
+        set
+        {
+            if (!Document.UpdateableChangeActive)
+                Internals.ActionAccumulator.AddFinishedActions(new LayerLockTransparency_Action(Id, value));
+        }
+    }
+
+    private bool shouldDrawOnMask = false;
+    public bool ShouldDrawOnMask
+    {
+        get => shouldDrawOnMask;
+        set
+        {
+            if (value == shouldDrawOnMask)
+                return;
+            shouldDrawOnMask = value;
+            OnPropertyChanged(nameof(ShouldDrawOnMask));
+        }
+    }
+}

+ 6 - 4
src/PixiEditor/Views/Layers/LayerControl.axaml.cs

@@ -1,10 +1,12 @@
-using Avalonia;
+using System.Windows.Input;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Models.Controllers.InputDevice;
+using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document.Nodes;
@@ -15,10 +17,10 @@ internal partial class LayerControl : UserControl
 {
     public static string? LayerControlDataName = typeof(LayerControl).FullName;
 
-    public static readonly StyledProperty<ImageLayerNodeViewModel> LayerProperty =
-        AvaloniaProperty.Register<LayerControl, ImageLayerNodeViewModel>(nameof(Layer));
+    public static readonly StyledProperty<ILayerHandler> LayerProperty =
+        AvaloniaProperty.Register<LayerControl, ILayerHandler>(nameof(Layer));
 
-    public ImageLayerNodeViewModel Layer
+    public ILayerHandler Layer
     {
         get => GetValue(LayerProperty);
         set => SetValue(LayerProperty, value);

+ 2 - 1
src/PixiEditor/Views/Layers/LayersManager.axaml

@@ -15,6 +15,7 @@
              xmlns:ui1="clr-namespace:PixiEditor.Helpers.UI"
              xmlns:panels="clr-namespace:PixiEditor.Views.Panels"
              xmlns:nodes="clr-namespace:PixiEditor.ViewModels.Document.Nodes"
+             xmlns:handlers="clr-namespace:PixiEditor.Models.Handlers"
              mc:Ignorable="d"
              d:DesignHeight="450" d:DesignWidth="250" x:Name="layersManager">
     <UserControl.Resources>
@@ -145,7 +146,7 @@
                                 PointerPressed="FolderControl_MouseDown"
                                 PointerReleased="FolderControl_MouseUp"/>
                     </TreeDataTemplate>
-                    <DataTemplate DataType="{x:Type nodes:ImageLayerNodeViewModel}">
+                    <DataTemplate DataType="{x:Type handlers:ILayerHandler}">
                         <layers:LayerControl
                             Layer="{Binding}"
                             Manager="{Binding ElementName=layersManager}"