Browse Source

Cross document data sharing, cross duplicate, transform fix

Krzysztof Krysiński 4 months ago
parent
commit
6ad3a4336f

+ 2 - 0
src/PixiEditor.ChangeableDocument.Gen/Helpers.cs

@@ -25,6 +25,7 @@ internal static class Helpers
         StringBuilder sb = new();
          
         sb.AppendLine("using Drawie.Backend.Core.Numerics;");
+        sb.AppendLine("using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;");
         sb.AppendLine("namespace PixiEditor.ChangeableDocument.Actions.Generated;\n");
         sb.AppendLine("[System.Runtime.CompilerServices.CompilerGenerated]");
         sb.AppendLine($"public record class {actionName} : PixiEditor.ChangeableDocument.Actions.IMakeChangeAction");
@@ -59,6 +60,7 @@ internal static class Helpers
         StringBuilder sb = new();
 
         sb.AppendLine("using Drawie.Backend.Core.Numerics;");
+        sb.AppendLine("using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;");
         sb.AppendLine("namespace PixiEditor.ChangeableDocument.Actions.Generated;");
         sb.AppendLine($"public record class {actionName} : PixiEditor.ChangeableDocument.Actions.IStartOrUpdateChangeAction" + (isCancelable ? ", PixiEditor.ChangeableDocument.Actions.ICancelableAction" : ""));
         sb.AppendLine("{");

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

@@ -48,6 +48,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     public bool VerticalSymmetryAxisEnabled { get; set; }
     public double HorizontalSymmetryAxisY { get; set; }
     public double VerticalSymmetryAxisX { get; set; }
+    public bool IsDisposed { get; private set; }
 
     public Document()
     {
@@ -57,6 +58,9 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     public void Dispose()
     {
+        if (IsDisposed) return;
+
+        IsDisposed = true;
         NodeGraph.Dispose();
         Selection.Dispose();
     }
@@ -154,7 +158,8 @@ internal class Document : IChangeable, IReadOnlyDocument
         List<IReadOnlyStructureNode> parents = new();
         childNode.TraverseForwards((node, input) =>
         {
-            if (node is IReadOnlyStructureNode parent && input is { InternalPropertyName: FolderNode.ContentInternalName })
+            if (node is IReadOnlyStructureNode parent &&
+                input is { InternalPropertyName: FolderNode.ContentInternalName })
                 parents.Add(parent);
             return true;
         });
@@ -162,6 +167,11 @@ internal class Document : IChangeable, IReadOnlyDocument
         return parents;
     }
 
+    public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode
+    {
+        return new DocumentNodePipe<T>(this, layerId);
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>

+ 58 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentMemoryPipe.cs

@@ -0,0 +1,58 @@
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal abstract class DocumentMemoryPipe<T> : ICrossDocumentPipe<T> where T : class
+{
+    protected Document Document { get; }
+    public bool IsOpen { get; private set; }
+    public bool CanOpen { get; private set; } = true;
+
+    public DocumentMemoryPipe(Document document)
+    {
+        Document = document;
+        IsOpen = false;
+    }
+
+    public void Open()
+    {
+        if (!CanOpen)
+            throw new InvalidOperationException("Pipe cannot be opened");
+        IsOpen = true;
+    }
+
+    public void Close()
+    {
+        IsOpen = false;
+    }
+
+    public T? TryAccessData()
+    {
+        if (!IsOpen)
+        {
+            if (!CanOpen)
+            {
+#if DEBUG
+                throw new InvalidOperationException("Trying to open a disposed pipe");
+#endif
+                return null;
+            }
+
+            Open();
+        }
+
+        if (!DocumentValid()) return null;
+
+        return GetData();
+    }
+
+    protected abstract T? GetData();
+
+    public void Dispose()
+    {
+        IsOpen = false;
+        CanOpen = false;
+    }
+
+    private bool DocumentValid() => Document is { IsDisposed: false };
+}

+ 21 - 0
src/PixiEditor.ChangeableDocument/Changeables/DocumentNodePipe.cs

@@ -0,0 +1,21 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Changeables;
+
+internal class DocumentNodePipe<T> : DocumentMemoryPipe<T> where T : class, IReadOnlyNode
+{
+    public Guid NodeGuid { get; }
+    public DocumentNodePipe(Document document, Guid nodeGuid) : base(document)
+    {
+        NodeGuid = nodeGuid;
+    }
+
+    protected override T? GetData()
+    {
+        var foundNode = Document.FindNode(NodeGuid);
+        if (foundNode is T casted) return casted;
+
+        return null;
+    }
+}

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

@@ -6,4 +6,5 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 public interface IReadOnlyFolderNode : IReadOnlyStructureNode
 {
     public HashSet<Guid> GetLayerNodeGuids();
+    public RenderInputProperty Content { get; }
 }

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

@@ -142,23 +142,23 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
     {
-        RectI? bounds = null;
+        RectD? bounds = null;
         if (Content.Connection != null)
         {
             Content.Connection.Node.TraverseBackwards((n) =>
             {
                 if (n is StructureNode structureNode)
                 {
-                    RectI? imageBounds = (RectI?)structureNode.GetTightBounds(frameTime);
-                    if (imageBounds != null)
+                    RectD? childBounds = structureNode.GetTightBounds(frameTime);
+                    if (childBounds != null)
                     {
                         if (bounds == null)
                         {
-                            bounds = imageBounds;
+                            bounds = childBounds;
                         }
                         else
                         {
-                            bounds = bounds.Value.Union(imageBounds.Value);
+                            bounds = bounds.Value.Union(childBounds.Value);
                         }
                     }
                 }
@@ -166,7 +166,7 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode, IClipSource, IPrev
                 return true;
             });
 
-            return (RectD?)bounds ?? RectD.Empty;
+            return bounds ?? RectD.Empty;
         }
 
         return null;

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

@@ -15,7 +15,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
 

+ 10 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/ICrossDocumentPipe.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+public interface ICrossDocumentPipe<T> : IDisposable
+{
+    public T? TryAccessData();
+    public bool CanOpen { get; }
+    public bool IsOpen { get; }
+    public void Open();
+    public void Close();
+}

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

@@ -105,4 +105,5 @@ public interface IReadOnlyDocument : IDisposable
     public ColorSpace ProcessingColorSpace { get; }
     public void InitProcessingColorSpace(ColorSpace processingColorSpace);
     public List<IReadOnlyStructureNode> GetParents(Guid memberGuid);
+    public ICrossDocumentPipe<T> CreateNodePipe<T>(Guid layerId) where T : class, IReadOnlyNode;
 }

+ 12 - 2
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateLayer_Change.cs

@@ -28,7 +28,17 @@ internal class DuplicateLayer_Change : Change
             return false;
         
         connectionsData = NodeOperations.CreateConnectionsData(layer);
-        
+
+        var targetInput = layer.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter) &&
+            x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
+
+        if (targetInput == null)
+        {
+            FailedMessage = "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER";
+            return false;
+        }
+
         return true;
     }
 
@@ -45,7 +55,7 @@ internal class DuplicateLayer_Change : Change
             x.ValueType == typeof(Painter) &&
             x.Connection is { Node: StructureNode }) as InputProperty<Painter?>;
         
-        var previousConnection = targetInput.Connection;
+        var previousConnection = targetInput?.Connection;
 
         List<IChangeInfo> operations = new();
 

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

@@ -0,0 +1,168 @@
+using System.Collections.Immutable;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class ImportFolder_Change : Change
+{
+    private ICrossDocumentPipe<IReadOnlyFolderNode> sourcefolderPipe;
+    private Guid duplicateGuid;
+    private Guid[] contentGuids;
+    private Guid[] contentDuplicateGuids;
+
+    private Guid[]? childGuidsToUse;
+
+    private ConnectionsData? connectionsData;
+    private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
+    private Dictionary<Guid, VecD> originalPositions;
+
+    [GenerateMakeChangeAction]
+    public ImportFolder_Change(ICrossDocumentPipe<IReadOnlyFolderNode> pipe, Guid newGuid, ImmutableList<Guid>? childGuids)
+    {
+        sourcefolderPipe = pipe;
+        duplicateGuid = newGuid;
+        childGuidsToUse = childGuids?.ToArray();
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (sourcefolderPipe is not { CanOpen: true } || target.NodeGraph.OutputNode == null)
+            return false;
+
+        var folder = sourcefolderPipe.TryAccessData();
+
+        connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
+
+        List<Guid> contentGuidList = new();
+
+        folder.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            contentGuidList.Add(x.Id);
+            contentConnectionsData[x.Id] = NodeOperations.CreateConnectionsData(x);
+            return true;
+        });
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        var readOnlyFolderNode = sourcefolderPipe.TryAccessData();
+
+        if (readOnlyFolderNode is not FolderNode folderNode || target.NodeGraph.OutputNode == null)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        FolderNode clone = (FolderNode)folderNode.Clone();
+        clone.Id = duplicateGuid;
+
+        InputProperty<Painter?> targetInput = target.NodeGraph.OutputNode.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+
+        var previousConnection = targetInput.Connection;
+
+        operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
+        DuplicateContent(target, clone, folderNode, operations);
+
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (contentDuplicateGuids is not null)
+        {
+            foreach (Guid contentGuid in contentDuplicateGuids)
+            {
+                Node contentNode = target.FindNodeOrThrow<Node>(contentGuid);
+                changes.AddRange(NodeOperations.DetachNode(target.NodeGraph, contentNode));
+                changes.Add(new DeleteNode_ChangeInfo(contentNode.Id));
+
+                target.NodeGraph.RemoveNode(contentNode);
+                contentNode.Dispose();
+            }
+        }
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.NodeGraph.OutputNode;
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
+        return changes;
+    }
+
+    private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
+        List<IChangeInfo> operations)
+    {
+        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+
+        nodeMap[existingLayer.Id] = clone.Id;
+        int counter = 0;
+        List<Guid> contentGuidList = new();
+
+        existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
+        {
+            if (x is not Node targetNode)
+                return false;
+
+            Node? node = targetNode.Clone();
+
+            if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+            {
+                node.Id = childGuidsToUse[counter];
+                counter++;
+            }
+
+            nodeMap[x.Id] = node.Id;
+            contentGuidList.Add(node.Id);
+
+            target.NodeGraph.AddNode(node);
+
+            operations.Add(CreateNode_ChangeInfo.CreateFromNode(node));
+            return true;
+        });
+
+        foreach (var data in contentConnectionsData)
+        {
+            var updatedData = data.Value.WithUpdatedIds(nodeMap);
+            Guid targetNodeId = nodeMap[data.Key];
+            operations.AddRange(NodeOperations.ConnectStructureNodeProperties(updatedData,
+                target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
+        }
+
+        contentDuplicateGuids = contentGuidList.ToArray();
+    }
+}

+ 112 - 0
src/PixiEditor.ChangeableDocument/Changes/Structure/ImportLayer_Change.cs

@@ -0,0 +1,112 @@
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+using PixiEditor.ChangeableDocument.ChangeInfos.Structure;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.Structure;
+
+internal class ImportLayer_Change : Change
+{
+    private ICrossDocumentPipe<IReadOnlyLayerNode> sourceDocumentPipe;
+    private Dictionary<Guid, VecD> originalPositions;
+    private ConnectionsData? connectionsData;
+
+    private Guid duplicateGuid;
+
+    [GenerateMakeChangeAction]
+    public ImportLayer_Change(ICrossDocumentPipe<IReadOnlyLayerNode> pipe, Guid newGuid)
+    {
+        sourceDocumentPipe = pipe;
+        duplicateGuid = newGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (sourceDocumentPipe is not { CanOpen: true })
+            return false;
+
+        if (!sourceDocumentPipe.IsOpen)
+        {
+            sourceDocumentPipe.Open();
+        }
+
+        IReadOnlyLayerNode? layer = sourceDocumentPipe.TryAccessData();
+        if (layer == null || target.NodeGraph.OutputNode == null)
+            return false;
+
+        connectionsData = NodeOperations.CreateConnectionsData(target.NodeGraph.OutputNode);
+
+        if (target.NodeGraph.OutputNode == null) return false;
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+
+        var layer = sourceDocumentPipe.TryAccessData();
+        if (layer is not LayerNode layerNode)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
+        var clone = (LayerNode)layerNode.Clone();
+        clone.Id = duplicateGuid;
+
+        var targetInput = target.NodeGraph.OutputNode?.InputProperties.FirstOrDefault(x =>
+            x.ValueType == typeof(Painter)) as InputProperty<Painter?>;
+
+        var previousConnection = targetInput?.Connection;
+
+        List<IChangeInfo> operations = new();
+
+        target.NodeGraph.AddNode(clone);
+
+        operations.Add(CreateLayer_ChangeInfo.FromLayer(clone));
+
+        operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
+
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
+
+        ignoreInUndo = false;
+
+        return operations;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var (member, parent) = target.FindChildAndParentOrThrow(duplicateGuid);
+
+        target.NodeGraph.RemoveNode(member);
+        member.Dispose();
+
+        List<IChangeInfo> changes = new();
+
+        changes.AddRange(NodeOperations.DetachStructureNode(member));
+        changes.Add(new DeleteStructureMember_ChangeInfo(member.Id));
+
+        if (connectionsData is not null)
+        {
+            Node originalNode = target.NodeGraph.OutputNode;
+            changes.AddRange(
+                NodeOperations.ConnectStructureNodeProperties(connectionsData, originalNode, target.NodeGraph));
+        }
+
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
+        return changes;
+    }
+
+    public override void Dispose()
+    {
+        sourceDocumentPipe?.Dispose();
+    }
+}
+

+ 1 - 0
src/PixiEditor/Helpers/Constants/ClipboardDataFormats.cs

@@ -9,4 +9,5 @@ public static class ClipboardDataFormats
     public const string DocumentFormat = "PixiEditor.Document";
     public const string NodeIdList = "PixiEditor.NodeIdList";
     public const string CelIdList = "PixiEditor.CelIdList";
+    public const string PixiVectorData = "PixiEditor.VectorData";
 }

+ 21 - 14
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -27,6 +27,7 @@ using PixiEditor.Models.Commands.Attributes.Evaluators;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.IO;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Parser;
 using PixiEditor.ViewModels.Document;
@@ -189,23 +190,28 @@ internal static class ClipboardController
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static bool TryPaste(DocumentViewModel document, IEnumerable<IDataObject> data, bool pasteAsNew = false)
+    public static bool TryPaste(DocumentViewModel document, DocumentManagerViewModel manager, IEnumerable<IDataObject>
+        data, bool pasteAsNew = false)
     {
-        Guid sourceDocument = GetSourceDocument(data);
+        Guid sourceDocument = GetSourceDocument(data, document.Id);
         Guid[] layerIds = GetLayerIds(data);
 
-        if (sourceDocument != document.Id)
-        {
-            layerIds = [];
-        }
-
         bool hasPos = data.Any(x => x.Contains(ClipboardDataFormats.PositionFormat));
 
-        if (pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, document)))
+        IDocument? targetDoc = manager.Documents.FirstOrDefault(x => x.Id == sourceDocument);
+
+        if (targetDoc != null && pasteAsNew && layerIds is { Length: > 0 } && (!hasPos || AllMatchesPos(layerIds, data, targetDoc)))
         {
             foreach (var layerId in layerIds)
             {
-                document.Operations.DuplicateMember(layerId);
+                if (sourceDocument == document.Id)
+                {
+                    document.Operations.DuplicateMember(layerId);
+                }
+                else
+                {
+                    document.Operations.ImportMember(layerId, targetDoc);
+                }
             }
 
             return true;
@@ -250,7 +256,7 @@ internal static class ClipboardController
         return true;
     }
 
-    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, DocumentViewModel doc)
+    private static bool AllMatchesPos(Guid[] layerIds, IEnumerable<IDataObject> data, IDocument doc)
     {
         var dataObjects = data as IDataObject[] ?? data.ToArray();
 
@@ -299,7 +305,7 @@ internal static class ClipboardController
         return [];
     }
 
-    private static Guid GetSourceDocument(IEnumerable<IDataObject> data)
+    private static Guid GetSourceDocument(IEnumerable<IDataObject> data, Guid fallback)
     {
         foreach (var dataObject in data)
         {
@@ -311,19 +317,20 @@ internal static class ClipboardController
             }
         }
 
-        return Guid.Empty;
+        return fallback;
     }
 
     /// <summary>
     ///     Pastes image from clipboard into new layer.
     /// </summary>
-    public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, bool pasteAsNew = false)
+    public static async Task<bool> TryPasteFromClipboard(DocumentViewModel document, DocumentManagerViewModel manager,
+        bool pasteAsNew = false)
     {
         var data = await TryGetDataObject();
         if (data == null)
             return false;
 
-        return TryPaste(document, data, pasteAsNew);
+        return TryPaste(document, manager, data, pasteAsNew);
     }
 
     private static async Task<List<DataObject?>> TryGetDataObject()

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

@@ -23,6 +23,7 @@ using PixiEditor.Models.Layers;
 using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
@@ -90,7 +91,8 @@ 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, Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
+        Internals.ActionAccumulator.AddActions(new ClearSelectedArea_Action(member.Id,
+            Internals.Tracker.Document.Selection.SelectionPath, drawOnMask, frame));
         if (clearSelection)
             Internals.ActionAccumulator.AddActions(new ClearSelection_Action());
         Internals.ActionAccumulator.AddFinishedActions();
@@ -225,6 +227,38 @@ internal class DocumentOperationsModule : IDocumentOperations
         }
     }
 
+    public void ImportMember(Guid layerId, IDocument sourceDocument)
+    {
+        if (Internals.ChangeController.IsBlockingChangeActive)
+            return;
+
+        Internals.ChangeController.TryStopActiveExecutor();
+
+        if (sourceDocument == this.Document)
+        {
+            DuplicateMember(layerId);
+            return;
+        }
+
+        if (!sourceDocument.StructureHelper.TryFindNode(layerId, out IStructureMemberHandler? member))
+            return;
+
+        if (member is ILayerHandler layer)
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportLayer_Action(sourceDocument.ShareNode<IReadOnlyLayerNode>(layer.Id), newGuid),
+                new CreateAnimationDataFromLayer_Action(newGuid));
+        }
+        else if (member is IFolderHandler folder)
+        {
+            Guid newGuid = Guid.NewGuid();
+            Internals.ActionAccumulator.AddFinishedActions(
+                new ImportFolder_Action(sourceDocument.ShareNode<IReadOnlyFolderNode>(folder.Id), newGuid, null),
+                new SetSelectedMember_PassthroughAction(newGuid));
+        }
+    }
+
     /// <summary>
     /// Delete the member with the <paramref name="guidValue"/>
     /// </summary>

+ 3 - 0
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -16,6 +16,8 @@ using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.Models.DocumentPassthroughActions;
 
 namespace PixiEditor.Models.Handlers;
@@ -69,4 +71,5 @@ internal interface IDocument : IHandler
 
     internal void InternalRaiseLayersChanged(LayersChangedEventArgs e);
     internal void InternalMarkSaveState(DocumentMarkType type);
+    public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode;
 }

+ 5 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -599,6 +599,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
     }
 
+    public ICrossDocumentPipe<T> ShareNode<T>(Guid layerId) where T : class, IReadOnlyNode
+    {
+        return Internals.Tracker.Document.CreateNodePipe<T>(layerId);
+    }
+
     public OneOf<Error, Surface> TryRenderWholeImage(KeyFrameTime frameTime, VecI renderSize)
     {
         try

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -77,7 +77,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         Dispatcher.UIThread.InvokeAsync(async () =>
         {
             Guid[] guids = doc.StructureHelper.GetAllLayers().Select(x => x.Id).ToArray();
-            await ClipboardController.TryPasteFromClipboard(doc, pasteAsNewLayer);
+            await ClipboardController.TryPasteFromClipboard(doc, Owner.DocumentManagerSubViewModel, pasteAsNewLayer);
 
             doc.Operations.InvokeCustomAction(() =>
             {

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

@@ -22,6 +22,7 @@ internal partial class LayersManager : UserControl
 {
     public const string LayersDataName = "PixiEditor.LayersData";
     public DocumentViewModel ActiveDocument => DataContext is LayersDockViewModel vm ? vm.ActiveDocument : null;
+    public DocumentManagerViewModel ManagerViewModel => DataContext is LayersDockViewModel vm ? vm.DocumentManager : null;
     private readonly IBrush? highlightColor;
 
     public LayersManager()
@@ -180,7 +181,7 @@ internal partial class LayersManager : UserControl
             e.Handled = true;
         }
 
-        if (ClipboardController.TryPaste(ActiveDocument, new[] { (IDataObject)e.Data }, true))
+        if (ClipboardController.TryPaste(ActiveDocument, ManagerViewModel, new[] { (IDataObject)e.Data }, true))
         {
             e.Handled = true;
         }