Browse Source

Merge pull request #872 from PixiEditor/fixes/30.03.2025

Fixes/30.03.2025
Krzysztof Krysiński 4 months ago
parent
commit
253f51ff1d
28 changed files with 213 additions and 80 deletions
  1. 1 1
      src/ColorPicker
  2. 6 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  3. 4 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  4. 15 4
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  5. 4 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodeFromName_Change.cs
  6. 7 1
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs
  7. 14 2
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  8. 46 0
      src/PixiEditor.ChangeableDocument/Rendering/RenderingUtils.cs
  9. 7 0
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  10. 2 0
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  11. 1 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  12. 2 2
      src/PixiEditor/Models/Controllers/ClipboardController.cs
  13. 17 4
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ColorPickerToolExecutor.cs
  14. 1 1
      src/PixiEditor/Models/Handlers/IDocument.cs
  15. 6 0
      src/PixiEditor/Models/Handlers/IViewport.cs
  16. 6 0
      src/PixiEditor/Models/Handlers/IWindowHandler.cs
  17. 2 37
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  18. 13 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  19. 17 6
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  20. 1 1
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  21. 4 1
      src/PixiEditor/ViewModels/SubViewModels/ClipboardViewModel.cs
  22. 2 1
      src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs
  23. 2 1
      src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs
  24. 1 0
      src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs
  25. 3 1
      src/PixiEditor/Views/Overlays/Handles/Handle.cs
  26. 23 5
      src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs
  27. 5 5
      src/PixiEditor/Views/Overlays/SnappingOverlay.cs
  28. 1 1
      src/PixiParser

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 2287f7cf314cf80b3d81b28770ac4936fcea9052
+Subproject commit 172589cc045a0d9b530618897ce570db9056d8c8

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

@@ -45,7 +45,7 @@ public static class FloodFillHelper
         VecI startingPos,
         Color drawingColor,
         float tolerance,
-        int frame)
+        int frame, bool lockTransparency)
     {
         if (selection is not null && !selection.Contains(startingPos.X + 0.5f, startingPos.Y + 0.5f))
             return new();
@@ -77,6 +77,11 @@ public static class FloodFillHelper
         if ((colorSpaceCorrectedColor.A == 0) || colorToReplace == colorSpaceCorrectedColor)
             return new();
 
+        if (colorToReplace.A == 0 && lockTransparency)
+        {
+            return new();
+        }
+
         RectI globalSelectionBounds = (RectI?)selection?.TightBounds ?? new RectI(VecI.Zero, document.Size);
 
         // Pre-multiplies the color and convert it to floats. Since floats are imprecise, a range is used.

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -2,6 +2,7 @@
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing.FloodFill;
 
@@ -32,7 +33,7 @@ internal class FloodFill_Change : Change
     {
         if (pos.X < 0 || pos.Y < 0 || pos.X >= target.Size.X || pos.Y >= target.Size.Y)
             return false;
-        
+
         return DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);
     }
 
@@ -46,7 +47,8 @@ internal class FloodFill_Change : Change
             target.ForEveryReadonlyMember(member => membersToReference.Add(member.Id));
         else
             membersToReference.Add(memberGuid);
-        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, tolerance, frame);
+        bool lockTransparency = target.FindMember(memberGuid) is ImageLayerNode { LockTransparency: true };
+        var floodFilledChunks = FloodFillHelper.FloodFill(membersToReference, target, selection, pos, color, tolerance, frame, lockTransparency);
         if (floodFilledChunks.Count == 0)
         {
             ignoreInUndo = true;

+ 15 - 4
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -4,6 +4,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.Objects;
 using PixiEditor.ChangeableDocument.Changes.Selection;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
@@ -35,6 +36,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
     private AffectedArea lastAffectedArea;
 
     private static Paint RegularPaint { get; } = new() { BlendMode = BlendMode.SrcOver };
+    private static Paint LockedAlphaPaint { get; } = new() { BlendMode = BlendMode.SrcIn, Color = Colors.White };
 
     [GenerateUpdateableChangeActions]
     public TransformSelected_UpdateableChange(
@@ -250,7 +252,8 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
             {
                 ChunkyImage memberImage =
                     DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
-                var area = DrawImage(member, memberImage);
+                var memberNode = target.FindMember(member.MemberId);
+                var area = DrawImage(member, memberImage, isTransformingSelection && memberNode is ImageLayerNode { LockTransparency: true });
                 member.SavedChunks = new(memberImage, memberImage.FindAffectedArea().Chunks);
                 memberImage.CommitChanges();
                 infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId, area, drawOnMask).AsT1);
@@ -287,8 +290,9 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
                 ChunkyImage targetImage =
                     DrawingChangeHelper.GetTargetImageOrThrow(target, member.MemberId, drawOnMask, frame);
 
+                var memberNode = target.FindMember(member.MemberId);
                 infos.Add(DrawingChangeHelper.CreateAreaChangeInfo(member.MemberId,
-                        DrawImage(member, targetImage), drawOnMask)
+                        DrawImage(member, targetImage, isTransformingSelection && memberNode is ImageLayerNode { LockTransparency: true }), drawOnMask)
                     .AsT1);
             }
             else if (member.IsTransformable)
@@ -392,7 +396,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
         return final;
     }
 
-    private AffectedArea DrawImage(MemberTransformationData data, ChunkyImage memberImage)
+    private AffectedArea DrawImage(MemberTransformationData data, ChunkyImage memberImage, bool lockAlpha)
     {
         var prevAffArea = memberImage.FindAffectedArea();
 
@@ -400,7 +404,14 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
         if (!keepOriginal)
             memberImage.EnqueueClearPath(data.OriginalPath!, data.RoundedOriginalBounds);
-        memberImage.EnqueueDrawImage(data.LocalMatrix, data.Image, RegularPaint, false);
+        var finalPaint = RegularPaint;
+
+        if (lockAlpha)
+        {
+            finalPaint = LockedAlphaPaint;
+        }
+
+        memberImage.EnqueueDrawImage(data.LocalMatrix, data.Image, finalPaint, false);
         hasEnqueudImages = true;
 
         var affectedArea = memberImage.FindAffectedArea();

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodeFromName_Change.cs

@@ -4,6 +4,7 @@ internal class CreateNodeFromName_Change : Change
 {
     private string nodeUniqueName;
     private Guid id;
+    private Guid pairId;
 
     private Type typeToCreate;
 
@@ -11,10 +12,11 @@ internal class CreateNodeFromName_Change : Change
     private CreateNode_Change change;
 
     [GenerateMakeChangeAction]
-    public CreateNodeFromName_Change(string nodeUniqueName, Guid id)
+    public CreateNodeFromName_Change(string nodeUniqueName, Guid id, Guid pairId)
     {
         this.id = id;
         this.nodeUniqueName = nodeUniqueName;
+        this.pairId = pairId;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -26,7 +28,7 @@ internal class CreateNodeFromName_Change : Change
         }
 
         typeToCreate = nodeType;
-        change = new CreateNode_Change(nodeType, id);
+        change = new CreateNode_Change(nodeType, id, pairId);
         return true;
     }
 

+ 7 - 1
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -15,12 +15,14 @@ internal class CreateNode_Change : Change
 {
     private Type nodeType;
     private Guid id;
+    private Guid? pairId;
 
     [GenerateMakeChangeAction]
-    public CreateNode_Change(Type nodeType, Guid id)
+    public CreateNode_Change(Type nodeType, Guid id, Guid pairId)
     {
         this.id = id;
         this.nodeType = nodeType;
+        this.pairId = pairId == Guid.Empty ? null : pairId;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -36,6 +38,10 @@ internal class CreateNode_Change : Change
             id = Guid.NewGuid();
 
         Node node = NodeOperations.CreateNode(nodeType, target);
+        if (pairId.HasValue && node is IPairNode pairNode)
+        {
+            pairNode.OtherNode = pairId.Value;
+        }
 
         node.Position = new VecD(0, 0);
         node.Id = id;

+ 14 - 2
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -203,7 +203,7 @@ public class DocumentRenderer : IPreviewRenderable
         return true;
     }
 
-    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize)
+    public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize, string? customOutput = null)
     {
         IsBusy = true;
 
@@ -223,7 +223,19 @@ public class DocumentRenderer : IPreviewRenderable
         RenderContext context =
             new(renderTexture.DrawingSurface, frameTime, ChunkResolution.Full, Document.Size,
                 Document.ProcessingColorSpace) { FullRerender = true };
-        Document.NodeGraph.Execute(context);
+
+        bool hasCustomOutput = !string.IsNullOrEmpty(customOutput) && customOutput != "DEFAULT";
+
+        var graph = hasCustomOutput
+            ? RenderingUtils.SolveFinalNodeGraph(customOutput, Document)
+            : Document.NodeGraph;
+
+        if (hasCustomOutput)
+        {
+            context.TargetOutput = customOutput;
+        }
+
+        graph.Execute(context);
 
         toRenderOn.Canvas.DrawSurface(renderTexture.DrawingSurface, 0, 0);
 

+ 46 - 0
src/PixiEditor.ChangeableDocument/Rendering/RenderingUtils.cs

@@ -0,0 +1,46 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Interfaces;
+
+namespace PixiEditor.ChangeableDocument.Rendering;
+
+public static class RenderingUtils
+{
+    public static IReadOnlyNodeGraph SolveFinalNodeGraph(string? targetOutput, IReadOnlyDocument document)
+    {
+        if (targetOutput == null || targetOutput == "DEFAULT")
+        {
+            return document.NodeGraph;
+        }
+
+        CustomOutputNode[] outputNodes = document.NodeGraph.AllNodes.OfType<CustomOutputNode>().ToArray();
+
+        foreach (CustomOutputNode outputNode in outputNodes)
+        {
+            if (outputNode.OutputName.Value == targetOutput)
+            {
+                return GraphFromOutputNode(outputNode);
+            }
+        }
+
+        return document.NodeGraph;
+    }
+
+    public static IReadOnlyNodeGraph GraphFromOutputNode(CustomOutputNode outputNode)
+    {
+        NodeGraph graph = new();
+        outputNode.TraverseBackwards(n =>
+        {
+            if (n is Node node)
+            {
+                graph.AddNode(node);
+            }
+
+            return true;
+        });
+
+        graph.CustomOutputNode = outputNode;
+        return graph;
+    }
+}

+ 7 - 0
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -429,6 +429,7 @@ internal class NodeGraphBuilder
         public KeyFrameData[] KeyFrames { get; set; }
         public Dictionary<string, object> AdditionalData { get; set; }
         public Dictionary<int, List<(string inputPropName, string outputPropName)>> InputConnections { get; set; }
+        public int? PairId { get; set; }
 
         public NodeBuilder WithId(int id)
         {
@@ -489,5 +490,11 @@ internal class NodeGraphBuilder
             KeyFrames = keyFrames;
             return this;
         }
+
+        public NodeBuilder WithPairId(int? nodePairId)
+        {
+            PairId = nodePairId;
+            return this;
+        }
     }
 }

+ 2 - 0
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -1,6 +1,7 @@
 using Drawie.Backend.Core;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.Models.IO;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Graph;
@@ -57,6 +58,7 @@ internal static class PixiParserDocumentEx
                     .WithKeyFrames(node.KeyFrames)
                     .WithInputValues(ToDictionary(node.InputPropertyValues))
                     .WithAdditionalData(node.AdditionalData)
+                    .WithPairId(node.PairId)
                     .WithConnections(node.InputConnections));
             }
         }

+ 1 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -69,6 +69,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<NodeGraphManagerViewModel>()
             .AddSingleton<AutosaveViewModel>()
             .AddSingleton<IColorsHandler, ColorsViewModel>(x => x.GetRequiredService<ColorsViewModel>())
+            .AddSingleton<IWindowHandler, WindowViewModel>(x => x.GetRequiredService<WindowViewModel>())
             .AddSingleton<RegistryViewModel>()
             .AddSingleton(static x => new DiscordViewModel(x.GetService<ViewModels_ViewModelMain>(), "764168193685979138"))
             .AddSingleton<DebugViewModel>()

+ 2 - 2
src/PixiEditor/Models/Controllers/ClipboardController.cs

@@ -128,7 +128,7 @@ internal static class ClipboardController
         await Clipboard.SetDataObjectAsync(data);
     }
 
-    public static async Task CopyVisibleToClipboard(DocumentViewModel document)
+    public static async Task CopyVisibleToClipboard(DocumentViewModel document, string? output = null)
     {
         await Clipboard.ClearAsync();
 
@@ -148,7 +148,7 @@ internal static class ClipboardController
         using Surface documentSurface = new Surface(document.SizeBindable);
 
         document.Renderer.RenderDocument(documentSurface.DrawingSurface,
-            document.AnimationDataViewModel.ActiveFrameTime, document.SizeBindable);
+            document.AnimationDataViewModel.ActiveFrameTime, document.SizeBindable, output);
 
         Surface surfaceToCopy = new Surface((VecI)copyArea.Size.Ceiling());
         using Paint paint = new Paint();

+ 17 - 4
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/ColorPickerToolExecutor.cs

@@ -13,6 +13,7 @@ internal class ColorPickerToolExecutor : UpdateableChangeExecutor
     private bool includeCanvas;
     private DocumentScope scope;
     private IColorsHandler? colorsViewModel;
+    private IWindowHandler? windowHandler;
 
     public override ExecutionState Start()
     {
@@ -25,8 +26,13 @@ internal class ColorPickerToolExecutor : UpdateableChangeExecutor
         scope = tool.Mode;
         includeReference = tool.PickFromReferenceLayer && document!.ReferenceLayerHandler.ReferenceTexture is not null;
         includeCanvas = tool.PickFromCanvas;
-        
-        colorsViewModel.PrimaryColor = document.PickColor(controller.LastPrecisePosition, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost);
+
+        windowHandler = GetHandler<IWindowHandler>();
+
+        string? customOutput = windowHandler.ActiveWindow is IViewport viewport ? viewport.RenderOutputName : null;
+        customOutput = customOutput == "DEFAULT" ? null : customOutput;
+
+        colorsViewModel.PrimaryColor = document.PickColor(controller.LastPrecisePosition, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost, customOutput);
         return ExecutionState.Success;
     }
 
@@ -34,12 +40,19 @@ internal class ColorPickerToolExecutor : UpdateableChangeExecutor
     {
         if (!includeReference)
             return;
-        colorsViewModel.PrimaryColor = document.PickColor(pos, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost);
+
+        string? customOutput = windowHandler?.ActiveWindow is IViewport viewport ? viewport.RenderOutputName : null;
+        customOutput = customOutput == "DEFAULT" ? null : customOutput;
+
+        colorsViewModel.PrimaryColor = document.PickColor(pos, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost, customOutput);
     }
 
     public override void OnPixelPositionChange(VecI pos)
     {
-        colorsViewModel.PrimaryColor = document.PickColor(pos, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost);
+        string? customOutput = windowHandler?.ActiveWindow is IViewport viewport ? viewport.RenderOutputName : null;
+        customOutput = customOutput == "DEFAULT" ? null : customOutput;
+
+        colorsViewModel.PrimaryColor = document.PickColor(pos, scope, includeReference, includeCanvas, document.AnimationHandler.ActiveFrameBindable, document.ReferenceLayerHandler.IsTopMost, customOutput);
     }
 
     public override void OnLeftMouseButtonUp(VecD argsPositionOnCanvas)

+ 1 - 1
src/PixiEditor/Models/Handlers/IDocument.cs

@@ -61,7 +61,7 @@ internal interface IDocument : IHandler
     public void SetSize(VecI infoSize);
 
     public Color PickColor(VecD controllerLastPrecisePosition, DocumentScope scope, bool includeReference,
-        bool includeCanvas, int frame, bool isTopMost);
+        bool includeCanvas, int frame, bool isTopMost, string? customOutput);
 
     public HashSet<Guid> ExtractSelectedLayers(bool includeFoldersWithMask = false);
     public void UpdateSavedState();

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

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Handlers;
+
+public interface IViewport
+{
+    public string? RenderOutputName { get; set; }
+}

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

@@ -0,0 +1,6 @@
+namespace PixiEditor.Models.Handlers;
+
+public interface IWindowHandler : IHandler
+{
+    public object ActiveWindow { get; }
+}

+ 2 - 37
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -40,7 +40,7 @@ internal class SceneRenderer
 
         string adjustedTargetOutput = targetOutput ?? "";
 
-        IReadOnlyNodeGraph finalGraph = SolveFinalNodeGraph(targetOutput);
+        IReadOnlyNodeGraph finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
         bool shouldRerender = ShouldRerender(target, resolution, adjustedTargetOutput, finalGraph);
         if (shouldRerender)
         {
@@ -184,42 +184,7 @@ internal class SceneRenderer
         return solveMatrixDiff;
     }
 
-    private IReadOnlyNodeGraph SolveFinalNodeGraph(string? targetOutput)
-    {
-        if (targetOutput == null)
-        {
-            return Document.NodeGraph;
-        }
-
-        CustomOutputNode[] outputNodes = Document.NodeGraph.AllNodes.OfType<CustomOutputNode>().ToArray();
-
-        foreach (CustomOutputNode outputNode in outputNodes)
-        {
-            if (outputNode.OutputName.Value == targetOutput)
-            {
-                return GraphFromOutputNode(outputNode);
-            }
-        }
-
-        return Document.NodeGraph;
-    }
-
-    private IReadOnlyNodeGraph GraphFromOutputNode(CustomOutputNode outputNode)
-    {
-        NodeGraph graph = new();
-        outputNode.TraverseBackwards(n =>
-        {
-            if (n is Node node)
-            {
-                graph.AddNode(node);
-            }
 
-            return true;
-        });
-
-        graph.CustomOutputNode = outputNode;
-        return graph;
-    }
 
     private bool HighDpiRenderNodePresent(IReadOnlyNodeGraph documentNodeGraph)
     {
@@ -246,7 +211,7 @@ internal class SceneRenderer
         double onionOpacity = animationData.OnionOpacity / 100.0;
         double alphaFalloffMultiplier = 1.0 / animationData.OnionFrames;
 
-        var finalGraph = SolveFinalNodeGraph(targetOutput);
+        var finalGraph = RenderingUtils.SolveFinalNodeGraph(targetOutput, Document);
 
         // Render previous frames'
         for (int i = 1; i <= animationData.OnionFrames; i++)

+ 13 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -444,6 +444,16 @@ internal partial class DocumentViewModel
                 }
             }
 
+            int? pairNodeId = null;
+            if (node is IPairNode pairNode)
+            {
+                if (pairNode.OtherNode != Guid.Empty &&
+                    nodeIdMap.TryGetValue(pairNode.OtherNode, out var value))
+                {
+                    pairNodeId = value;
+                }
+            }
+
             Node parserNode = new Node()
             {
                 Id = nodeIdMap[node.Id],
@@ -453,7 +463,8 @@ internal partial class DocumentViewModel
                 InputPropertyValues = properties,
                 AdditionalData = converted,
                 KeyFrames = keyFrames,
-                InputConnections = connections.ToArray()
+                InputConnections = connections.ToArray(),
+                PairId = pairNodeId,
             };
 
             targetGraph.AllNodes.Add(parserNode);
@@ -553,6 +564,7 @@ internal partial class DocumentViewModel
                     if (child is IReadOnlyRasterKeyFrame rasterKeyFrame)
                     {
                         if (!nodeIdMap.ContainsKey(rasterKeyFrame.NodeId)) continue;
+                        if (!keyFrameIds.ContainsKey(rasterKeyFrame.Id)) continue;
 
                         BuildRasterKeyFrame(rasterKeyFrame, graph, group, nodeIdMap, keyFrameIds);
                     }

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

@@ -50,6 +50,7 @@ using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.Models.IO;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Skia;
@@ -374,7 +375,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (builderInstance.Graph.AllNodes.Count == 0 || !builderInstance.Graph.AllNodes.Any(x => x is OutputNode))
         {
             Guid outputNodeGuid = Guid.NewGuid();
-            acc.AddActions(new CreateNode_Action(typeof(OutputNode), outputNodeGuid));
+            acc.AddActions(new CreateNode_Action(typeof(OutputNode), outputNodeGuid, Guid.Empty));
         }
 
         AddAnimationData(builderInstance.AnimationData, mappedNodeIds, mappedKeyFrameIds);
@@ -434,7 +435,14 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         {
             Guid guid = Guid.NewGuid();
             mappedNodeIds.Add(id, guid);
-            acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid));
+            Guid pairGuid = Guid.Empty;
+
+            if (serializedNode.PairId != null && mappedNodeIds.TryGetValue(serializedNode.PairId.Value, out Guid pairId))
+            {
+                pairGuid = pairId;
+            }
+
+            acc.AddActions(new CreateNodeFromName_Action(serializedNode.UniqueNodeName, guid, pairGuid));
             acc.AddFinishedActions(new NodePosition_Action([guid], serializedNode.Position.ToVecD()),
                 new EndNodePosition_Action());
 
@@ -710,7 +718,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     /// <param name="includeCanvas">Should the color be picked from the canvas</param>
     /// <param name="referenceTopmost">Is the reference layer topmost. (Only affects the result is includeReference and includeCanvas are set.)</param>
     public Color PickColor(VecD pos, DocumentScope scope, bool includeReference, bool includeCanvas, int frame,
-        bool referenceTopmost = false)
+        bool referenceTopmost = false, string? customOutput = null)
     {
         if (scope == DocumentScope.SingleLayer && includeReference && includeCanvas)
             includeReference = false;
@@ -736,7 +744,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
 
         if (includeCanvas)
-            return PickColorFromCanvas((VecI)pos, scope, frame);
+        {
+            return PickColorFromCanvas((VecI)pos, scope, frame, customOutput);
+        }
+
         if (includeReference)
             return PickColorFromReferenceLayer(pos) ?? Colors.Transparent;
         return Colors.Transparent;
@@ -758,7 +769,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
     }
 
-    public Color PickColorFromCanvas(VecI pos, DocumentScope scope, KeyFrameTime frameTime)
+    public Color PickColorFromCanvas(VecI pos, DocumentScope scope, KeyFrameTime frameTime, string? customOutput = null)
     {
         // there is a tiny chance that the image might get disposed by another thread
         try
@@ -768,7 +779,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             if (scope == DocumentScope.Canvas)
             {
                 using Surface tmpSurface = new Surface(SizeBindable); // new Surface is on purpose, Surface.ForDisplay doesn't work here
-                Renderer.RenderDocument(tmpSurface.DrawingSurface, frameTime, SizeBindable);
+                Renderer.RenderDocument(tmpSurface.DrawingSurface, frameTime, SizeBindable, customOutput);
 
                 return tmpSurface.GetSrgbPixel(pos);
             }

+ 1 - 1
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -253,7 +253,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         else
         {
             Guid nodeId = Guid.NewGuid();
-            changes.Add(new CreateNode_Action(nodeType, nodeId));
+            changes.Add(new CreateNode_Action(nodeType, nodeId, Guid.Empty));
 
             if (pos != default)
             {

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

@@ -319,7 +319,10 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
         if (doc is null)
             return;
 
-        await ClipboardController.CopyVisibleToClipboard(doc);
+        await ClipboardController.CopyVisibleToClipboard(
+            doc, Owner.WindowSubViewModel.ActiveWindow is ViewportWindowViewModel viewportWindowViewModel
+                ? viewportWindowViewModel.RenderOutputName
+                : null);
 
         hasImageInClipboard = true;
     }

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/ViewportWindowViewModel.cs

@@ -5,6 +5,7 @@ using PixiDocks.Core.Docking.Events;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Models.DocumentModels;
 using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 using PixiEditor.ViewModels.Dock;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views.Visuals;
@@ -12,7 +13,7 @@ using PixiEditor.Views.Visuals;
 namespace PixiEditor.ViewModels.SubViewModels;
 #nullable enable
 internal class ViewportWindowViewModel : SubViewModel<WindowViewModel>, IDockableContent, IDockableCloseEvents,
-    IDockableSelectionEvents
+    IDockableSelectionEvents, IViewport
 {
     public DocumentViewModel Document { get; }
     public ExecutionTrigger<VecI> CenterViewportTrigger { get; } = new ExecutionTrigger<VecI>();

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs

@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Input;
 using PixiDocks.Core.Docking;
 using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands;
+using PixiEditor.Models.Handlers;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.Views;
@@ -21,7 +22,7 @@ namespace PixiEditor.ViewModels.SubViewModels;
 
 #nullable enable
 [Commands_Command.Group("PixiEditor.Window", "WINDOWS")]
-internal class WindowViewModel : SubViewModel<ViewModelMain>
+internal class WindowViewModel : SubViewModel<ViewModelMain>, IWindowHandler
 {
     private CommandController commandController;
     public RelayCommand<string> ShowAvalonDockWindowCommand { get; set; }

+ 1 - 0
src/PixiEditor/Views/Overlays/Handles/AnchorHandle.cs

@@ -14,6 +14,7 @@ public class AnchorHandle : RectangleHandle
     private Paint selectedPaint;
     
     public bool IsSelected { get; set; } = false;
+    public override VecD HitSizeMargin { get; set; } = new VecD(5, 5);
 
     public AnchorHandle(Overlay owner) : base(owner)
     {

+ 3 - 1
src/PixiEditor/Views/Overlays/Handles/Handle.cs

@@ -33,6 +33,8 @@ public abstract class Handle : IHandle
     public bool HitTestVisible { get; set; } = true;
     public bool IsHovered => isHovered;
 
+    public virtual VecD HitSizeMargin { get; set; } = VecD.Zero;
+
     public event HandleEvent OnPress;
     public event HandleEvent OnDrag;
     public event HandleEvent OnRelease;
@@ -60,7 +62,7 @@ public abstract class Handle : IHandle
 
     public virtual bool IsWithinHandle(VecD handlePos, VecD pos, double zoomboxScale)
     {
-        return TransformHelper.IsWithinHandle(handlePos, pos, zoomboxScale, Size);
+        return TransformHelper.IsWithinHandle(handlePos, pos, zoomboxScale, Size + HitSizeMargin);
     }
 
     public static T? GetResource<T>(string key)

+ 23 - 5
src/PixiEditor/Views/Overlays/PathOverlay/VectorPathOverlay.cs

@@ -61,6 +61,8 @@ public class VectorPathOverlay : Overlay
     private EditableVectorPath editableVectorPath;
     private bool canInsert = false;
 
+    private bool isDragging = false;
+
     static VectorPathOverlay()
     {
         AffectsOverlayRender(PathProperty);
@@ -103,7 +105,7 @@ public class VectorPathOverlay : Overlay
             insertPreviewHandle.Draw(context);
         }
 
-        if (IsOverAnyHandle() || canInsert)
+        if ((IsOverAnyHandle() && !isDragging) || canInsert)
         {
             TryHighlightSnap(null, null);
         }
@@ -153,7 +155,7 @@ public class VectorPathOverlay : Overlay
             }
         }
 
-        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(1, 1);
+        transformHandle.Position = Path.TightBounds.BottomRight + new VecD(transformHandle.Size.X / ZoomScale, transformHandle.Size.Y / ZoomScale);
         transformHandle.Draw(context);
     }
 
@@ -339,6 +341,7 @@ public class VectorPathOverlay : Overlay
         TryHighlightSnap(null, null);
         args.Pointer.Capture(this);
         args.Handled = true;
+        isDragging = true;
     }
 
 
@@ -348,13 +351,14 @@ public class VectorPathOverlay : Overlay
 
         VectorPath updatedPath = new VectorPath(pathOnStartDrag);
 
-        delta = TryFindAnySnap(delta, pathOnStartDrag, out string axisX, out string axisY);
+        delta = TryFindAnySnap(delta, pathOnStartDrag, out string axisX, out string axisY, out VecD? snapPoint);
         updatedPath.Transform(Matrix3X3.CreateTranslation((float)delta.X, (float)delta.Y));
 
-        TryHighlightSnap(axisX, axisY);
+        TryHighlightSnap(axisX, axisY, snapPoint);
 
         Path = updatedPath;
         args.Handled = true;
+        isDragging = true;
     }
 
     protected override void OnKeyPressed(Key key, KeyModifiers keyModifiers, string? symbol)
@@ -524,6 +528,12 @@ public class VectorPathOverlay : Overlay
         }
     }
 
+    protected override void OnPointerReleased(PointerReleasedEventArgs e)
+    {
+        base.OnPointerReleased(e);
+        isDragging = false;
+    }
+
     private bool AddNewPointFromClick(VecD point)
     {
         var selectedHandle = anchorHandles.FirstOrDefault(h => h.IsSelected);
@@ -787,11 +797,12 @@ public class VectorPathOverlay : Overlay
         anchorHandles.Clear();
     }
 
-    private VecD TryFindAnySnap(VecD delta, VectorPath path, out string? axisX, out string? axisY)
+    private VecD TryFindAnySnap(VecD delta, VectorPath path, out string? axisX, out string? axisY, out VecD? snapPoint)
     {
         VecD closestSnapDelta = new VecD(double.PositiveInfinity, double.PositiveInfinity);
         axisX = null;
         axisY = null;
+        snapPoint = null;
 
         SnappingController.RemoveAll("editingPath");
 
@@ -802,12 +813,14 @@ public class VectorPathOverlay : Overlay
             {
                 closestSnapDelta = new VecD(snap.X, closestSnapDelta.Y);
                 axisX = x;
+                snapPoint = (VecD)point;
             }
 
             if (snap.Y < closestSnapDelta.Y && !string.IsNullOrEmpty(y))
             {
                 closestSnapDelta = new VecD(closestSnapDelta.X, snap.Y);
                 axisY = y;
+                snapPoint = (VecD)point;
             }
         }
 
@@ -823,6 +836,11 @@ public class VectorPathOverlay : Overlay
             closestSnapDelta = new VecD(closestSnapDelta.X, 0);
         }
 
+        if (snapPoint != null)
+        {
+            snapPoint = snapPoint + delta + closestSnapDelta;
+        }
+
         return delta + closestSnapDelta;
     }
 

+ 5 - 5
src/PixiEditor/Views/Overlays/SnappingOverlay.cs

@@ -69,7 +69,7 @@ internal class SnappingOverlay : Overlay
                     VecD snapPointValue = snapPoint.Value();
                     context.DrawLine(new VecD(snapPointValue.X, mousePoint.Y), new VecD(snapPointValue.X, snapPointValue.Y), horizontalAxisPen);
 
-                    DrawDistanceText(context, snapPointValue + new VecD(10 / ZoomScale, 0), mousePoint);
+                    DrawDistanceText(context, snapPointValue, new VecD(10 / ZoomScale, 0), mousePoint);
                 }
             }
         }
@@ -83,7 +83,7 @@ internal class SnappingOverlay : Overlay
                     var snapPointValue = snapPoint.Value();
                     context.DrawLine(new VecD(mousePoint.X, snapPointValue.Y), new VecD(snapPointValue.X, snapPointValue.Y), verticalAxisPen);
 
-                    DrawDistanceText(context, snapPointValue + new VecD(0, -10 / ZoomScale), mousePoint);
+                    DrawDistanceText(context, snapPointValue, new VecD(0, -10 / ZoomScale), mousePoint);
                 }
             }
         }
@@ -94,7 +94,7 @@ internal class SnappingOverlay : Overlay
         }
     }
 
-    private void DrawDistanceText(Canvas context, VecD snapPointValue, VecD mousePoint)
+    private void DrawDistanceText(Canvas context, VecD snapPointValue, VecD drawOffset, VecD mousePoint)
     {
         VecD distance = snapPointValue - mousePoint;
         VecD center = (snapPointValue + mousePoint) / 2;
@@ -104,10 +104,10 @@ internal class SnappingOverlay : Overlay
         distanceTextPaint.Style = PaintStyle.Stroke;
         distanceTextPaint.StrokeWidth = 2f / (float)ZoomScale;
 
-        context.DrawText($"{distance.Length.ToString("F2", CultureInfo.CurrentCulture)} px", center, distanceFont, distanceTextPaint);
+        context.DrawText($"{distance.Length.ToString("0.##", CultureInfo.CurrentCulture)} px", center + drawOffset, distanceFont, distanceTextPaint);
         distanceTextPaint.Color = Colors.White;
         distanceTextPaint.Style = PaintStyle.Fill;
-        context.DrawText($"{distance.Length.ToString("F2", CultureInfo.CurrentCulture)} px", center, distanceFont, distanceTextPaint);
+        context.DrawText($"{distance.Length.ToString("0.##", CultureInfo.CurrentCulture)} px", center + drawOffset, distanceFont, distanceTextPaint);
     }
 
     protected override void ZoomChanged(double newZoom)

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit f3475837fbd05e8ba11bcc5f4fc8cca1458d73d9
+Subproject commit 1c7d1a546ef948a825137eb3b147f05076cce843