Kaynağa Gözat

Merge pull request #993 from PixiEditor/fixes/25.07.2025

Fixes/25.07.2025
Krzysztof Krysiński 1 ay önce
ebeveyn
işleme
7074a74316

+ 18 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Matrix/Matrix3X3BaseNode.cs

@@ -40,7 +40,8 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
 
         Float3x3 mtx = Matrix.Value.Invoke(FuncContext.NoContext);
 
-        surface.Canvas.SetMatrix(surface.Canvas.TotalMatrix.Concat(mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity));
+        surface.Canvas.SetMatrix(
+            surface.Canvas.TotalMatrix.Concat(mtx.GetConstant() as Matrix3X3? ?? Matrix3X3.Identity));
         if (!surface.LocalClipBounds.IsZeroOrNegativeArea)
         {
             Background.Value?.Paint(context, surface);
@@ -49,5 +50,21 @@ public abstract class Matrix3X3BaseNode : RenderNode, IRenderInput
         surface.Canvas.RestoreToCount(layer);
     }
 
+    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        if (Background.Value == null)
+            return null;
+
+        return base.GetPreviewBounds(frame, elementToRenderName);
+    }
+
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        if (Background.Value == null)
+            return false;
+
+        return base.RenderPreview(renderOn, context, elementToRenderName);
+    }
+
     protected abstract Float3x3 CalculateMatrix(FuncContext ctx, Float3x3 input);
 }

+ 6 - 5
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -125,18 +125,18 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         IsBusy = false;
     }
 
-    public async Task RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn,
+    public async Task<bool> RenderNodePreview(IPreviewRenderable previewRenderable, DrawingSurface renderOn,
         RenderContext context,
         string elementToRenderName)
     {
-        if (previewRenderable is Node { IsDisposed: true }) return;
+        if (previewRenderable is Node { IsDisposed: true }) return false;
         TaskCompletionSource<bool> tcs = new();
         RenderRequest request = new(tcs, context, renderOn, previewRenderable, elementToRenderName);
 
         renderRequests.Enqueue(request);
         ExecuteRenderRequests();
 
-        await tcs.Task;
+        return await tcs.Task;
     }
 
     public static IReadOnlyNodeGraph ConstructMembersOnlyGraph(IReadOnlyNodeGraph fullGraph)
@@ -279,9 +279,10 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
 
             try
             {
+                bool result = true;
                 if (request.PreviewRenderable != null)
                 {
-                    request.PreviewRenderable.RenderPreview(request.RenderOn, request.Context,
+                    result = request.PreviewRenderable.RenderPreview(request.RenderOn, request.Context,
                         request.ElementToRenderName);
                 }
                 else if (request.NodeGraph != null)
@@ -289,7 +290,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
                     request.NodeGraph.Execute(request.Context);
                 }
 
-                request.TaskCompletionSource.SetResult(true);
+                request.TaskCompletionSource.SetResult(result);
             }
             catch (Exception e)
             {

+ 2 - 1
src/PixiEditor/Helpers/Converters/FormattedColorConverter.cs

@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using System.Globalization;
 using Avalonia.Media;
+using PixiEditor.Helpers.Extensions;
 
 namespace PixiEditor.Helpers.Converters;
 
@@ -24,7 +25,7 @@ internal class FormattedColorConverter
 
         return format.ToLowerInvariant() switch
         {
-            "hex" => color.ToString(),
+            "hex" => color.ToColor().ToRgbHex(),
             "rgba" => $"({color.R}, {color.G}, {color.B}, {color.A})",
             _ => "",
         };

+ 7 - 2
src/PixiEditor/Helpers/Converters/SocketColorConverter.cs

@@ -6,8 +6,13 @@ namespace PixiEditor.Helpers.Converters;
 
 internal class SocketColorConverter : SingleInstanceConverter<SocketColorConverter>
 {
-    Color unknownColor = Color.FromRgb(255, 0, 255);
+    static Color unknownColor = Color.FromRgb(255, 0, 255);
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return SocketToColor(value);
+    }
+
+    public static Color SocketToColor(object value)
     {
         if (value is IBrush brush)
         {
@@ -16,7 +21,7 @@ internal class SocketColorConverter : SingleInstanceConverter<SocketColorConvert
             if (brush is GradientBrush linearGradientBrush)
                 return linearGradientBrush.GradientStops.FirstOrDefault()?.Color ?? unknownColor;
         }
-        
+
         return unknownColor;
     }
 }

+ 1 - 4
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -260,10 +260,7 @@ internal class ClassicDesktopEntry
             if (restartQueued)
             {
                 var process = Process.GetCurrentProcess().MainModule.FileName;
-                desktop.Exit += (_, _) =>
-                {
-                    Process.Start(process);
-                };
+                Process.Start(process);
             }
         });
     }

+ 18 - 2
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -23,6 +23,10 @@ public class PreviewPainter : IDisposable
     public VecI DocumentSize { get; set; }
     public DocumentRenderer Renderer { get; set; }
 
+    public bool CanRender => canRender;
+
+    public event Action<bool>? CanRenderChanged;
+
     private Dictionary<int, Texture> renderTextures = new();
     private Dictionary<int, PainterInstance> painterInstances = new();
 
@@ -32,6 +36,8 @@ public class PreviewPainter : IDisposable
     private Dictionary<int, VecI> pendingResizes = new();
     private HashSet<int> pendingRemovals = new();
 
+    private bool canRender;
+
     private int lastRequestId = 0;
 
     public PreviewPainter(DocumentRenderer renderer, IPreviewRenderable previewRenderable, KeyFrameTime frameTime,
@@ -126,11 +132,21 @@ public class PreviewPainter : IDisposable
         RepaintDirty();
     }
 
-
-
     private void RepaintDirty()
     {
         var dirtyArray = dirtyTextures.ToArray();
+        bool couldRender = canRender;
+        canRender = PreviewRenderable?.GetPreviewBounds(FrameTime.Frame, ElementToRenderName) != null;
+        if (couldRender != canRender)
+        {
+            CanRenderChanged?.Invoke(canRender);
+        }
+
+        if (!CanRender)
+        {
+            return;
+        }
+
         foreach (var texture in dirtyArray)
         {
             if (!renderTextures.TryGetValue(texture, out var renderTexture))

+ 17 - 35
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -69,41 +69,23 @@
                             </ControlTheme>
                         </ItemsControl.ItemContainerTheme>
                     </ItemsControl>
-                    <ItemsControl Name="PART_Connections"
-                                  ItemsSource="{Binding NodeGraph.Connections, RelativeSource={RelativeSource TemplatedParent}}">
-                        <ItemsControl.ItemsPanel>
-                            <ItemsPanelTemplate>
-                                <Canvas RenderTransformOrigin="0, 0">
-                                    <Canvas.RenderTransform>
-                                        <TransformGroup>
-                                            <ScaleTransform
-                                                ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                            <TranslateTransform
-                                                X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                                Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
-                                        </TransformGroup>
-                                    </Canvas.RenderTransform>
-                                </Canvas>
-                            </ItemsPanelTemplate>
-                        </ItemsControl.ItemsPanel>
-                        <ItemsControl.ItemTemplate>
-                            <DataTemplate>
-                                <nodes:ConnectionView
-                                    InputNodePosition="{Binding InputNode.PositionBindable}"
-                                    OutputNodePosition="{Binding OutputNode.PositionBindable}"
-                                    InputProperty="{Binding InputProperty}"
-                                    OutputProperty="{Binding OutputProperty}">
-                                    <nodes:ConnectionView.IsVisible>
-                                        <MultiBinding Converter="{x:Static BoolConverters.And}">
-                                            <Binding Path="InputProperty.IsVisible" />
-                                            <Binding Path="OutputProperty.IsVisible" />
-                                        </MultiBinding>
-                                    </nodes:ConnectionView.IsVisible>
-                                </nodes:ConnectionView>
-                            </DataTemplate>
-                        </ItemsControl.ItemTemplate>
-                    </ItemsControl>
+                    <nodes:ConnectionRenderer
+                        ClipToBounds="False"
+                        Name="PART_ConnectionRenderer"
+                        RenderTransformOrigin="0, 0"
+                        SocketsInfo="{Binding SocketsInfo, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                        Connections="{Binding NodeGraph.Connections, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}">
+                        <nodes:ConnectionRenderer.RenderTransform>
+                            <TransformGroup>
+                                <ScaleTransform
+                                    ScaleX="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    ScaleY="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                                <TranslateTransform
+                                    X="{Binding CanvasX, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
+                                    Y="{Binding CanvasY, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}" />
+                            </TransformGroup>
+                        </nodes:ConnectionRenderer.RenderTransform>
+                    </nodes:ConnectionRenderer>
                     <ItemsControl
                         ZIndex="-1"
                         Name="PART_Frames"

+ 1 - 1
src/PixiEditor/Styles/Templates/NodeView.axaml

@@ -56,7 +56,7 @@
                                 </ItemsControl>
                             </StackPanel>
                         </Border>
-                        <Border IsVisible="{Binding !!ResultPreview, RelativeSource={RelativeSource TemplatedParent}}"
+                        <Border IsVisible="{Binding CanRenderPreview, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=False}"
                                 CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
                             <Panel RenderOptions.BitmapInterpolationMode="None" Width="200" Height="200">
                                 <Panel.Background>

+ 10 - 0
src/PixiEditor/ViewModels/DockableViewModel.cs

@@ -1,6 +1,7 @@
 using Avalonia.Media;
 using PixiDocks.Core.Docking;
 using PixiDocks.Core.Docking.Events;
+using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Dock;
 
 namespace PixiEditor.ViewModels;
@@ -15,5 +16,14 @@ internal abstract class DockableViewModel : ViewModelBase, IDockableContent
 
     public DockableViewModel()
     {
+        if (ILocalizationProvider.Current != null)
+        {
+            ILocalizationProvider.Current.OnLanguageChanged += OnLanguageChanged;
+        }
+    }
+
+    private void OnLanguageChanged(Language language)
+    {
+        OnPropertyChanged(nameof(Title));
     }
 }

+ 16 - 9
src/PixiEditor/Views/Layers/LayerControl.axaml.cs

@@ -144,19 +144,26 @@ internal partial class LayerControl : UserControl
 
     public static Guid[]? ExtractMemberGuids(IDataObject droppedMemberDataObject)
     {
-        object droppedLayer = droppedMemberDataObject.Get(LayersManager.LayersDataName);
-        if (droppedLayer is null)
-            return null;
+        try
+        {
+            object droppedLayer = droppedMemberDataObject.Get(LayersManager.LayersDataName);
+            if (droppedLayer is null)
+                return null;
 
-        if (droppedLayer is Guid droppedLayerGuid)
-            return new[] { droppedLayerGuid };
+            if (droppedLayer is Guid droppedLayerGuid)
+                return new[] { droppedLayerGuid };
 
-        if (droppedLayer is Guid[] droppedLayerGuids)
+            if (droppedLayer is Guid[] droppedLayerGuids)
+            {
+                return droppedLayerGuids;
+            }
+
+            return null;
+        }
+        catch (Exception e)
         {
-            return droppedLayerGuids;
+            return null;
         }
-
-        return null;
     }
 
     private bool HandleDrop(IDataObject dataObj, StructureMemberPlacement placement)

+ 1 - 8
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -32,7 +32,7 @@
                     </StackPanel>
                 </dialogs:DialogTitleBar.AdditionalElement>
             </dialogs:DialogTitleBar>
-            <Panel DockPanel.Dock="Left"
+            <Panel DockPanel.Dock="Left" Name="LogoPanel"
                    HorizontalAlignment="Left" Width="20" Height="20">
                 <Panel.Margin>
                     <OnPlatform>
@@ -150,13 +150,6 @@
             </Panel>
 
             <StackPanel Orientation="Horizontal">
-                <StackPanel.Margin>
-                    <OnPlatform>
-                        <OnPlatform.macOS>
-                            <Thickness>95, 0, 0, 0</Thickness>
-                        </OnPlatform.macOS>
-                    </OnPlatform>
-                </StackPanel.Margin>
                 <xaml:Menu IsVisible="{OnPlatform macOS=false, Default=true}"
                            Margin="5, 0, 0, 0"
                            DockPanel.Dock="Left"

+ 17 - 0
src/PixiEditor/Views/Main/MainTitleBar.axaml.cs

@@ -12,6 +12,7 @@ public partial class MainTitleBar : UserControl
 {
 
     private MiniAnimationPlayer miniPlayer;
+    private Panel logoPanel;
     public MainTitleBar()
     {
         InitializeComponent();
@@ -26,6 +27,22 @@ public partial class MainTitleBar : UserControl
     {
         base.OnLoaded(e);
         miniPlayer = this.FindControl<MiniAnimationPlayer>("MiniPlayer");
+        logoPanel = this.FindControl<Panel>("LogoPanel");
+
+        if (IOperatingSystem.Current.IsMacOs && VisualRoot is Window window && logoPanel != null)
+        {
+            window.PropertyChanged += OnPropertyChanged;
+        }
+    }
+
+    private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Property.Name == nameof(Window.WindowState) && logoPanel != null)
+        {
+            logoPanel.Margin = e.NewValue is WindowState and WindowState.FullScreen
+                ? new Thickness(10, 0, 0, 0)
+                : new Thickness(75, 0, 0, 0);
+        }
     }
 
     protected override void OnSizeChanged(SizeChangedEventArgs e)

+ 47 - 0
src/PixiEditor/Views/Nodes/Connection.cs

@@ -0,0 +1,47 @@
+using Avalonia;
+using Avalonia.Media;
+using Drawie.Numerics;
+
+namespace PixiEditor.Views.Nodes;
+
+public class Connection
+{
+    public VecD StartPoint { get; set; }
+    public VecD EndPoint { get; set; }
+    public LinearGradientBrush LineBrush { get; set; }
+    public double Thickness { get; set; }
+
+    private Pen pen = new() { LineCap = PenLineCap.Round };
+
+    public void Render(DrawingContext context)
+    {
+        var p1 = new Point(StartPoint.X, StartPoint.Y);
+        var p2 = new Point(EndPoint.X, EndPoint.Y);
+
+        // curved line
+        var controlPoint = new Point((p1.X + p2.X) / 2, p1.Y);
+        var controlPoint2 = new Point((p1.X + p2.X) / 2, p2.Y);
+
+        if (p1.X < p2.X)
+        {
+            p1 = new Point(p1.X - 5, p1.Y);
+            p2 = new Point(p2.X + 5, p2.Y);
+
+            controlPoint2 = new Point(p2.X, (p1.Y + p2.Y) / 2);
+            controlPoint = new Point(p1.X, (p1.Y + p2.Y) / 2);
+        }
+
+        var geometry = new StreamGeometry();
+        using var ctx = geometry.Open();
+        ctx.BeginFigure(p1, false);
+        ctx.CubicBezierTo(controlPoint, controlPoint2, p2);
+
+        LineBrush.StartPoint = new RelativePoint(p1.X, p1.Y, RelativeUnit.Absolute);
+        LineBrush.EndPoint = new RelativePoint(p2.X, p2.Y, RelativeUnit.Absolute);
+
+        pen.Brush = LineBrush;
+        pen.Thickness = Thickness;
+
+        context.DrawGeometry(LineBrush, pen, geometry);
+    }
+}

+ 110 - 0
src/PixiEditor/Views/Nodes/ConnectionRenderer.cs

@@ -0,0 +1,110 @@
+using System.Collections.ObjectModel;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Drawie.Numerics;
+using PixiEditor.Helpers.Converters;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.Views.Nodes;
+
+public class ConnectionRenderer : Control
+{
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty =
+        AvaloniaProperty.Register<ConnectionRenderer, SocketsInfo>(
+            nameof(SocketsInfo));
+
+    public SocketsInfo SocketsInfo
+    {
+        get => GetValue(SocketsInfoProperty);
+        set => SetValue(SocketsInfoProperty, value);
+    }
+
+    internal static readonly StyledProperty<ObservableCollection<NodeConnectionViewModel>> ConnectionsProperty =
+        AvaloniaProperty.Register<ConnectionRenderer, ObservableCollection<NodeConnectionViewModel>>(
+            nameof(Connections));
+
+    internal ObservableCollection<NodeConnectionViewModel> Connections
+    {
+        get => GetValue(ConnectionsProperty);
+        set => SetValue(ConnectionsProperty, value);
+    }
+
+    private double thickness = 2;
+    public static readonly StyledProperty<TransformGroup> ContentTransformProperty = AvaloniaProperty.Register<ConnectionRenderer, TransformGroup>("ContentTransform");
+
+    public override void Render(DrawingContext context)
+    {
+        if (SocketsInfo == null || Connections == null)
+        {
+            return;
+        }
+
+        foreach (var connection in Connections)
+        {
+            var inputSocket =
+                SocketsInfo.Sockets.TryGetValue($"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}", out var socket)
+                    ? socket
+                    : null;
+            var outputSocket =
+                SocketsInfo.Sockets.TryGetValue($"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}", out socket)
+                    ? socket
+                    : null;
+
+            if (inputSocket == null || outputSocket == null)
+            {
+                continue;
+            }
+
+            Point startPoint = SocketsInfo.GetSocketPosition(inputSocket);
+            Point endPoint = SocketsInfo.GetSocketPosition(outputSocket);
+
+            LinearGradientBrush brush = new LinearGradientBrush
+            {
+                GradientStops = new GradientStops
+                {
+                    new GradientStop { Offset = 0, Color = Color.FromRgb(85, 85, 85) },
+                    new GradientStop { Offset = 0.05, Color = SocketColorConverter.SocketToColor(connection.InputProperty.SocketBrush) },
+                    new GradientStop { Offset = 0.95, Color = SocketColorConverter.SocketToColor(connection.OutputProperty.SocketBrush) },
+                    new GradientStop { Offset = 1, Color = Color.FromRgb(85, 85, 85) }
+                }
+            };
+
+            Pen pen = new() { LineCap = PenLineCap.Round };
+
+            var p1 = new Point(startPoint.X, startPoint.Y);
+            var p2 = new Point(endPoint.X, endPoint.Y);
+
+            // curved line
+            var controlPoint = new Point((p1.X + p2.X) / 2, p1.Y);
+            var controlPoint2 = new Point((p1.X + p2.X) / 2, p2.Y);
+
+            if (p1.X < p2.X)
+            {
+                p1 = new Point(p1.X - 5, p1.Y);
+                p2 = new Point(p2.X + 5, p2.Y);
+
+                controlPoint2 = new Point(p2.X, (p1.Y + p2.Y) / 2);
+                controlPoint = new Point(p1.X, (p1.Y + p2.Y) / 2);
+            }
+
+            var geometry = new StreamGeometry();
+            using var ctx = geometry.Open();
+            ctx.BeginFigure(p1, false);
+            ctx.CubicBezierTo(controlPoint, controlPoint2, p2);
+
+            brush.StartPoint = new RelativePoint(new Point(0, 0), RelativeUnit.Relative);
+            brush.EndPoint = new RelativePoint(new Point(1, 0), RelativeUnit.Relative);
+
+            /*if (startPoint.X < endPoint.X)
+            {
+                (brush.StartPoint, brush.EndPoint) = (brush.EndPoint, brush.StartPoint);
+            }*/
+
+            pen.Brush = brush;
+            pen.Thickness = thickness;
+
+            context.DrawGeometry(brush, pen, geometry);
+        }
+    }
+}

+ 72 - 9
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -87,6 +87,15 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             "CreateNodeFromContextCommand");
 
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty = AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
+        nameof(SocketsInfo));
+
+    public SocketsInfo SocketsInfo
+    {
+        get => GetValue(SocketsInfoProperty);
+        set => SetValue(SocketsInfoProperty, value);
+    }
+
     public ICommand CreateNodeFromContextCommand
     {
         get => GetValue(CreateNodeFromContextCommandProperty);
@@ -214,6 +223,12 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, int>("ActiveFrame");
 
     private Panel rootPanel;
+    private ConnectionRenderer connectionRenderer;
+
+    static NodeGraphView()
+    {
+        NodeGraphProperty.Changed.Subscribe(OnNodeGraphChanged);
+    }
 
     public NodeGraphView()
     {
@@ -226,6 +241,16 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         AllNodeTypes = new ObservableCollection<Type>(GatherAssemblyTypes<NodeViewModel>());
         AllNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(AllNodeTypes.Select(x => new NodeTypeInfo(x)));
+        SocketsInfo = new SocketsInfo((socket =>
+        {
+            var canvas = nodeItemsControl.FindDescendantOfType<Canvas>();
+            var view = nodeViewsCache.FirstOrDefault(x =>
+                x is ContentPresenter { Child: NodeView nodeView } && nodeView.Node == socket.Node) as ContentPresenter;
+            if (view == null)
+                return new Point(0, 0);
+
+            return (view.Child as NodeView).GetSocketPoint(socket, canvas);
+        }));
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -234,6 +259,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         nodeItemsControl = e.NameScope.Find<ItemsControl>("PART_Nodes");
         connectionItemsControl = e.NameScope.Find<ItemsControl>("PART_Connections");
         selectionRectangle = e.NameScope.Find<Rectangle>("PART_SelectionRectangle");
+        connectionRenderer = e.NameScope.Find<ConnectionRenderer>("PART_ConnectionRenderer");
 
         rootPanel = e.NameScope.Find<Panel>("PART_RootPanel");
 
@@ -369,10 +395,7 @@ internal class NodeGraphView : Zoombox.Zoombox
             if (sender is NodeViewModel node)
             {
                 Dispatcher.UIThread.Post(
-                    () =>
-                    {
-                        UpdateConnections(FindNodeView(node));
-                    }, DispatcherPriority.Render);
+                    UpdateConnections, DispatcherPriority.Render);
             }
         }
     }
@@ -566,7 +589,7 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         if (e.Source is NodeView nodeView)
         {
-            UpdateConnections(nodeView);
+            UpdateConnections(/*nodeView*/);
         }
     }
 
@@ -616,7 +639,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         if (e.Property == BoundsProperty)
         {
             NodeView nodeView = (NodeView)sender!;
-            UpdateConnections(nodeView);
+            UpdateConnections(/*nodeView*/);
         }
     }
 
@@ -650,9 +673,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
     }
 
-    private void UpdateConnections(NodeView nodeView)
+    private void UpdateConnections(/*NodeView nodeView*/)
     {
-        if (nodeView == null)
+        connectionRenderer?.InvalidateVisual();
+        /*if (nodeView == null)
         {
             return;
         }
@@ -661,11 +685,13 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             NodePropertyViewModel property = (NodePropertyViewModel)propertyView.DataContext;
             UpdateConnectionView(property);
-        }
+        }*/
     }
 
     private void UpdateConnectionView(NodePropertyViewModel? propertyView)
     {
+        if (connectionItemsControl == null) return;
+
         foreach (var connection in connectionItemsControl.ItemsPanelRoot.Children)
         {
             if (connection is ContentPresenter contentPresenter)
@@ -789,4 +815,41 @@ internal class NodeGraphView : Zoombox.Zoombox
             node.IsNodeSelected = false;
         }
     }
+
+    private void OnConnectionsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        if (e.Action == NotifyCollectionChangedAction.Add)
+        {
+            foreach (NodeConnectionViewModel connection in e.NewItems)
+            {
+                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
+                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+            }
+        }
+        else if (e.Action == NotifyCollectionChangedAction.Reset)
+        {
+            SocketsInfo.Sockets.Clear();
+        }
+
+        UpdateConnections();
+    }
+
+    private static void OnNodeGraphChanged(AvaloniaPropertyChangedEventArgs<INodeGraphHandler> e)
+    {
+        var nodeGraph = e.Sender as NodeGraphView;
+        if (e.OldValue.Value != null)
+        {
+            e.OldValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
+        }
+        if (e.NewValue.Value != null)
+        {
+            e.NewValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
+            nodeGraph.SocketsInfo.Sockets.Clear();
+            foreach (var connection in e.NewValue.Value.Connections)
+            {
+                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
+                nodeGraph.SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+            }
+        }
+    }
 }

+ 36 - 0
src/PixiEditor/Views/Nodes/NodeView.cs

@@ -5,6 +5,7 @@ using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Media;
 using Avalonia.Threading;
@@ -66,6 +67,15 @@ public class NodeView : TemplatedControl
     public static readonly StyledProperty<string> IconProperty = AvaloniaProperty.Register<NodeView, string>(
         nameof(Icon));
 
+    public static readonly StyledProperty<bool> CanRenderPreviewProperty = AvaloniaProperty.Register<NodeView, bool>(
+        nameof(CanRenderPreview));
+
+    public bool CanRenderPreview
+    {
+        get => GetValue(CanRenderPreviewProperty);
+        private set => SetValue(CanRenderPreviewProperty, value);
+    }
+
     public string Icon
     {
         get => GetValue(IconProperty);
@@ -167,6 +177,7 @@ public class NodeView : TemplatedControl
     static NodeView()
     {
         IsSelectedProperty.Changed.Subscribe(NodeSelectionChanged);
+        ResultPreviewProperty.Changed.Subscribe(PainterChanged);
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -300,4 +311,29 @@ public class NodeView : TemplatedControl
             nodeView.PseudoClasses.Set(":selected", e.NewValue.Value);
         }
     }
+
+    private static void PainterChanged(AvaloniaPropertyChangedEventArgs<PreviewPainter> e)
+    {
+        if (e.Sender is NodeView nodeView)
+        {
+            if (e.OldValue.Value is not null)
+            {
+                e.OldValue.Value.CanRenderChanged -= nodeView.ResultPreview_CanRenderChanged;
+            }
+
+            if (e.NewValue.Value is not null)
+            {
+                e.NewValue.Value.CanRenderChanged += nodeView.ResultPreview_CanRenderChanged;
+                nodeView.ResultPreview_CanRenderChanged(e.NewValue.Value.CanRender);
+            }
+        }
+    }
+
+    private void ResultPreview_CanRenderChanged(bool canRender)
+    {
+        if (CanRenderPreview != canRender)
+        {
+            CanRenderPreview = canRender;
+        }
+    }
 }

+ 16 - 0
src/PixiEditor/Views/Nodes/SocketsInfo.cs

@@ -0,0 +1,16 @@
+using Avalonia;
+using PixiEditor.Models.Handlers;
+using PixiEditor.Views.Nodes.Properties;
+
+namespace PixiEditor.Views.Nodes;
+
+public class SocketsInfo
+{
+    public Dictionary<string, INodePropertyHandler> Sockets { get; } = new();
+    public Func<INodePropertyHandler, Point> GetSocketPosition { get; set; }
+
+    public SocketsInfo(Func<INodePropertyHandler, Point> getSocketPosition)
+    {
+        GetSocketPosition = getSocketPosition;
+    }
+}

+ 1 - 1
src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml

@@ -14,7 +14,7 @@
 
     <Grid>
         <CheckBox VerticalAlignment="Center" Focusable="False"
-                  localization:Translator.Key="{Binding Label}"
+                  localization:Translator.LocalizedString="{Binding Label}"
                   IsChecked="{Binding Value, Mode=TwoWay}">
            <CheckBox.IsVisible>
                <MultiBinding Converter="{converters:AllTrueConverter}">

+ 12 - 0
src/PixiEditor/Views/Visuals/PreviewPainterControl.cs

@@ -57,6 +57,18 @@ public class PreviewPainterControl : DrawieControl
         FrameToRender = frameToRender;
     }
 
+    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+    {
+        base.OnDetachedFromVisualTree(e);
+        if (PreviewPainter != null && painterInstance != null)
+        {
+            PreviewPainter.RemovePainterInstance(painterInstance.RequestId);
+            painterInstance.RequestMatrix = null;
+            painterInstance.RequestRepaint = null;
+            painterInstance = null;
+        }
+    }
+
     private static void PainterChanged(AvaloniaPropertyChangedEventArgs<PreviewPainter> args)
     {
         var sender = args.Sender as PreviewPainterControl;