ソースを参照

Merge pull request #723 from PixiEditor/fixes/10.01.2025

Fixes/10.01.2025
Krzysztof Krysiński 11 ヶ月 前
コミット
9210a6e2fe

+ 5 - 4
src/ChunkyImageLib/Operations/BresenhamLineHelper.cs

@@ -2,19 +2,20 @@
 using Drawie.Numerics;
 
 namespace ChunkyImageLib.Operations;
+
 public static class BresenhamLineHelper
 {
-    public static VecF[] GetBresenhamLine(VecI start, VecI end)
+    public static VecI[] GetBresenhamLine(VecI start, VecI end)
     {
         int count = Math.Abs((start - end).LongestAxis) + 1;
         if (count > 100000)
-            return Array.Empty<VecF>();
-        VecF[] output = new VecF[count];
+            return [];
+        VecI[] output = new VecI[count];
         CalculateBresenhamLine(start, end, output);
         return output;
     }
 
-    private static void CalculateBresenhamLine(VecI start, VecI end, VecF[] output)
+    private static void CalculateBresenhamLine(VecI start, VecI end, VecI[] output)
     {
         int index = 0;
 

+ 1 - 1
src/ChunkyImageLib/Operations/BresenhamLineOperation.cs

@@ -23,7 +23,7 @@ internal class BresenhamLineOperation : IMirroredDrawOperation
         this.color = color;
         this.blendMode = blendMode;
         paint = new Paint() { BlendMode = blendMode };
-        points = BresenhamLineHelper.GetBresenhamLine(from, to);
+        points = BresenhamLineHelper.GetBresenhamLine(from, to).Select(v => new VecF(v)).ToArray();
     }
 
     public void DrawOnChunk(Chunk targetChunk, VecI chunkPos)

+ 31 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -110,7 +110,24 @@ public static class FloodFillHelper
             {
                 if (colorToReplace.A == 0 && !processedEmptyChunks.Contains(chunkPos))
                 {
-                    drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                    int saved = drawingChunk.Surface.DrawingSurface.Canvas.Save();
+                    if (selection is not null && !selection.IsEmpty)
+                    {
+                        using VectorPath localSelection = new VectorPath(selection);
+                        localSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
+                        
+                        drawingChunk.Surface.DrawingSurface.Canvas.ClipPath(localSelection);
+                        if (SelectionIntersectsChunk(selection, chunkPos, chunkSize))
+                        {
+                            drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                        }
+                    }
+                    else
+                    {
+                        drawingChunk.Surface.DrawingSurface.Canvas.Clear(drawingColor);
+                    }
+
+                    drawingChunk.Surface.DrawingSurface.Canvas.RestoreToCount(saved);
                     for (int i = 0; i < chunkSize; i++)
                     {
                         if (chunkPos.Y > 0)
@@ -180,6 +197,9 @@ public static class FloodFillHelper
             return null;
         if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetRawPixel(pos)))
             return null;
+        
+        if(!SelectionIntersectsChunk(selection, chunkPos, chunkSize))
+            return null;
 
         byte[] pixelStates = new byte[chunkSize * chunkSize];
         DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);
@@ -249,7 +269,7 @@ public static class FloodFillHelper
         RectI localBounds = globalBounds.Offset(-chunkPos * chunkSize).Intersect(new(0, 0, chunkSize, chunkSize));
         if (localBounds.IsZeroOrNegativeArea)
             return;
-        VectorPath shiftedSelection = new VectorPath(selection);
+        using VectorPath shiftedSelection = new VectorPath(selection);
         shiftedSelection.Transform(Matrix3X3.CreateTranslation(-chunkPos.X * chunkSize, -chunkPos.Y * chunkSize));
 
         fixed (byte* arr = array)
@@ -262,4 +282,13 @@ public static class FloodFillHelper
             drawingSurface.Canvas.Flush();
         }
     }
+    
+    private static bool SelectionIntersectsChunk(VectorPath selection, VecI chunkPos, int chunkSize)
+    {
+        if (selection is null || selection.IsEmpty)
+            return true;
+        
+        RectD chunkBounds = new(chunkPos * chunkSize, new VecI(chunkSize));
+        return selection.Bounds.IntersectsWithInclusive(chunkBounds);
+    }
 }

+ 13 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -24,6 +24,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     private readonly List<VecI> points = new();
     private int frame;
     private VecF lastPos;
+    private int lastAppliedPointIndex = -1;
 
     [GenerateUpdateableChangeActions]
     public LineBasedPen_UpdateableChange(Guid memberGuid, Color color, VecI pos, float strokeWidth, bool erasing,
@@ -59,7 +60,12 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     [UpdateChangeMethod]
     public void Update(VecI pos, float strokeWidth)
     {
-        points.Add(pos);
+        if (points.Count > 0)
+        {
+            var bresenham = BresenhamLineHelper.GetBresenhamLine(points[^1], pos);
+            points.AddRange(bresenham);
+        }
+        
         this.strokeWidth = strokeWidth;
     }
 
@@ -81,16 +87,13 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
     {
         var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask, frame);
 
-        var (from, to) = points.Count > 1 ? (points[^2], points[^1]) : (points[0], points[0]);
-
         int opCount = image.QueueLength;
 
-        var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-
         float spacingPixels = strokeWidth * spacing;
-
-        foreach (var point in bresenham)
+        
+        for(int i = Math.Max(lastAppliedPointIndex, 0); i < points.Count; i++)
         {
+            var point = points[i];
             if (points.Count > 1 && VecF.Distance(lastPos, point) < spacingPixels)
                 continue;
 
@@ -103,6 +106,8 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
 
             image.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
         }
+        
+        lastAppliedPointIndex = points.Count - 1;
 
         var affChunks = image.FindAffectedArea(opCount);
 
@@ -126,7 +131,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         VecF lastPos = points[0];
 
         float spacingInPixels = strokeWidth * this.spacing;
-
+        
         for (int i = 0; i < points.Count; i++)
         {
             if (i > 0 && VecF.Distance(lastPos, points[i]) < spacingInPixels)

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/PixelPerfectPen_UpdateableChange.cs

@@ -73,8 +73,8 @@ internal class PixelPerfectPen_UpdateableChange : UpdateableChange
         (pixelsToConfirm2, pixelsToConfirm) = (pixelsToConfirm, pixelsToConfirm2);
         pixelsToConfirm.Clear();
 
-        VecF[] line = BresenhamLineHelper.GetBresenhamLine(incomingPoints[pointsCount - 2], incomingPoints[pointsCount - 1]);
-        foreach (VecF pixel in line)
+        VecI[] line = BresenhamLineHelper.GetBresenhamLine(incomingPoints[pointsCount - 2], incomingPoints[pointsCount - 1]);
+        foreach (VecI pixel in line)
         {
             pixelsToConfirm.Add(pixel);
         }

+ 2 - 2
src/PixiEditor/ViewModels/SubViewModels/AnimationsViewModel.cs

@@ -214,7 +214,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
                 return active;
             }
 
-            for (int i = active + 1; i < activeDocument.AnimationDataViewModel.FramesCount; i++)
+            for (int i = active + 1; i < groupViewModel.StartFrameBindable + groupViewModel.DurationBindable; i++)
             {
                 if (groupViewModel.Children.All(x => !x.IsWithinRange(i)))
                 {
@@ -222,7 +222,7 @@ internal class AnimationsViewModel : SubViewModel<ViewModelMain>
                 }
             }
 
-            return activeDocument.AnimationDataViewModel.FramesCount + 1;
+            return groupViewModel.StartFrameBindable + groupViewModel.DurationBindable;
         }
 
         return active;

+ 91 - 40
src/PixiEditor/Views/Animations/Timeline.cs

@@ -13,6 +13,7 @@ using Avalonia.VisualTree;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Helpers;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.Models.Handlers;
 using PixiEditor.ViewModels.Document;
 
 namespace PixiEditor.Views.Animations;
@@ -27,7 +28,7 @@ namespace PixiEditor.Views.Animations;
 internal class Timeline : TemplatedControl, INotifyPropertyChanged
 {
     private const float MarginMultiplier = 1.5f;
-    
+
     public static readonly StyledProperty<KeyFrameCollection> KeyFramesProperty =
         AvaloniaProperty.Register<Timeline, KeyFrameCollection>(
             nameof(KeyFrames));
@@ -88,14 +89,16 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
     public static readonly StyledProperty<double> MinLeftOffsetProperty = AvaloniaProperty.Register<Timeline, double>(
         nameof(MinLeftOffset), 30);
 
-    public static readonly StyledProperty<ICommand> ChangeKeyFramesLengthCommandProperty = AvaloniaProperty.Register<Timeline, ICommand>(
-        nameof(ChangeKeyFramesLengthCommand));
+    public static readonly StyledProperty<ICommand> ChangeKeyFramesLengthCommandProperty =
+        AvaloniaProperty.Register<Timeline, ICommand>(
+            nameof(ChangeKeyFramesLengthCommand));
 
     public static readonly StyledProperty<int> DefaultEndFrameProperty = AvaloniaProperty.Register<Timeline, int>(
         nameof(DefaultEndFrame));
 
-    public static readonly StyledProperty<bool> OnionSkinningEnabledProperty = AvaloniaProperty.Register<Timeline, bool>(
-        nameof(OnionSkinningEnabled));
+    public static readonly StyledProperty<bool> OnionSkinningEnabledProperty =
+        AvaloniaProperty.Register<Timeline, bool>(
+            nameof(OnionSkinningEnabled));
 
     public static readonly StyledProperty<double> OnionOpacityProperty = AvaloniaProperty.Register<Timeline, double>(
         nameof(OnionOpacity), 50);
@@ -185,10 +188,11 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
     private Control? extendingElement;
     private Rectangle _selectionRectangle;
     private ItemsControl? _keyFramesHost;
-    
+
     private Vector clickPos;
-    
+
     private bool shouldClearNextSelection = true;
+    private bool shouldShiftSelect = false;
     private CelViewModel clickedCel;
     private bool dragged;
     private Guid[] draggedKeyFrames;
@@ -209,15 +213,16 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         DraggedKeyFrameCommand = new RelayCommand<PointerEventArgs>(KeyFramesDragged);
         ReleasedKeyFrameCommand = new RelayCommand<CelViewModel>(KeyFramesReleased);
     }
-    
-    public void SelectKeyFrame(CelViewModel? keyFrame, bool clearSelection = true)
+
+    public void SelectKeyFrame(ICelHandler? keyFrame, bool clearSelection = true)
     {
         if (clearSelection)
         {
             ClearSelectedKeyFrames();
         }
 
-        keyFrame?.Document.AnimationDataViewModel.AddSelectedKeyFrame(keyFrame.Id);
+
+        keyFrame?.Document.AnimationHandler.AddSelectedKeyFrame(keyFrame.Id);
     }
 
     public bool DragAllSelectedKeyFrames(int delta)
@@ -227,15 +232,15 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         {
             return false;
         }
-        
+
         Guid[] ids = SelectedKeyFrames.Select(x => x.Id).ToArray();
-        
+
         draggedKeyFrames = ids;
-        
+
         ChangeKeyFramesLengthCommand.Execute((ids, delta, false));
         return true;
     }
-    
+
     public void EndDragging()
     {
         if (dragged)
@@ -245,6 +250,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
                 ChangeKeyFramesLengthCommand.Execute((draggedKeyFrames.ToArray(), 0, true));
             }
         }
+
         clickedCel = null;
     }
 
@@ -265,25 +271,70 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
         _timelineKeyFramesScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineKeyFramesScroll");
         _timelineHeaderScroll = e.NameScope.Find<ScrollViewer>("PART_TimelineHeaderScroll");
-        
+
         _selectionRectangle = e.NameScope.Find<Rectangle>("PART_SelectionRectangle");
 
         _timelineKeyFramesScroll.ScrollChanged += TimelineKeyFramesScrollOnScrollChanged;
         _contentGrid.PointerPressed += ContentOnPointerPressed;
         _contentGrid.PointerMoved += ContentOnPointerMoved;
         _contentGrid.PointerCaptureLost += ContentOnPointerLost;
-        
+
         extendingElement = new Control();
         extendingElement.SetValue(MarginProperty, new Thickness(0, 0, 0, 0));
         _contentGrid.Children.Add(extendingElement);
-        
+
         _keyFramesHost = e.NameScope.Find<ItemsControl>("PART_KeyFramesHost");
     }
-    
+
     private void KeyFramesReleased(CelViewModel? e)
     {
         if (!dragged)
         {
+            if (shouldShiftSelect)
+            {
+                var lastSelected = SelectedKeyFrames.LastOrDefault();
+                if (lastSelected != null)
+                {
+                    int startFrame = lastSelected.StartFrameBindable;
+                    int endFrame = e.StartFrameBindable;
+                    if (startFrame > endFrame)
+                    {
+                        (startFrame, endFrame) = (endFrame, startFrame);
+                    }
+
+                    int groupStartIndex = -1;
+                    int groupEndIndex = -1;
+                    
+                    for (int i = 0; i < KeyFrames.Count; i++)
+                    {
+                        if (KeyFrames[i].LayerGuid == lastSelected.LayerGuid)
+                        {
+                            groupStartIndex = i;
+                        }
+                        if (KeyFrames[i].LayerGuid == e.LayerGuid)
+                        {
+                            groupEndIndex = i;
+                        }
+                    }
+
+                    if (groupStartIndex != -1 && groupEndIndex != -1 && groupStartIndex > groupEndIndex)
+                    {
+                        (groupStartIndex, groupEndIndex) = (groupEndIndex, groupStartIndex);
+                    }
+
+                    for (int i = groupStartIndex; i <= groupEndIndex; i++)
+                    {
+                        foreach (var keyFrame in KeyFrames[i].Children)
+                        {
+                            if (keyFrame.StartFrameBindable >= startFrame && keyFrame.StartFrameBindable <= endFrame)
+                            {
+                                SelectKeyFrame(keyFrame, false);
+                            }
+                        }
+                    }
+                }
+            }
+
             SelectKeyFrame(e, shouldClearNextSelection);
             shouldClearNextSelection = true;
         }
@@ -325,7 +376,8 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
     private void KeyFramePressed(PointerPressedEventArgs? e)
     {
-        shouldClearNextSelection = !e.KeyModifiers.HasFlag(KeyModifiers.Control);
+        shouldShiftSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
+        shouldClearNextSelection = !shouldShiftSelect && !e.KeyModifiers.HasFlag(KeyModifiers.Control);
         KeyFrame target = null;
         if (e.Source is Control obj)
         {
@@ -360,7 +412,6 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         _timelineHeaderScroll!.Offset = new Vector(0, scrollViewer.Offset.Y);
     }
 
-   
 
     private void PlayToggleOnClick(object? sender, RoutedEventArgs e)
     {
@@ -395,12 +446,12 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         {
             newScale -= ticks;
         }
-        
+
         newScale = Math.Clamp(newScale, 1, 900);
         Scale = newScale;
-        
+
         double mouseXInViewport = e.GetPosition(_timelineKeyFramesScroll).X;
-            
+
         double currentFrameUnderMouse = towardsFrame;
         double newOffsetX = currentFrameUnderMouse * newScale - mouseXInViewport + MinLeftOffset;
 
@@ -416,22 +467,22 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
         Dispatcher.UIThread.Post(
             () =>
-        {
-            newOffsetX = Math.Clamp(newOffsetX, 0, _timelineKeyFramesScroll.ScrollBarMaximum.X);
-            
-            ScrollOffset = new Vector(newOffsetX, 0);
-        }, DispatcherPriority.Render);
+            {
+                newOffsetX = Math.Clamp(newOffsetX, 0, _timelineKeyFramesScroll.ScrollBarMaximum.X);
+
+                ScrollOffset = new Vector(newOffsetX, 0);
+            }, DispatcherPriority.Render);
 
         e.Handled = true;
     }
-    
+
     private void ContentOnPointerPressed(object? sender, PointerPressedEventArgs e)
     {
         if (e.Source is not Grid content)
         {
             return;
         }
-        
+
         var mouseButton = e.GetMouseButton(content);
 
         if (mouseButton == MouseButton.Left)
@@ -439,7 +490,6 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             _selectionRectangle.IsVisible = true;
             _selectionRectangle.Width = 0;
             _selectionRectangle.Height = 0;
-            
         }
         else if (mouseButton == MouseButton.Middle)
         {
@@ -450,13 +500,12 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             {
                 extendingElement.Margin = new Thickness(_timelineKeyFramesScroll.Viewport.Width, 0, 0, 0);
             }
-            
         }
-        
+
         clickPos = e.GetPosition(content);
         e.Handled = true;
     }
-    
+
     private void ContentOnPointerMoved(object? sender, PointerEventArgs e)
     {
         if (e.Source is not Grid content)
@@ -483,7 +532,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         newOffsetX = Math.Clamp(newOffsetX, 0, _timelineKeyFramesScroll.ScrollBarMaximum.X);
         newOffsetY = Math.Clamp(newOffsetY, 0, _timelineKeyFramesScroll.ScrollBarMaximum.Y);
         ScrollOffset = new Vector(newOffsetX, newOffsetY);
-            
+
         extendingElement.Margin += new Thickness(deltaX, 0, 0, 0);
     }
 
@@ -508,7 +557,8 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         foreach (var frame in frames)
         {
             var translated = frame.TranslatePoint(new Point(0, 0), _contentGrid);
-            Rect frameBounds = new Rect(translated.Value.X, translated.Value.Y, frame.Bounds.Width, frame.Bounds.Height);
+            Rect frameBounds = new Rect(translated.Value.X, translated.Value.Y, frame.Bounds.Width,
+                frame.Bounds.Height);
             if (bounds.Contains(frameBounds))
             {
                 SelectKeyFrame(frame.Item, false);
@@ -585,11 +635,11 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             cel.Document.AnimationDataViewModel.RemoveSelectedKeyFrame(cel.Id);
             cel.PropertyChanged -= KeyFrameOnPropertyChanged;
         }
-        
+
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
-    
+
     private static void OnDefaultEndFrameChanged(AvaloniaPropertyChangedEventArgs e)
     {
         if (e.Sender is not Timeline timeline)
@@ -602,7 +652,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(EndFrame)));
         }
     }
-    
+
     private void KeyFrameOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
         if (sender is CelViewModel keyFrame)
@@ -611,7 +661,8 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             {
                 PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
             }
-            else if (e.PropertyName == nameof(CelViewModel.StartFrameBindable) || e.PropertyName == nameof(CelViewModel.DurationBindable))
+            else if (e.PropertyName == nameof(CelViewModel.StartFrameBindable) ||
+                     e.PropertyName == nameof(CelViewModel.DurationBindable))
             {
                 PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
             }

+ 15 - 8
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.ComponentModel;
 using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
@@ -199,7 +200,7 @@ internal class NodeGraphView : Zoombox.Zoombox
     private ItemsControl nodeItemsControl;
     private ItemsControl connectionItemsControl;
     private Rectangle selectionRectangle;
-    
+
     private List<INodeHandler> selectedNodesOnStartDrag = new();
 
     private List<Control> nodeViewsCache = new();
@@ -248,14 +249,15 @@ internal class NodeGraphView : Zoombox.Zoombox
                 }
 
                 nodeViewsCache.Add(presenter);
+                presenter.PropertyChanged += OnPresenterPropertyChanged;
                 if (presenter.Child == null)
                 {
-                    presenter.PropertyChanged += OnPresenterPropertyChanged;
                     continue;
                 }
 
                 NodeView nodeView = (NodeView)presenter.Child;
                 nodeView.PropertyChanged += NodeView_PropertyChanged;
+                nodeView.Node.PropertyChanged += Node_PropertyChanged;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Remove)
@@ -269,15 +271,16 @@ internal class NodeGraphView : Zoombox.Zoombox
 
                 nodeViewsCache.Remove(presenter);
 
+                presenter.PropertyChanged -= OnPresenterPropertyChanged;
                 if (presenter.Child == null)
                 {
-                    presenter.PropertyChanged -= OnPresenterPropertyChanged;
                     continue;
                 }
 
 
                 NodeView nodeView = (NodeView)presenter.Child;
                 nodeView.PropertyChanged -= NodeView_PropertyChanged;
+                nodeView.Node.PropertyChanged -= Node_PropertyChanged;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Reset)
@@ -285,7 +288,7 @@ internal class NodeGraphView : Zoombox.Zoombox
             nodeViewsCache.Clear();
         }
     }
-    
+
     private void OnPresenterPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
     {
         if (e.Property == ContentPresenter.ChildProperty)
@@ -293,17 +296,21 @@ internal class NodeGraphView : Zoombox.Zoombox
             if (e.NewValue is NodeView nodeView)
             {
                 nodeView.PropertyChanged += NodeView_PropertyChanged;
+                nodeView.Node.PropertyChanged += Node_PropertyChanged;
             }
         }
-
-        if (e.Property == Canvas.LeftProperty || e.Property == Canvas.TopProperty)
+    }
+    
+    private void Node_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName == nameof(NodeViewModel.PositionBindable))
         {
-            if (e.Sender is ContentPresenter presenter && presenter.Child is NodeView nodeView)
+            if (sender is NodeViewModel node)
             {
                 Dispatcher.UIThread.Post(
                     () =>
                     {
-                        UpdateConnections(nodeView);
+                        UpdateConnections(FindNodeView(node));
                     }, DispatcherPriority.Render);
             }
         }