Explorar o código

Move tool improvements wip

flabbet hai 1 ano
pai
achega
9630c7293a

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

@@ -16,4 +16,5 @@ public interface IReadOnlyStructureNode : IReadOnlyNode
     public string MemberName { get; set; }
     public RectI? GetTightBounds(KeyFrameTime frameTime);
     public ChunkyImage? EmbeddedMask { get; }
+    public ShapeCorners GetTransformationCorners(KeyFrameTime frameTime);
 }

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

@@ -27,6 +27,10 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
     public OutputProperty<Texture?> FilterlessOutput { get; }
 
     public ChunkyImage? EmbeddedMask { get; set; }
+    public virtual ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return new ShapeCorners((RectD)GetTightBounds(frameTime).Value);
+    }
 
     public string MemberName
     {

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

@@ -59,6 +59,11 @@ public class VectorLayerNode : LayerNode, ITransformableObject
         return (RectI)ShapeData.AABB;
     }
 
+    public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        return new ShapeCorners(ShapeData.Position, ShapeData.Size).AsRotated(RotationRadians, ShapeData.Position);
+    }
+
     public override Node CreateCopy()
     {
         return new VectorLayerNode();

+ 0 - 313
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -1,313 +0,0 @@
-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;
-using PixiEditor.DrawingApi.Core.Surfaces;
-using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
-using PixiEditor.DrawingApi.Core.Surfaces.Vector;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changes.Drawing;
-
-internal class TransformSelectedArea_UpdateableChange : UpdateableChange
-{
-    private readonly Guid[] membersToTransform;
-    private readonly bool drawOnMask;
-    private bool keepOriginal;
-    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;
-
-    private RectD originalTightBounds;
-    private RectI roundedTightBounds;
-    private VectorPath? originalPath;
-
-    private bool hasEnqueudImages = false;
-    private int frame;
-
-    private static Paint RegularPaint { get; } = new() { BlendMode = BlendMode.SrcOver };
-
-    [GenerateUpdateableChangeActions]
-    public TransformSelectedArea_UpdateableChange(
-        IEnumerable<Guid> membersToTransform,
-        ShapeCorners corners,
-        bool keepOriginal,
-        bool transformMask,
-        int frame)
-    {
-        this.membersToTransform = membersToTransform.Select(static a => a).ToArray();
-        this.corners = corners;
-        this.keepOriginal = keepOriginal;
-        this.drawOnMask = transformMask;
-        this.frame = frame;
-    }
-
-    public override bool InitializeAndValidate(Document target)
-    {
-        if (membersToTransform.Length == 0)
-            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 };
-
-        originalTightBounds = originalPath.TightBounds;
-        roundedTightBounds = (RectI)originalTightBounds.RoundOutwards();
-        //boundsRoundingOffset = bounds.TopLeft - roundedBounds.TopLeft;
-
-        foreach (var guid in membersToTransform)
-        {
-            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)
-    {
-        // get rid of transparent areas on edges
-        var memberImageBounds = image.FindChunkAlignedMostUpToDateBounds();
-        if (memberImageBounds is null)
-            return new None();
-        pathBounds = pathBounds.Intersect(memberImageBounds.Value);
-        pathBounds = pathBounds.Intersect(new RectI(VecI.Zero, image.LatestSize));
-        if (pathBounds.IsZeroOrNegativeArea)
-            return new None();
-
-        // shift the clip to account for the image being smaller than the selection
-        VectorPath clipPath = new VectorPath(path) { FillType = PathFillType.EvenOdd };
-        clipPath.Transform(Matrix3X3.CreateTranslation(-pathBounds.X, -pathBounds.Y));
-
-        // draw
-        Surface output = new(pathBounds.Size);
-        output.DrawingSurface.Canvas.Save();
-        output.DrawingSurface.Canvas.ClipPath(clipPath);
-        image.DrawMostUpToDateRegionOn(pathBounds, ChunkResolution.Full, output.DrawingSurface, VecI.Zero);
-        output.DrawingSurface.Canvas.Restore();
-
-        return (output, pathBounds);
-    }
-
-    [UpdateChangeMethod]
-    public void Update(ShapeCorners corners, bool keepOriginal)
-    {
-        this.keepOriginal = keepOriginal;
-        this.corners = corners;
-        globalMatrix = OperationHelper.CreateMatrixFromPoints(corners, originalTightBounds.Size);
-    }
-
-    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();
-        if (images != null)
-        {
-            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);
-            }
-        }
-
-        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;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
-    {
-        List<IChangeInfo> infos = new();
-        if (images != null)
-        {
-            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;
-    }
-
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
-    {
-        List<IChangeInfo> infos = new();
-        foreach (var (guid, storage) in savedChunks!)
-        {
-            var storageCopy = storage;
-            var chunks =
-                DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, frame,
-                    ref storageCopy);
-            infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(guid, chunks, drawOnMask).AsT1);
-        }
-
-        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)));
-
-        savedChunks = null;
-        return infos;
-    }
-
-    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.");
-
-        if (images is not null)
-        {
-            foreach (var (_, (image, _)) in images)
-            {
-                image.Dispose();
-            }
-        }
-
-        if (savedChunks is not null)
-        {
-            foreach (var (_, chunks) in savedChunks)
-            {
-                chunks.Dispose();
-            }
-        }
-    }
-    
-    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;
-    }
-}

+ 395 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -0,0 +1,395 @@
+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;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.Drawing;
+
+internal class TransformSelected_UpdateableChange : UpdateableChange
+{
+    private readonly bool drawOnMask;
+    private bool keepOriginal;
+    private ShapeCorners masterCorners;
+    private VecD originalMasterCornersSize;
+
+    private List<MemberTransformationData> memberData;
+
+    private VectorPath? originalPath;
+    private RectD originalSelectionBounds;
+
+    private bool isTransformingSelection;
+    private bool hasEnqueudImages = false;
+    private int frame;
+    private ShapeCorners lastCorners;
+    private bool appliedOnce;
+    private static Paint RegularPaint { get; } = new() { BlendMode = BlendMode.SrcOver };
+
+    [GenerateUpdateableChangeActions]
+    public TransformSelected_UpdateableChange(
+        ShapeCorners masterCorners,
+        bool keepOriginal,
+        Dictionary<Guid, ShapeCorners> memberCorners,
+        bool transformMask,
+        int frame)
+    {
+        memberData = new();
+        foreach (var corners in memberCorners)
+        {
+            memberData.Add(new MemberTransformationData(corners.Key) { MemberCorners = corners.Value });
+        }
+
+        this.masterCorners = masterCorners;
+        lastCorners = masterCorners;
+        this.keepOriginal = keepOriginal;
+        this.drawOnMask = transformMask;
+        this.frame = frame;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (memberData.Count == 0)
+            return false;
+
+        RectD originalTightBounds = default;
+
+        if (target.Selection.SelectionPath is { IsEmpty: false })
+        {
+            originalPath = new VectorPath(target.Selection.SelectionPath) { FillType = PathFillType.EvenOdd };
+            originalTightBounds = originalPath.TightBounds;
+            originalSelectionBounds = masterCorners.AABBBounds;
+            isTransformingSelection = true;
+        }
+
+        foreach (var member in memberData)
+        {
+            StructureNode layer = target.FindMemberOrThrow(member.MemberId);
+
+            if (layer is IReadOnlyImageNode)
+            {
+                ChunkyImage image =
+                    DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+                VectorPath pathToExtract = originalPath;
+                RectD targetBounds = originalTightBounds;
+
+                if (pathToExtract == null)
+                {
+                    RectI tightBounds = layer.GetTightBounds(frame).Value;
+                    pathToExtract = new VectorPath();
+                    pathToExtract.AddRect(tightBounds);
+                    targetBounds = pathToExtract.Bounds;
+                }
+
+                member.OriginalPath = pathToExtract;
+                member.OriginalBounds = targetBounds;
+                member.GlobalMatrix = OperationHelper.CreateMatrixFromPoints(member.MemberCorners, targetBounds.Size);
+                var extracted = ExtractArea(image, pathToExtract, member.RoundedOriginalBounds.Value);
+                if (extracted.IsT0)
+                    continue;
+
+                member.AddImage(extracted.AsT1.image, extracted.AsT1.extractedRect.Pos);
+            }
+            else if (layer is ITransformableObject transformable)
+            {
+                member.AddTransformableObject(transformable,
+                    new ShapeCorners(transformable.Position, transformable.Size)
+                        .AsRotated(transformable.RotationRadians, transformable.Position));
+            }
+        }
+
+        originalMasterCornersSize = masterCorners.RectSize;
+        return true;
+    }
+
+    [UpdateChangeMethod]
+    public void Update(ShapeCorners masterCorners, bool keepOriginal)
+    {
+        this.keepOriginal = keepOriginal;
+        lastCorners = this.masterCorners;
+        this.masterCorners = masterCorners;
+
+        foreach (var member in memberData)
+        {
+            if (member.IsImage)
+            {
+                var corners = MasterToMemberCoords(member.MemberCorners);
+                member.GlobalMatrix = OperationHelper.CreateMatrixFromPoints(corners, member.OriginalBounds.Value.Size);
+            }
+        }
+    }
+
+    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();
+        if (memberImageBounds is null)
+            return new None();
+        pathBounds = pathBounds.Intersect(memberImageBounds.Value);
+        pathBounds = pathBounds.Intersect(new RectI(VecI.Zero, image.LatestSize));
+        if (pathBounds.IsZeroOrNegativeArea)
+            return new None();
+
+        // shift the clip to account for the image being smaller than the selection
+        VectorPath clipPath = new VectorPath(path) { FillType = PathFillType.EvenOdd };
+        clipPath.Transform(Matrix3X3.CreateTranslation(-pathBounds.X, -pathBounds.Y));
+
+        // draw
+        Surface output = new(pathBounds.Size);
+        output.DrawingSurface.Canvas.Save();
+        output.DrawingSurface.Canvas.ClipPath(clipPath);
+        image.DrawMostUpToDateRegionOn(pathBounds, ChunkResolution.Full, output.DrawingSurface, VecI.Zero);
+        output.DrawingSurface.Canvas.Restore();
+
+        return (output, pathBounds);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        if (appliedOnce)
+            throw new InvalidOperationException("Apply called twice");
+        appliedOnce = true;
+
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            if (member.IsImage)
+            {
+                ChunkyImage memberImage =
+                    DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+                var area = DrawImage(member, memberImage);
+                member.SavedChunks = new(memberImage, memberImage.FindAffectedArea().Chunks);
+                memberImage.CommitChanges();
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId, area, drawOnMask).AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                ShapeCorners localCorners = MasterToMemberCoords(member.MemberCorners);
+
+                member.TransformableObject.Position = localCorners.RectCenter;
+                member.TransformableObject.Size = localCorners.RectSize;
+                member.TransformableObject.RotationRadians = localCorners.RectRotation;
+
+                member.MemberCorners = localCorners;
+
+                AffectedArea area = GetTranslationAffectedArea(member.OriginalCorners.Value);
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
+            }
+        }
+
+        if (isTransformingSelection)
+        {
+            infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalSelectionBounds,
+                masterCorners));
+        }
+
+        hasEnqueudImages = false;
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> ApplyTemporarily(Document target)
+    {
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            ShapeCorners localCorners = MasterToMemberCoords(member.MemberCorners);
+            
+            if (member.IsImage)
+            {
+                ChunkyImage targetImage =
+                    DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
+                
+                member.MemberCorners = localCorners;
+                
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId,
+                        DrawImage(member, targetImage), drawOnMask)
+                    .AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                VecD translated = localCorners.RectCenter;
+                member.TransformableObject.Position = translated;
+                member.TransformableObject.Size = localCorners.RectSize;
+                member.TransformableObject.RotationRadians = localCorners.RectRotation; 
+
+                member.MemberCorners = localCorners;
+
+                AffectedArea translationAffectedArea = GetTranslationAffectedArea(member.OriginalCorners.Value);
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, translationAffectedArea));
+            }
+        }
+
+
+        if (isTransformingSelection)
+        {
+            infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalSelectionBounds,
+                masterCorners));
+        }
+
+        return infos;
+    }
+
+    private ShapeCorners MasterToMemberCoords(ShapeCorners memberCorner)
+    {
+        VecD posDiff = masterCorners.RectCenter - lastCorners.RectCenter;
+        VecD sizeDiff = masterCorners.RectSize - lastCorners.RectSize;
+        double rotDiff = masterCorners.RectRotation - lastCorners.RectRotation;
+
+        ShapeCorners localCorners =
+            new ShapeCorners(memberCorner.RectCenter + posDiff, memberCorner.RectSize + sizeDiff)
+                .AsRotated(memberCorner.RectRotation, memberCorner.RectCenter + posDiff)
+                .AsRotated(rotDiff, masterCorners.RectCenter);
+
+        return localCorners;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        List<IChangeInfo> infos = new();
+
+        foreach (var member in memberData)
+        {
+            if (member.SavedChunks is not null)
+            {
+                var storageCopy = member.SavedChunks;
+                var chunks =
+                    DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, member.MemberId, drawOnMask, frame,
+                        ref storageCopy);
+
+                member.SavedChunks = null;
+                infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId, chunks, drawOnMask).AsT1);
+            }
+            else if (member.IsTransformable)
+            {
+                member.TransformableObject.Position = member.OriginalCorners.Value.RectCenter;
+                member.TransformableObject.Size = member.OriginalCorners.Value.RectSize;
+                member.TransformableObject.RotationRadians = member.OriginalCorners.Value.RectRotation;
+
+                AffectedArea area = GetTranslationAffectedArea(member.OriginalCorners.Value);
+                infos.Add(new TransformObject_ChangeInfo(member.MemberId, area));
+            }
+        }
+
+        if (originalPath != null)
+        {
+            (var toDispose, target.Selection.SelectionPath) =
+                (target.Selection.SelectionPath, new VectorPath(originalPath!));
+            toDispose.Dispose();
+        }
+
+        infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
+
+        return infos;
+    }
+
+    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.");
+
+        foreach (var member in memberData)
+        {
+            member.Dispose();
+        }
+    }
+
+    private AffectedArea GetTranslationAffectedArea(ShapeCorners originalCorners)
+    {
+        RectI oldBounds = (RectI)originalCorners.AABBBounds.RoundOutwards();
+
+        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(MemberTransformationData data, ChunkyImage memberImage)
+    {
+        var prevAffArea = memberImage.FindAffectedArea();
+
+        memberImage.CancelChanges();
+        
+        Matrix3X3 globalMatrix = data.GlobalMatrix!.Value; 
+
+        var originalPos = data.ImagePos!.Value;
+
+        if (!keepOriginal)
+            memberImage.EnqueueClearPath(data.OriginalPath!, data.RoundedOriginalBounds!.Value);
+        Matrix3X3 localMatrix = Matrix3X3.CreateTranslation(originalPos.X - (float)data.OriginalBounds.Value.Left,
+            originalPos.Y - (float)data.OriginalBounds.Value.Top);
+        localMatrix = localMatrix.PostConcat(globalMatrix);
+        memberImage.EnqueueDrawImage(localMatrix, data.Image, RegularPaint, false);
+        hasEnqueudImages = true;
+
+        var affectedArea = memberImage.FindAffectedArea();
+        affectedArea.UnionWith(prevAffArea);
+        return affectedArea;
+    }
+}
+
+class MemberTransformationData : IDisposable
+{
+    public Guid MemberId { get; }
+    public ShapeCorners MemberCorners { get; set; }
+
+    public ITransformableObject? TransformableObject { get; private set; }
+    public ShapeCorners? OriginalCorners { get; private set; }
+
+    public CommittedChunkStorage? SavedChunks { get; set; }
+    public VectorPath? OriginalPath { get; set; }
+    public Surface? Image { get; set; }
+    public RectD? OriginalBounds { get; set; }
+    public VecI? ImagePos { get; set; }
+    public bool IsImage => Image != null;
+    public bool IsTransformable => TransformableObject != null;
+    public RectI? RoundedOriginalBounds => (RectI?)OriginalBounds?.RoundOutwards();
+    public Matrix3X3? GlobalMatrix { get; set; }
+
+    public MemberTransformationData(Guid memberId)
+    {
+        MemberId = memberId;
+    }
+
+    public void AddTransformableObject(ITransformableObject transformableObject, ShapeCorners originalCorners)
+    {
+        TransformableObject = transformableObject;
+        OriginalCorners = originalCorners;
+    }
+
+    public void AddImage(Surface img, VecI extractedRectPos)
+    {
+        Image = img;
+        ImagePos = extractedRectPos;
+    }
+
+    public void Dispose()
+    {
+        Image?.Dispose();
+        Image = null;
+        OriginalPath?.Dispose();
+        OriginalPath = null;
+        SavedChunks?.Dispose();
+    }
+}

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

@@ -484,7 +484,7 @@ internal class DocumentOperationsModule : IDocumentOperations
         if (Document.SelectedStructureMember is null ||
             Internals.ChangeController.IsChangeActive && !toolLinked)
             return;
-        Internals.ChangeController.TryStartExecutor(new TransformSelectedAreaExecutor(toolLinked));
+        Internals.ChangeController.TryStartExecutor(new TransformSelectedExecutor(toolLinked));
     }
 
     /// <summary>

+ 24 - 30
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedAreaExecutor.cs → src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -3,20 +3,22 @@ using System.Linq;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Tools;
 using PixiEditor.Numerics;
+using PixiEditor.ViewModels.Document.Nodes;
 
 namespace PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 #nullable enable
-internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
+internal class TransformSelectedExecutor : UpdateableChangeExecutor
 {
-    private Guid[]? membersToTransform;
+    private Dictionary<Guid, ShapeCorners> memberCorners = new(); 
     private IMoveToolHandler? tool;
     public override ExecutorType Type { get; }
 
-    public TransformSelectedAreaExecutor(bool toolLinked)
+    public TransformSelectedExecutor(bool toolLinked)
     {
         Type = toolLinked ? ExecutorType.ToolLinked : ExecutorType.Regular;
     }
@@ -36,40 +38,32 @@ 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(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(
-            new TransformSelectedArea_Action(membersToTransform, corners, tool.KeepOriginalImage, false, document.AnimationHandler.ActiveFrameBindable));
-        return ExecutionState.Success;
-    }
 
-    private RectD GetMembersTightBounds(List<IStructureMemberHandler> members)
-    {
-        RectI rect = members[0].TightBounds ?? RectI.Empty;
-        
-        for (int i = 1; i < members.Count; i++)
+        memberCorners = new();
+        foreach (IStructureMemberHandler member in members)
         {
-            RectI? memberRect = members[i].TightBounds;
-            if (memberRect is not null)
+            ShapeCorners targetCorners = member.TransformationCorners;
+
+            if (member is IRasterLayerHandler && !document.SelectionPathBindable.IsEmpty)
             {
-                rect = rect.Union(memberRect.Value);
-            } 
+                targetCorners = new ShapeCorners(document.SelectionPathBindable.TightBounds);
+            }
+            
+            memberCorners.Add(member.Id, targetCorners);
         }
-
-        return (RectD)rect;
+        
+        ShapeCorners masterCorners = new ShapeCorners(memberCorners.Values.Select(static c => c.AABBBounds).Aggregate((a, b) => a.Union(b)));
+        
+        document.TransformHandler.ShowTransform(DocumentTransformMode.Scale_Rotate_Shear_Perspective, true, masterCorners, Type == ExecutorType.Regular);
+        internals!.ActionAccumulator.AddActions(
+            new TransformSelected_Action(masterCorners, tool.KeepOriginalImage, memberCorners, false, document.AnimationHandler.ActiveFrameBindable));
+        return ExecutionState.Success;
     }
 
     public override void OnTransformMoved(ShapeCorners corners)
     {
         internals!.ActionAccumulator.AddActions(
-            new TransformSelectedArea_Action(membersToTransform!, corners, tool!.KeepOriginalImage, false, document!.AnimationHandler.ActiveFrameBindable));
+            new TransformSelected_Action(corners, tool!.KeepOriginalImage, memberCorners, false, document!.AnimationHandler.ActiveFrameBindable));
     }
 
     public override void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);
@@ -85,7 +79,7 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             tool.TransformingSelectedArea = false;
         }
         
-        internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
+        internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
         onEnded!.Invoke(this);
@@ -103,7 +97,7 @@ internal class TransformSelectedAreaExecutor : UpdateableChangeExecutor
             tool.TransformingSelectedArea = false;
         }
         
-        internals!.ActionAccumulator.AddActions(new EndTransformSelectedArea_Action());
+        internals!.ActionAccumulator.AddActions(new EndTransformSelected_Action());
         internals!.ActionAccumulator.AddFinishedActions();
         document!.TransformHandler.HideTransform();
     }

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

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Handlers;
+
+internal interface IRasterLayerHandler : ILayerHandler
+{
+    
+}

+ 2 - 0
src/PixiEditor/Models/Handlers/IStructureMemberHandler.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using Avalonia.Media.Imaging;
 using ChunkyImageLib;
+using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Models.Layers;
@@ -20,6 +21,7 @@ internal interface IStructureMemberHandler : INodeHandler
     public IDocument Document { get; }
     public bool IsVisibleBindable { get; set; }
     public RectI? TightBounds { get; }
+    public ShapeCorners TransformationCorners { get; }
     public void SetMaskIsVisible(bool infoIsVisible);
     public void SetClipToMemberBelowEnabled(bool infoClipToMemberBelow);
     public void SetBlendMode(BlendMode infoBlendMode);

+ 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, ITransparencyLockableMember
+internal class ImageLayerNodeViewModel : StructureMemberViewModel<ImageLayerNode>, ITransparencyLockableMember, IRasterLayerHandler
 {
     bool lockTransparency;
     public void SetLockTransparency(bool lockTransparency)

+ 5 - 1
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -1,4 +1,5 @@
-using PixiEditor.ChangeableDocument.Actions.Generated;
+using ChunkyImageLib.DataHolders;
+using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.Helpers;
@@ -40,6 +41,9 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
 
     public RectI? TightBounds => Internals.Tracker.Document.FindMember(Id)
         ?.GetTightBounds(Document.AnimationDataViewModel.ActiveFrameBindable);
+    
+    public ShapeCorners TransformationCorners => Internals.Tracker.Document.FindMember(Id)
+        ?.GetTransformationCorners(Document.AnimationDataViewModel.ActiveFrameBindable) ?? new ShapeCorners();
 
     public void SetMaskIsVisible(bool maskIsVisible)
     {

+ 0 - 20
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -24,8 +24,6 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     private bool transformingSelectedArea = false;
     public bool MoveAllLayers { get; set; }
 
-    private bool removeSelection = false;
-
     public override string Icon => PixiPerfectIcons.MousePointer;
 
     public MoveToolViewModel()
@@ -79,30 +77,12 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 
     public override void OnSelected()
     {
-        RectI? bounds = GetSelectedLayersBounds();
-        bool? isEmpty = ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.SelectionPathBindable
-            ?.IsEmpty;
-        if ((!isEmpty.HasValue || isEmpty.Value) && bounds.HasValue)
-        {
-            ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.Operations.Select(bounds.Value);
-            VectorPath path = new VectorPath();
-            path.AddRect(bounds.Value);
-            ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument.UpdateSelectionPath(path);
-            removeSelection = true;
-        }
-
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
     }
 
     public override void OnDeselecting()
     {
         ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TryStopToolLinkedExecutor();
-
-        if (removeSelection)
-        {
-            ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.ClearSelection();
-            removeSelection = false;
-        }
     }
 
     private static RectI? GetSelectedLayersBounds()