Browse Source

Merge pull request #1252 from PixiEditor/fixes/2.0.1.7

Fixes/2.0.1.7
Krzysztof Krysiński 1 month ago
parent
commit
9011c1da6d
24 changed files with 494 additions and 123 deletions
  1. 141 11
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 1
      src/Drawie
  3. 36 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  4. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  5. 1 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  6. 21 19
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  7. 6 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs
  8. 17 0
      src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs
  9. 47 26
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  10. 6 1
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  11. 64 12
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  12. 3 0
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  13. 38 8
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  14. 23 0
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  15. 23 11
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  16. 2 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  17. 17 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  18. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs
  19. 3 24
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  20. 8 5
      src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs
  21. 1 0
      src/PixiEditor/Views/Input/EditableTextBlock.axaml
  22. 10 0
      src/PixiEditor/Views/Input/EditableTextBlock.axaml.cs
  23. 11 0
      src/PixiEditor/Views/Layers/FolderControl.axaml.cs
  24. 7 1
      src/PixiEditor/Views/Layers/LayersManager.axaml

+ 141 - 11
src/ChunkyImageLib/ChunkyImage.cs

@@ -64,8 +64,10 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private readonly object lockObject = new();
     private int commitCounter = 0;
 
-    private RectI cachedPreciseBounds = RectI.Empty;
-    private int lastBoundsCacheHash = -1;
+    private RectI cachedPreciseCommitedBounds = RectI.Empty;
+    private RectI cachedPreciseLatestBounds = RectI.Empty;
+    private int lastCommitedBoundsCacheHash = -1;
+    private int lastLatestBoundsCacheHash = -1;
 
     public const int FullChunkSize = ChunkPool.FullChunkSize;
     private static Paint ClippingPaint { get; } = new Paint() { BlendMode = BlendMode.DstIn };
@@ -200,9 +202,9 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         {
             ThrowIfDisposed();
 
-            if (lastBoundsCacheHash == GetCacheHash())
+            if (lastCommitedBoundsCacheHash == GetCacheHash())
             {
-                return cachedPreciseBounds;
+                return cachedPreciseCommitedBounds;
             }
 
             var chunkSize = suggestedResolution.PixelSize();
@@ -250,8 +252,120 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
             preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
 
-            cachedPreciseBounds = preciseBounds.GetValueOrDefault();
-            lastBoundsCacheHash = GetCacheHash();
+            cachedPreciseCommitedBounds = preciseBounds.GetValueOrDefault();
+            lastCommitedBoundsCacheHash = GetCacheHash();
+
+            return preciseBounds;
+        }
+    }
+
+    public RectI? FindTightLatestBounds(ChunkResolution suggestedResolution = ChunkResolution.Full,
+        bool fallbackToChunkAligned = false)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+
+            if(queuedOperations.Count == 0)
+            {
+                return FindTightCommittedBounds(suggestedResolution, fallbackToChunkAligned);
+            }
+
+            /*if (lastLatestBoundsCacheHash == GetCacheHash())
+            {
+                return cachedPreciseLatestBounds;
+            }*/
+
+            var chunkSize = suggestedResolution.PixelSize();
+            var multiplier = suggestedResolution.Multiplier();
+            RectI scaledLatestSize = (RectI)(new RectD(VecI.Zero, LatestSize * multiplier)).RoundOutwards();
+
+            RectI? preciseBounds = null;
+
+            var possibleChunks = new HashSet<VecI>();
+            foreach (var (pos, _) in committedChunks[ChunkResolution.Full])
+                possibleChunks.Add(pos);
+
+            foreach (var (pos, _) in latestChunks[ChunkResolution.Full])
+                possibleChunks.Add(pos);
+
+            foreach (var chunkPos in possibleChunks)
+            {
+                var committedChunk = GetCommittedChunk(chunkPos, suggestedResolution);
+                var latestChunk = GetLatestChunk(chunkPos, suggestedResolution);
+
+                Chunk? chunk;
+                bool isTempChunk = false;
+
+                if (latestChunk != null && committedChunk != null)
+                {
+                    // both exist, need to merge
+                    var tempChunk = Chunk.Create(ProcessingColorSpace, suggestedResolution);
+                    tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(committedChunk.Surface.DrawingSurface, 0, 0,
+                        ReplacingPaint);
+                    blendModePaint.BlendMode = blendMode;
+                    tempChunk.Surface.DrawingSurface.Canvas.DrawSurface(latestChunk.Surface.DrawingSurface, 0, 0,
+                        blendModePaint);
+                    if (lockTransparency)
+                        OperationHelper.ClampAlpha(tempChunk.Surface.DrawingSurface,
+                            committedChunk.Surface.DrawingSurface);
+                    chunk = tempChunk;
+                    isTempChunk = true;
+                }
+                else if (latestChunk != null)
+                {
+                    chunk = latestChunk;
+                }
+                else
+                {
+                    chunk = committedChunk;
+                }
+
+
+                if (chunk != null)
+                {
+                    RectI visibleArea = new RectI(chunkPos * chunkSize, new VecI(chunkSize))
+                        .Intersect(scaledLatestSize).Translate(-chunkPos * chunkSize);
+
+                    RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
+                    if (chunkPreciseBounds is null)
+                        continue;
+                    RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * chunkSize);
+
+                    preciseBounds ??= globalChunkBounds;
+                    preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
+
+                    if (isTempChunk)
+                    {
+                        chunk.Dispose();
+                    }
+                }
+                else
+                {
+                    if (fallbackToChunkAligned)
+                    {
+                        return FindChunkAlignedMostUpToDateBounds();
+                    }
+
+                    RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
+                        .Intersect(new RectI(VecI.Zero, LatestSize)).Translate(-chunkPos * FullChunkSize);
+
+                    RectI? chunkPreciseBounds = chunk.FindPreciseBounds(visibleArea);
+                    if (chunkPreciseBounds is null)
+                        continue;
+                    RectI globalChunkBounds = (RectI)chunkPreciseBounds.Value.Scale(multiplier)
+                        .Offset(chunkPos * chunkSize).RoundOutwards();
+
+                    preciseBounds ??= globalChunkBounds;
+                    preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
+                }
+            }
+
+            preciseBounds = (RectI?)preciseBounds?.Scale(suggestedResolution.InvertedMultiplier()).RoundOutwards();
+            preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, LatestSize));
+
+            cachedPreciseLatestBounds = preciseBounds.GetValueOrDefault();
+            lastLatestBoundsCacheHash = GetCacheHash();
 
             return preciseBounds;
         }
@@ -1608,10 +1722,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
 
     public int GetCacheHash()
     {
-        return commitCounter + queuedOperations.Count + operationCounter + activeClips.Count
-               + (int)blendMode + (lockTransparency ? 1 : 0)
-               + (horizontalSymmetryAxis is not null ? (int)(horizontalSymmetryAxis * 100) : 0)
-               + (verticalSymmetryAxis is not null ? (int)(verticalSymmetryAxis * 100) : 0)
-               + (clippingPath is not null ? 1 : 0);
+        HashCode hash = new HashCode();
+        hash.Add(commitCounter);
+        hash.Add(queuedOperations.Count);
+        hash.Add(operationCounter);
+
+        foreach (var queuedOperation in queuedOperations)
+        {
+            hash.Add(queuedOperation.affectedArea.GlobalArea?.GetHashCode() ?? 0);
+            hash.Add(queuedOperation.operation.GetHashCode());
+        }
+
+        hash.Add(activeClips.Count);
+        hash.Add((int)blendMode);
+        hash.Add(lockTransparency);
+        if (horizontalSymmetryAxis is not null)
+            hash.Add((int)(horizontalSymmetryAxis * 100));
+        if (verticalSymmetryAxis is not null)
+            hash.Add((int)(verticalSymmetryAxis * 100));
+        if (clippingPath is not null)
+            hash.Add(1);
+        return hash.ToHashCode();
     }
 }

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 753784f70c3a455dce172ad2e92be63329f9d4c3
+Subproject commit 64373b7e3e5131a31e8a8d1d394267bd3f058f75

+ 36 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -182,6 +182,42 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource
         return null;
     }
 
+    public override ShapeCorners GetTransformationCorners(KeyFrameTime frameTime)
+    {
+        RectD? bounds = null;
+        if (!IsVisible.Value)
+            return new ShapeCorners();
+
+        if (Content.Connection != null)
+        {
+            Content.Connection.Node.TraverseBackwards(
+                (n, input) =>
+                {
+                    if (n is StructureNode { IsVisible.Value: true } structureNode)
+                    {
+                        ShapeCorners childBounds = structureNode.GetTransformationCorners(frameTime);
+                        if (childBounds != default)
+                        {
+                            if (bounds == null)
+                            {
+                                bounds = childBounds.AABBBounds;
+                            }
+                            else
+                            {
+                                bounds = bounds.Value.Union(childBounds.AABBBounds);
+                            }
+                        }
+                    }
+
+                    return true;
+                }, FilterInvisibleFolders);
+
+            return bounds != null ? new ShapeCorners(bounds.Value) : new ShapeCorners();
+        }
+
+        return new ShapeCorners();
+    }
+
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)
     {
         RectD? bounds = null;

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

@@ -47,7 +47,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightCommittedBounds();
+        return (RectD?)GetLayerImageAtFrame(frameTime.Frame).FindTightLatestBounds();
     }
 
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -114,6 +114,7 @@ public static class FloodFillHelper
             {
                 var chunk = Chunk.Create(document.ProcessingColorSpace);
                 chunk.Surface.DrawingSurface.Canvas.Clear(Colors.Transparent);
+
                 drawingChunks[chunkPos] = chunk;
             }
 

+ 21 - 19
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -65,7 +65,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     {
         if (memberData.Count == 0)
             return false;
-        
+
         originalCornersSize = masterCorners.RectSize;
         RectD tightBoundsWithSelection = default;
         bool hasSelection = target.Selection.SelectionPath is { IsEmpty: false };
@@ -77,9 +77,9 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             originalSelectionBounds = tightBoundsWithSelection;
             selectionAwareSize = tightBoundsWithSelection.Size;
             isTransformingSelection = true;
-            
+
             tightBoundsSize = tightBoundsWithSelection.Size;
-            cornersToSelectionOffset = new RectD(masterCorners.TopLeft - tightBoundsWithSelection.TopLeft, 
+            cornersToSelectionOffset = new RectD(masterCorners.TopLeft - tightBoundsWithSelection.TopLeft,
                 tightBoundsSize - masterCorners.RectSize);
         }
 
@@ -90,30 +90,32 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
         }
 
         StructureNode firstLayer = foundMember;
-        RectD tightBounds = firstLayer.GetTightBounds(frame) ?? default;
+        RectD tightBounds = firstLayer.GetTransformationCorners(frame).AABBBounds;
 
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         {
             tightBounds = vectorLayer.EmbeddedShapeData?.GeometryAABB ?? default;
+            hasSelection = false;
+            isTransformingSelection = false;
         }
 
         for (var i = 1; i < memberData.Count; i++)
         {
             StructureNode layer = target.FindMemberOrThrow(memberData[i].MemberId);
-            
-            var layerTightBounds = layer.GetTightBounds(frame);
-            
+
+            var layerTightBounds = layer.GetTransformationCorners(frame).AABBBounds;
+
             if (tightBounds == default)
             {
-                tightBounds = layerTightBounds.GetValueOrDefault();
+                tightBounds = layerTightBounds;
             }
 
-            if (layerTightBounds is not null)
+            if (layerTightBounds != default)
             {
-                tightBounds = tightBounds.Union(layerTightBounds.Value);
+                tightBounds = tightBounds.Union(layerTightBounds);
             }
         }
-        
+
         if (tightBounds == default)
             return false;
 
@@ -141,10 +143,10 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
         MemberTransformationData member,
         ITransformableObject transformable, RectD tightBounds)
     {
-        member.OriginalBounds = tightBounds; 
+        member.OriginalBounds = tightBounds;
         VecD posRelativeToMaster = member.OriginalBounds.Value.TopLeft - masterCorners.TopLeft;
 
-        member.OriginalPos = (VecI)posRelativeToMaster;
+        member.OriginalPos = posRelativeToMaster;
         member.AddTransformableObject(transformableId, transformable.TransformationMatrix);
     }
 
@@ -158,7 +160,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
         if (pathToExtract == null)
         {
-            RectD tightBounds = layer.GetTightBounds(frame).GetValueOrDefault();
+            RectD tightBounds = layer.GetTransformationCorners(frame).AABBBounds;
             pathToExtract = new VectorPath();
             pathToExtract.AddRect(tightBounds.RoundOutwards());
         }
@@ -187,14 +189,14 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
             if (member.IsImage)
             {
-                localMatrix = 
+                localMatrix =
                     Matrix3X3.CreateTranslation(
                         (float)-cornersToSelectionOffset.TopLeft.X, (float)-cornersToSelectionOffset.TopLeft.Y)
                         .PostConcat(
                     Matrix3X3.CreateTranslation(
                     (float)member.OriginalPos.Value.X - (float)member.OriginalBounds.Value.Left,
                     (float)member.OriginalPos.Value.Y - (float)member.OriginalBounds.Value.Top));
-                
+
                 localMatrix = localMatrix.PostConcat(selectionAwareSize.Length > 0 ? globalMatrixWithSelection : tightBoundsGlobalMatrix);
             }
             else if (member.OriginalMatrix is not null)
@@ -205,7 +207,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
                     localMatrix = localMatrix.PostConcat(Matrix3X3.CreateTranslation(
                             (float)member.OriginalPos.Value.X - (float)member.OriginalBounds.Value.Left,
                             (float)member.OriginalPos.Value.Y - (float)member.OriginalBounds.Value.Top))
-                        .PostConcat(tightBoundsGlobalMatrix);
+                        .PostConcat(selectionAwareSize.Length > 0 ? globalMatrixWithSelection : tightBoundsGlobalMatrix);
                 }
                 else
                 {
@@ -282,7 +284,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             infos.Add(SelectionChangeHelper.DoSelectionTransform(target, originalPath!, originalSelectionBounds,
                 masterCorners, cornersToSelectionOffset, originalCornersSize));
         }
-        
+
         hasEnqueudImages = false;
         ignoreInUndo = false;
         return infos;
@@ -365,7 +367,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
         }
 
         infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
-        
+
         appliedOnce = false;
 
         return infos;

+ 6 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -116,7 +116,7 @@ public static class NodeOperations
         List<IChangeInfo> changes = new();
         IOutputProperty? previouslyConnected = null;
 
-        if(parentInput == null) return changes;
+        if (parentInput == null) return changes;
 
         if (parentInput.Connection != null)
         {
@@ -240,6 +240,11 @@ public static class NodeOperations
     public static List<IChangeInfo> RevertPositions(Dictionary<Guid, VecD> positions, IReadOnlyDocument target)
     {
         List<IChangeInfo> changes = new();
+        if (positions == null)
+        {
+            return changes;
+        }
+
         foreach (var (guid, position) in positions)
         {
             var node = target.FindNode(guid) as Node;

+ 17 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportFolder_Change.cs

@@ -152,6 +152,11 @@ internal class ImportFolder_Change : Change
                     counter++;
                 }
 
+                if (node is LayerNode layerNode)
+                {
+                    ResizeImageData(layerNode, target.Size);
+                }
+
                 contentGuidToNodeMap[x.Id] = node.Id;
 
                 target.NodeGraph.AddNode(node);
@@ -179,6 +184,18 @@ internal class ImportFolder_Change : Change
         }
     }
 
+    private void ResizeImageData(LayerNode layerNode, VecI docSize)
+    {
+        foreach (var imageData in layerNode.KeyFrames)
+        {
+            if (imageData.Data is ChunkyImage img)
+            {
+                img.EnqueueResize(docSize);
+                img.CommitChanges();
+            }
+        }
+    }
+
     public override void Dispose()
     {
         base.Dispose();

+ 47 - 26
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -81,6 +81,11 @@ public class DocumentRenderer : IDisposable
         {
             renderTexture.DrawingSurface.Canvas.Restore();
             toRenderOn.Canvas.Restore();
+            if (membersOnlyGraph is IDisposable disposableGraph)
+            {
+                disposableGraph.Dispose();
+            }
+
             IsBusy = false;
         }
     }
@@ -142,38 +147,50 @@ public class DocumentRenderer : IDisposable
 
         fullGraph.OutputNode.TraverseBackwards((node, input) =>
         {
-            if (node is StructureNode structureNode && membersToCombine != null &&
-                !membersToCombine.Contains(structureNode.Id))
-            {
-                return true;
-            }
+            return CloneElement(membersToCombine, fullGraph, node, membersOnlyGraph, input, nodeMapping);
+        });
 
-            if (node is LayerNode layer)
-            {
-                LayerNode clone = (LayerNode)layer.Clone();
-                membersOnlyGraph.AddNode(clone);
+        return membersOnlyGraph;
+    }
 
+    private static bool CloneElement(HashSet<Guid>? membersToCombine, IReadOnlyNodeGraph fullGraph, IReadOnlyNode node,
+        NodeGraph membersOnlyGraph, IInputProperty input, Dictionary<Guid, Guid> nodeMapping,
+        IInputProperty? fallback = null)
+    {
+        if (node is StructureNode structureNode && membersToCombine != null &&
+            !membersToCombine.Contains(structureNode.Id))
+        {
+            return true;
+        }
 
-                IInputProperty targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+        if (node is LayerNode layer)
+        {
+            LayerNode clone = (LayerNode)layer.Clone();
+            membersOnlyGraph.AddNode(clone);
 
-                clone.Output.ConnectTo(targetInput);
-                nodeMapping[layer.Id] = clone.Id;
-            }
-            else if (node is FolderNode folder)
-            {
-                FolderNode clone = (FolderNode)folder.Clone();
-                membersOnlyGraph.AddNode(clone);
+            IInputProperty targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping, fallback);
 
-                var targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping);
+            clone.Output.ConnectTo(targetInput);
+            nodeMapping[layer.Id] = clone.Id;
+        }
+        else if (node is FolderNode folder)
+        {
+            FolderNode clone = (FolderNode)folder.Clone();
+            membersOnlyGraph.AddNode(clone);
 
-                clone.Output.ConnectTo(targetInput);
-                nodeMapping[folder.Id] = clone.Id;
-            }
+            var targetInput = GetTargetInput(input, fullGraph, membersOnlyGraph, nodeMapping, fallback);
 
-            return true;
-        });
+            clone.Output.ConnectTo(targetInput);
+            nodeMapping[folder.Id] = clone.Id;
 
-        return membersOnlyGraph;
+            folder.Content.Connection?.Node.TraverseBackwards((childNode, childInput) =>
+            {
+                return CloneElement(membersToCombine, fullGraph, childNode, membersOnlyGraph, childInput,
+                    nodeMapping, clone.Content);
+            });
+        }
+
+        return true;
     }
 
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize,
@@ -238,9 +255,13 @@ public class DocumentRenderer : IDisposable
     private static IInputProperty GetTargetInput(IInputProperty? input,
         IReadOnlyNodeGraph sourceGraph,
         NodeGraph membersOnlyGraph,
-        Dictionary<Guid, Guid> nodeMapping)
+        Dictionary<Guid, Guid> nodeMapping, IInputProperty? fallback = null)
     {
-        if (input == null)
+        if (input == null && fallback != null)
+        {
+            return fallback;
+        }
+        else if (input == null)
         {
             if (membersOnlyGraph.OutputNode is IRenderInput inputNode) return inputNode.Background;
 

+ 6 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -20,7 +20,12 @@
         }
       },
       "Select",
-      "MagicWand",
+      {
+        "ToolName": "MagicWand",
+        "Settings": {
+          "Tolerance": 0
+        }
+      },
       "Lasso",
       {
         "ToolName": "FloodFill",

+ 64 - 12
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -249,22 +249,40 @@ internal static class ClipboardController
         if (targetDoc != null && !hadSelection && pasteAsNew && layerIds is { Length: > 0 } &&
             (!hasPos || await AllMatchesPos(layerIds, data, targetDoc)))
         {
-            foreach (var layerId in layerIds)
+            List<Guid> adjustedLayerIds = AdjustIdsForImport(layerIds, targetDoc, document);
+            List<Guid?> newIds = new();
+            using var block = document.Operations.StartChangeBlock();
+            foreach (var layerId in adjustedLayerIds)
             {
                 if (targetDoc.StructureHelper.Find(layerId) == null)
                     continue;
 
                 if (sourceDocument == document.Id)
                 {
-                    document.Operations.DuplicateMember(layerId);
+                    newIds.Add(document.Operations.DuplicateMember(layerId));
                 }
                 else
                 {
-                    document.Operations.ImportMember(layerId, targetDoc);
+                    newIds.Add(document.Operations.ImportMember(layerId, targetDoc));
                 }
             }
 
             manager.Owner.ToolsSubViewModel.SetActiveTool<MoveToolViewModel>(false);
+            Guid? mainGuid = newIds.FirstOrDefault(x => x != null);
+            if (mainGuid != null)
+            {
+                document.Operations.ClearSoftSelectedMembers();
+                document.Operations.SetSelectedMember(mainGuid.Value);
+
+                Guid[] restGuids = newIds.Where(x => x != null && x != mainGuid).Select(x => x.Value).ToArray();
+                if (restGuids.Length > 0)
+                {
+                    foreach (var guid in restGuids)
+                    {
+                        document.Operations.AddSoftSelectedMember(guid);
+                    }
+                }
+            }
 
             return true;
         }
@@ -278,7 +296,7 @@ internal static class ClipboardController
             var dataImage = images[0];
             var position = dataImage.Position;
 
-            if (document.SizeBindable.X < position.X || document.SizeBindable.Y < position.Y)
+            if (document.SizeBindable.X < position.X || document.SizeBindable.Y < position.Y || !hasPos)
             {
                 position = VecI.Zero;
             }
@@ -295,12 +313,18 @@ internal static class ClipboardController
 
                 manager.Owner.ToolsSubViewModel.SetActiveTool<MoveToolViewModel>(false);
                 document.Operations.SetSelectedMember(guid.Value);
-                document.Operations.PasteImageWithTransform(dataImage.Image, position, guid.Value, false);
+                document.Operations.InvokeCustomAction(() =>
+                {
+                    document.Operations.PasteImageWithTransform(dataImage.Image, position, guid.Value, false);
+                });
             }
             else
             {
                 manager.Owner.ToolsSubViewModel.SetActiveTool<MoveToolViewModel>(false);
-                document.Operations.PasteImageWithTransform(dataImage.Image, position);
+                document.Operations.InvokeCustomAction(() =>
+                {
+                    document.Operations.PasteImageWithTransform(dataImage.Image, position);
+                });
             }
 
             return true;
@@ -310,6 +334,34 @@ internal static class ClipboardController
         return true;
     }
 
+    private static List<Guid> AdjustIdsForImport(Guid[] layerIds, IDocument targetDoc, DocumentViewModel document)
+    {
+        // This should only copy root level layers
+        List<Guid> adjustedIds = new();
+        foreach (var layerId in layerIds)
+        {
+           var parents = targetDoc.StructureHelper.GetParents(layerId);
+           if (parents.Count == 0)
+           {
+                adjustedIds.Add(layerId);
+                continue;
+           }
+
+           // only include if no parent is in layerIds
+           if (!parents.Any(x => layerIds.Contains(x.Id)))
+           {
+                adjustedIds.Add(layerId);
+           }
+        }
+
+        var all = targetDoc.StructureHelper.GetAllMembersInOrder();
+
+        // order by document order
+        adjustedIds = adjustedIds.OrderBy(x => all.FindIndex(y => y.Id == x)).ToList();
+
+        return adjustedIds;
+    }
+
     private static async Task<bool> AllMatchesPos(Guid[] layerIds, IImportObject[] dataFormats, IDocument doc)
     {
         var dataObjectWithPos = dataFormats.FirstOrDefault(x => x.Contains(ClipboardDataFormats.PositionFormat));
@@ -320,7 +372,7 @@ internal static class ClipboardController
             pos = await GetVecD(ClipboardDataFormats.PositionFormat, dataFormats);
         }
 
-        RectD? tightBounds = null;
+        RectD? transformBounds = null;
         for (var i = 0; i < layerIds.Length; i++)
         {
             var layerId = layerIds[i];
@@ -329,17 +381,17 @@ internal static class ClipboardController
 
             if (layer == null) return false;
 
-            if (tightBounds == null)
+            if (transformBounds == null)
             {
-                tightBounds = layer.TightBounds;
+                transformBounds = layer.TransformationCorners.AABBBounds;
             }
-            else if (layer.TightBounds.HasValue)
+            else if (!layer.TransformationCorners.HasNaNOrInfinity)
             {
-                tightBounds = tightBounds.Value.Union(layer.TightBounds.Value);
+                transformBounds = transformBounds.Value.Union(layer.TransformationCorners.AABBBounds);
             }
         }
 
-        return tightBounds.HasValue && tightBounds.Value.Pos.AlmostEquals(pos);
+        return transformBounds.HasValue && transformBounds.Value.Pos.AlmostEquals(pos);
     }
 
     private static async Task<Guid[]> GetLayerIds(IImportObject[] formats)

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

@@ -824,6 +824,9 @@ internal class DocumentUpdater
     private void ProcessNodePosition(NodePosition_ChangeInfo info)
     {
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
+        if (node == null)
+            return;
+
         node.SetPosition(info.NewPosition);
     }
 

+ 38 - 8
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -82,8 +82,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// <param name="lastTransformRect"></param>
     public void DeleteSelectedPixels(int frame, bool clearSelection = false, RectD? lastTransformRect = null)
     {
-        var member = Document.SelectedStructureMember;
-        if (Internals.ChangeController.IsBlockingChangeActive || member is null)
+        var members = Document.SelectedMembers;
+        if (Internals.ChangeController.IsBlockingChangeActive || members?.Count == 0)
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
@@ -99,11 +99,40 @@ internal class DocumentOperationsModule : IDocumentOperations
             }
         }
 
-        bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
-        if (drawOnMask && !member.HasMaskBindable)
-            return;
-        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id,
-            selection, drawOnMask, frame));
+        foreach (var memberGuid in members)
+        {
+            var member = Document.StructureHelper.FindNode<IStructureMemberHandler>(memberGuid);
+            if (member is null)
+                continue;
+
+            bool drawOnMask = member is not ILayerHandler layer || layer.ShouldDrawOnMask;
+            if (drawOnMask && !member.HasMaskBindable)
+                return;
+
+            if (member is not IRasterLayerHandler)
+            {
+                var bounds = member.TightBounds;
+                if (bounds is null)
+                    continue;
+
+                using var rectPath = new VectorPath();
+                rectPath.AddRect(new RectD(bounds.Value.Pos, bounds.Value.Size));
+
+                using var opped = selection.Op(rectPath, VectorPathOp.Intersect);
+
+                if (!opped.IsEmpty)
+                {
+                    // TODO: Cut vectors
+                    Internals.ActionAccumulator.AddActions(new DeleteNode_Action(member.Id));
+                }
+            }
+            else
+            {
+                Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id,
+                    selection, drawOnMask, frame));
+            }
+        }
+
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
@@ -268,7 +297,8 @@ internal class DocumentOperationsModule : IDocumentOperations
         {
             Internals.ActionAccumulator.AddFinishedActions(
                 new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
-                new SetSelectedMember_PassthroughAction(newGuid));
+                new SetSelectedMember_PassthroughAction(newGuid),
+                new CreateAnimationDataFromFolder_Action(newGuid));
         }
 
         return newGuid;

+ 23 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -274,4 +274,27 @@ internal class DocumentStructureModule
 
         return children;
     }
+
+    public List<IStructureMemberHandler> GetAllMembersInOrder()
+    {
+        var allMembers = doc.NodeGraphHandler.StructureTree.Members;
+
+        List<IStructureMemberHandler> membersInOrder = new List<IStructureMemberHandler>();
+        for (var i = allMembers.Count - 1; i >= 0; i--)
+        {
+            var member = allMembers[i];
+            membersInOrder.Add(member);
+
+            if (member is IFolderHandler folder)
+            {
+                membersInOrder.AddRange(GetFolderChildren(folder.Id));
+            }
+            else
+            {
+                membersInOrder.Add(member);
+            }
+        }
+
+        return membersInOrder;
+    }
 }

+ 23 - 11
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -174,10 +174,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             .ToList();
 
         var nonSelected = orderedBySize.Where(x => x != document.SelectedStructureMember
-                                              && !document.SoftSelectedStructureMembers.Contains(x));
+                                                   && !document.SoftSelectedStructureMembers.Contains(x));
 
         var smallestSizeDifferenceList = nonSelected
-            .Where(x => x.TightBounds is not null && (x.TightBounds.Value.Size.Length + (x is IFolderHandler ? 1 : 0)) <= (document.SelectedStructureMember?.TightBounds?.Size.Length ?? double.MaxValue))
+            .Where(x => x.TightBounds is not null &&
+                        (x.TightBounds.Value.Size.Length + (x is IFolderHandler ? 1 : 0)) <=
+                        (document.SelectedStructureMember?.TightBounds?.Size.Length ?? double.MaxValue))
             .ToList();
 
         if (!smallestSizeDifferenceList.Any() && orderedBySize.Count != 0)
@@ -198,7 +200,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
             }
             else
             {
-                if (document.SoftSelectedStructureMembers.Contains(topMost) || document.SelectedStructureMember?.Id == topMost.Id)
+                if (document.SoftSelectedStructureMembers != null && (document.SoftSelectedStructureMembers.Contains(topMost)) || document.SelectedStructureMember?.Id == topMost.Id)
                 {
                     Deselect(smallestSizeDifferenceList);
                 }
@@ -216,7 +218,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 Deselect(topMostList);
             }
         }
-        else if(!topMostWithinClick.Any())
+        else if (!topMostWithinClick.Any())
         {
             document?.Operations.ClearSoftSelectedMembers();
             document?.Operations.SetSelectedMember(Guid.Empty);
@@ -242,28 +244,36 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
     private void Deselect(List<IStructureMemberHandler> topMostWithinClick)
     {
-        var topMost = topMostWithinClick.FirstOrDefault();
+        if (document is null)
+            return;
+
+        var topMost = topMostWithinClick?.FirstOrDefault();
         if (topMost is not null)
         {
             bool deselectingWasMain = document.SelectedStructureMember?.Id == topMost.Id;
             if (deselectingWasMain)
             {
-                Guid? nextMain = document.SoftSelectedStructureMembers.FirstOrDefault().Id;
-                List<Guid> softSelected = document.SoftSelectedStructureMembers
+                Guid? nextMain = document.SoftSelectedStructureMembers?.FirstOrDefault()?.Id;
+                List<Guid> softSelected = document.SoftSelectedStructureMembers?
                     .Select(x => x.Id).Where(x => x != nextMain.Value).ToList();
 
                 document.Operations.ClearSoftSelectedMembers();
-                document.Operations.SetSelectedMember(nextMain.Value);
+                if (nextMain.HasValue)
+                {
+                    document.Operations.SetSelectedMember(nextMain.Value);
+                }
 
                 foreach (var guid in softSelected)
                 {
+                    if (guid == Guid.Empty) continue;
+
                     document.Operations.AddSoftSelectedMember(guid);
                 }
             }
             else
             {
-                List<Guid> softSelected = document.SoftSelectedStructureMembers
-                    .Select(x => x.Id).Where(x => x != topMost.Id).ToList();
+                List<Guid> softSelected = document.SoftSelectedStructureMembers?
+                    .Select(x => x.Id).Where(x => x != topMost?.Id).ToList();
 
                 document.Operations.ClearSoftSelectedMembers();
 
@@ -341,7 +351,8 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
         if (!movedOnce)
         {
             internals!.ActionAccumulator.AddActions(
-                new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, tool.BilinearTransform, memberCorners, false,
+                new TransformSelected_Action(lastCorners, tool.KeepOriginalImage, tool.BilinearTransform, memberCorners,
+                    false,
                     document.AnimationHandler.ActiveFrameBindable));
 
             movedOnce = true;
@@ -456,6 +467,7 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
     public void OnLineOverlayMoved(VecD start, VecD end) { }
 
     public void OnSelectedObjectNudged(VecI distance) => document!.TransformHandler.Nudge(distance);
+
     public bool IsTransformingMember(Guid id)
     {
         if (document!.SelectedStructureMember is null)

+ 2 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -130,8 +130,9 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
         clickPos = args.PositionOnCanvas;
         var firstLayer = topMostWithinClick.FirstOrDefault();
         args.Handled = firstLayer != null;
-        if (firstLayer is not IVectorLayerHandler layerHandler)
+        if (firstLayer is not IVectorLayerHandler layerHandler || layerHandler.GetShapeData(document.AnimationHandler.ActiveFrameTime) is not TextVectorData)
         {
+            args.Handled = false;
             if (document.TextOverlayHandler.IsActive)
             {
                 args.Handled = true;

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

@@ -439,7 +439,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 {
                     object value =
                         SerializationUtil.Deserialize(propertyValue.Value, config, allFactories, serializerData);
-                    acc.AddActions(new UpdatePropertyValue_Action(guid, propertyValue.Key, value), new EndUpdatePropertyValue_Action());
+                    acc.AddActions(new UpdatePropertyValue_Action(guid, propertyValue.Key, value),
+                        new EndUpdatePropertyValue_Action());
                 }
             }
 
@@ -805,6 +806,21 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (bounds.IsZeroOrNegativeArea)
             return new None();
 
+        var toAdd = new HashSet<Guid>();
+        foreach (var layer in selectedLayers)
+        {
+            var parents = StructureHelper.GetParents(layer);
+            if (parents is null)
+                continue;
+
+            foreach (var parent in parents)
+            {
+                toAdd.Add(parent.Id);
+            }
+        }
+
+        selectedLayers.UnionWith(toAdd);
+
         RectI finalBounds = default;
 
         for (int i = 0; i < selectedLayers.Count; i++)

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs

@@ -9,8 +9,15 @@ namespace PixiEditor.ViewModels.Document.Nodes;
 [NodeViewModel("FOLDER_NODE", "STRUCTURE", PixiPerfectIcons.Folder)]
 internal class FolderNodeViewModel : StructureMemberViewModel<FolderNode>, IFolderHandler
 {
+    private bool isOpen;
     public ObservableCollection<IStructureMemberHandler> Children { get; } = new();
 
+    public bool IsOpen
+    {
+        get => isOpen;
+        set => SetProperty(ref isOpen, value);
+    }
+
     public int CountChildrenRecursive()
     {
         int count = 0;

+ 3 - 24
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -96,29 +96,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
 
         Dispatcher.UIThread.InvokeAsync(async () =>
         {
-            Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
             await ClipboardController.TryPasteFromClipboard(doc, Owner.DocumentManagerSubViewModel, pasteAsNewLayer);
-
-            // Leaving the code below commented out in case something breaks.
-            // It instantly ended paste image operation after I made it interruptable,
-            // I did test it, and it seems everything works fine without it.
-            /*doc.Operations.InvokeCustomAction(
-                () =>
-            {
-                Guid[] newGuids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
-
-                var diff = newGuids.Except(guids).ToArray();
-                if (diff.Length > 0)
-                {
-                    doc.Operations.ClearSoftSelectedMembers();
-                    doc.Operations.SetSelectedMember(diff[0]);
-
-                    for (int i = 1; i < diff.Length; i++)
-                    {
-                        doc.Operations.AddSoftSelectedMember(diff[i]);
-                    }
-                }
-            }, false);*/
         });
     }
 
@@ -398,7 +376,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         var selectedNodes = doc.NodeGraph.AllNodes.Where(x => x.IsNodeSelected).Select(x => x.Id).ToArray();
         if (selectedNodes.Length == 0)
             return;
-        
+
         await ClipboardController.CopyNodes(selectedNodes, doc.Id);
 
         areNodesInClipboard = true;
@@ -583,7 +561,8 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         return ColorSearchResult.GetIcon(targetColor.ToOpaqueMediaColor().ToOpaqueColor());
     }
 
-    private void ConnectRelatedNodes(IDocument sourceDoc, DocumentViewModel targetDoc, Dictionary<Guid, Guid> nodeMapping)
+    private void ConnectRelatedNodes(IDocument sourceDoc, DocumentViewModel targetDoc,
+        Dictionary<Guid, Guid> nodeMapping)
     {
         foreach (var connection in sourceDoc.NodeGraphHandler.Connections)
         {

+ 8 - 5
src/PixiEditor/ViewModels/Tools/Tools/MoveToolViewModel.cs

@@ -79,18 +79,21 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
 
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
-        DuplicateOnMove = ctrlIsDown && argsKey is Key.None or Key.LeftCtrl or Key.RightCtrl && !shiftIsDown && !altIsDown;
+        DuplicateOnMove = ctrlIsDown && argsKey is Key.None or Key.LeftCtrl or Key.RightCtrl && !shiftIsDown &&
+                          !altIsDown;
     }
-    
+
     protected override void OnSelected(bool restoring)
     {
-        if (TransformingSelectedArea)
+        if (TransformingSelectedArea || restoring)
         {
             return;
         }
 
         DuplicateOnMove = false;
-        ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument?.Operations.TransformSelectedArea(true);
+        var activeDoc = ViewModelMain.Current.DocumentManagerSubViewModel.ActiveDocument;
+
+        activeDoc?.Operations.TransformSelectedArea(true);
     }
 
     protected override void OnDeselecting(bool transient)
@@ -125,7 +128,7 @@ internal class MoveToolViewModel : ToolViewModel, IMoveToolHandler
     {
         DuplicateOnMove = false;
     }
-    
+
     public override void OnPreRedoInlet()
     {
         DuplicateOnMove = false;

+ 1 - 0
src/PixiEditor/Views/Input/EditableTextBlock.axaml

@@ -10,6 +10,7 @@
     <Grid>
         <TextBlock Foreground="{Binding ElementName=etb, Path=Foreground}"
                    DoubleTapped="OnDoubleTapped"
+                   PointerPressed="TextBlock_OnPointerPressed"
                    TextTrimming="CharacterEllipsis" Name="textBlock"
                    IsVisible="{Binding Path=TextBlockVisibility, ElementName=etb}"
                    Text="{Binding Path=Text, ElementName=etb, Mode=TwoWay}" />

+ 10 - 0
src/PixiEditor/Views/Input/EditableTextBlock.axaml.cs

@@ -115,6 +115,16 @@ internal partial class EditableTextBlock : UserControl
         e.Handled = true;
     }
 
+
+    private void TextBlock_OnPointerPressed(object? sender, PointerPressedEventArgs e)
+    {
+        if (e.ClickCount == 2)
+        {
+            EnableEditing();
+            e.Handled = true;
+        }
+    }
+
     private void TextBox_KeyDown(object sender, KeyEventArgs e)
     {
         if (e.Key is Key.Enter or Key.Escape)

+ 11 - 0
src/PixiEditor/Views/Layers/FolderControl.axaml.cs

@@ -53,14 +53,17 @@ internal partial class FolderControl : UserControl
         TopDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         TopDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         TopDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Top);
+        TopDropGrid.AddHandler(PointerEnteredEvent, Grid_PointerEntered);
 
         BottomDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_DragEnter);
         BottomDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_DragLeave);
         BottomDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Bottom);
+        TopDropGrid.AddHandler(PointerEnteredEvent, Grid_PointerEntered);
 
         middleDropGrid.AddHandler(DragDrop.DragEnterEvent, Grid_CenterEnter);
         middleDropGrid.AddHandler(DragDrop.DragLeaveEvent, Grid_CenterLeave);
         middleDropGrid.AddHandler(DragDrop.DropEvent, Grid_Drop_Center);
+        middleDropGrid.AddHandler(PointerEnteredEvent, Grid_PointerEntered);
 
         BackgroundGrid.AddHandler(DragDrop.DropEvent, BackgroundGrid_Drop);
         centerGrid.AddHandler(DragDrop.DropEvent, BackgroundGrid_Drop);
@@ -68,6 +71,14 @@ internal partial class FolderControl : UserControl
         DisableDropPanels();
     }
 
+    private void Grid_PointerEntered(object? sender, PointerEventArgs e)
+    {
+        if (!e.Properties.IsLeftButtonPressed)
+        {
+            DisableDropPanels();
+        }
+    }
+
     private void DisableDropPanels()
     {
         TopDropGrid.IsVisible = false;

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

@@ -137,7 +137,13 @@
                 Background="{DynamicResource ThemeBackgroundBrush}"
                 Grid.Row="3" VerticalAlignment="Bottom" />
             <TreeView DockPanel.Dock="Top" Name="treeView" BorderThickness="0"
-                      ItemsSource="{Binding DataContext.ActiveDocument.NodeGraph.StructureTree.Members, ElementName=layersManager}">
+                      AutoScrollToSelectedItem="True"
+                          ItemsSource="{Binding DataContext.ActiveDocument.NodeGraph.StructureTree.Members, ElementName=layersManager}">
+                <TreeView.Styles>
+                    <Style Selector="TreeViewItem">
+                        <Setter Property="IsExpanded" Value="{Binding (nodes:FolderNodeViewModel).IsOpen, Mode=TwoWay}"></Setter>
+                    </Style>
+                </TreeView.Styles>
                 <TreeView.ItemsPanel>
                     <ItemsPanelTemplate>
                         <StackPanel />