Browse Source

Merge branch 'funcy-nodes' into node-backend

flabbet 1 year ago
parent
commit
75ca0a2327
76 changed files with 1547 additions and 84 deletions
  1. 8 3
      src/PixiEditor.AvaloniaUI/Helpers/Converters/BoolToValueConverter.cs
  2. 26 0
      src/PixiEditor.AvaloniaUI/Helpers/Converters/NodeInternalNameToStyleConverter.cs
  3. 20 0
      src/PixiEditor.AvaloniaUI/Helpers/Converters/UnsetSkipMultiConverter.cs
  4. 44 3
      src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs
  5. 4 0
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs
  6. 3 1
      src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs
  7. 5 0
      src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs
  8. 1 0
      src/PixiEditor.AvaloniaUI/Styles/PixiEditor.Controls.axaml
  9. 21 0
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeFrameView.axaml
  10. 51 1
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml
  11. 6 5
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml
  12. 11 6
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml
  13. 5 10
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml
  14. 56 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/NodeGraphViewModel.cs
  15. 42 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeFrameViewModel.cs
  16. 93 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeFrameViewModelBase.cs
  17. 49 12
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs
  18. 2 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs
  19. 74 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeZoneViewModel.cs
  20. 8 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/BooleanPropertyViewModel.cs
  21. 10 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorPropertyViewModel.cs
  22. 8 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/DoublePropertyViewModel.cs
  23. 13 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/GenericEnumPropertyViewModel.cs
  24. 35 0
      src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/VecIPropertyViewModel.cs
  25. 12 0
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs
  26. 33 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/NodeFrameView.cs
  27. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/BooleanPropertyView.axaml
  28. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/BooleanPropertyView.axaml.cs
  29. 16 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml
  30. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml.cs
  31. 16 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/DoublePropertyView.axaml
  32. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/DoublePropertyView.axaml.cs
  33. 16 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericEnumPropertyView.axaml
  34. 13 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs
  35. 8 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericPropertyView.axaml
  36. 7 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs
  37. 18 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml
  38. 14 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml.cs
  39. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNodeFrame_ChangeInfo.cs
  40. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNodeZone_ChangeInfo.cs
  41. 8 4
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs
  42. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/DeleteNodeFrame_ChangeInfo.cs
  43. 3 0
      src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/PropertyValueUpdated_ChangeInfo.cs
  44. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs
  45. 3 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs
  46. 2 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs
  47. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs
  48. 22 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldInputProperty.cs
  49. 11 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldOutputProperty.cs
  50. 8 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/IFieldInputProperty.cs
  51. 5 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/FieldContext.cs
  52. 2 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs
  53. 8 15
      src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeFactory.cs
  54. 48 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineColorNode.cs
  55. 41 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs
  56. 29 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSizeNode.cs
  57. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs
  58. 59 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs
  59. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs
  60. 62 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs
  61. 29 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs
  62. 37 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateColorNode.cs
  63. 31 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateVecDNode.cs
  64. 61 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateModifyImageNodePair_Change.cs
  65. 32 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodeFrame_Change.cs
  66. 5 5
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs
  67. 67 0
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs
  68. 9 0
      src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs
  69. 4 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IPixmapImplementation.cs
  70. 2 1
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/IImageImplementation.cs
  71. 5 0
      src/PixiEditor.DrawingApi.Core/Surface/ImageData/Image.cs
  72. 9 0
      src/PixiEditor.DrawingApi.Core/Surface/Pixmap.cs
  73. 9 3
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs
  74. 7 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPixmapImplementation.cs
  75. 9 9
      src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs
  76. 23 0
      src/PixiEditor.UI.Common/Accents/Base.axaml

+ 8 - 3
src/PixiEditor.AvaloniaUI/Helpers/Converters/BoolToValueConverter.cs

@@ -14,19 +14,24 @@ internal class BoolToValueConverter : MarkupConverter
     {
     {
         if (value is bool and true)
         if (value is bool and true)
         {
         {
-            return GetValue(TrueValue);
+            return GetValue(TrueValue, targetType);
         }
         }
 
 
-        return GetValue(FalseValue);
+        return GetValue(FalseValue, targetType);
     }
     }
 
 
-    private object GetValue(object value)
+    private object GetValue(object value, Type targetType)
     {
     {
         if (value is string s && s.StartsWith("localized:"))
         if (value is string s && s.StartsWith("localized:"))
         {
         {
             return new LocalizedString(s.Split("localized:")[1]);
             return new LocalizedString(s.Split("localized:")[1]);
         }
         }
 
 
+        if (value is string enumString && targetType.IsEnum)
+        {
+            return Enum.Parse(targetType, enumString);
+        }
+
         return value;
         return value;
     }
     }
 
 

+ 26 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/NodeInternalNameToStyleConverter.cs

@@ -0,0 +1,26 @@
+using System.Globalization;
+using System.Net.Mime;
+using Avalonia;
+using Avalonia.Styling;
+using PixiEditor.UI.Common.Converters;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class NodeInternalNameToStyleConverter : SingleInstanceConverter<NodeInternalNameToStyleConverter>
+{
+    public override object Convert(object? value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value == null)
+            return AvaloniaProperty.UnsetValue;
+        
+        string s = (string)value;
+        s = s.Replace(".", string.Empty);
+        
+        if (Application.Current.Styles.TryGetResource($"{s}{parameter}", Application.Current.ActualThemeVariant, out var output))
+        {
+            return output;
+        }
+
+        return AvaloniaProperty.UnsetValue;
+    }
+}

+ 20 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/UnsetSkipMultiConverter.cs

@@ -0,0 +1,20 @@
+using System.Globalization;
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class UnsetSkipMultiConverter : SingleInstanceMultiValueConverter<UnsetSkipMultiConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => value;
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        foreach (var value in values)
+        {
+            if (value is not UnsetValueType)
+                return value;
+        }
+
+        return AvaloniaProperty.UnsetValue;
+    }
+}

+ 44 - 3
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -170,12 +170,24 @@ internal class DocumentUpdater
             case DeleteNode_ChangeInfo info:
             case DeleteNode_ChangeInfo info:
                 ProcessDeleteNode(info);
                 ProcessDeleteNode(info);
                 break;
                 break;
+            case CreateNodeFrame_ChangeInfo info:
+                ProcessCreateNodeFrame(info);
+                break;
+            case CreateNodeZone_ChangeInfo info:
+                ProcessCreateNodeZone(info);
+                break;
+            case DeleteNodeFrame_ChangeInfo info:
+                ProcessDeleteNodeFrame(info);
+                break;
             case ConnectProperty_ChangeInfo info:
             case ConnectProperty_ChangeInfo info:
                 ProcessConnectProperty(info);
                 ProcessConnectProperty(info);
                 break;
                 break;
             case NodePosition_ChangeInfo info:
             case NodePosition_ChangeInfo info:
                 ProcessNodePosition(info);
                 ProcessNodePosition(info);
                 break;
                 break;
+            case PropertyValueUpdated_ChangeInfo info:
+                ProcessNodePropertyValueUpdated(info);
+                break;
         }
         }
     }
     }
 
 
@@ -477,9 +489,14 @@ internal class DocumentUpdater
     
     
     private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     private void ProcessCreateNode<T>(CreateNode_ChangeInfo info) where T : NodeViewModel, new()
     {
     {
-        T node = new T() { 
-            NodeName = info.NodeName, Id = info.Id, 
-            Document = (DocumentViewModel)doc, Internals = helper };
+        T node = new T()
+        {
+            NodeName = info.NodeName,
+            InternalName = info.InternalName,
+            Id = info.Id,
+            Document = (DocumentViewModel)doc,
+            Internals = helper
+        };
 
 
         node.SetPosition(info.Position);
         node.SetPosition(info.Position);
         
         
@@ -499,6 +516,7 @@ internal class DocumentUpdater
             prop.DisplayName = input.DisplayName;
             prop.DisplayName = input.DisplayName;
             prop.PropertyName = input.PropertyName;
             prop.PropertyName = input.PropertyName;
             prop.IsInput = isInput;
             prop.IsInput = isInput;
+            prop.IsFunc = input.ValueType.IsAssignableTo(typeof(Delegate));
             inputs.Add(prop);
             inputs.Add(prop);
         }
         }
         
         
@@ -511,6 +529,21 @@ internal class DocumentUpdater
         doc.NodeGraphHandler.RemoveNode(info.Id);
         doc.NodeGraphHandler.RemoveNode(info.Id);
     }
     }
     
     
+    private void ProcessCreateNodeFrame(CreateNodeFrame_ChangeInfo info)
+    {
+        doc.NodeGraphHandler.AddFrame(info.Id, info.NodeIds);
+    }
+
+    private void ProcessCreateNodeZone(CreateNodeZone_ChangeInfo info)
+    {
+        doc.NodeGraphHandler.AddZone(info.Id, info.internalName, info.StartId, info.EndId);
+    }
+
+    private void ProcessDeleteNodeFrame(DeleteNodeFrame_ChangeInfo info)
+    {
+        doc.NodeGraphHandler.RemoveFrame(info.Id);
+    }
+
     private void ProcessConnectProperty(ConnectProperty_ChangeInfo info)
     private void ProcessConnectProperty(ConnectProperty_ChangeInfo info)
     {
     {
         NodeViewModel outputNode = info.OutputNodeId.HasValue ? doc.StructureHelper.FindNode<NodeViewModel>(info.OutputNodeId.Value) : null;
         NodeViewModel outputNode = info.OutputNodeId.HasValue ? doc.StructureHelper.FindNode<NodeViewModel>(info.OutputNodeId.Value) : null;
@@ -539,4 +572,12 @@ internal class DocumentUpdater
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
         node.SetPosition(info.NewPosition);
         node.SetPosition(info.NewPosition);
     }
     }
+    
+    private void ProcessNodePropertyValueUpdated(PropertyValueUpdated_ChangeInfo info)
+    {
+        NodeViewModel node = doc.StructureHelper.FindNode<NodeViewModel>(info.NodeId);
+        var property = node.FindInputProperty(info.Property);
+        
+        property.InternalSetValue(info.Value);
+    }
 }
 }

+ 4 - 0
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeGraphHandler.cs

@@ -8,11 +8,15 @@ internal interface INodeGraphHandler
 {
 {
    public ObservableCollection<INodeHandler> AllNodes { get; }
    public ObservableCollection<INodeHandler> AllNodes { get; }
    public ObservableCollection<NodeConnectionViewModel> Connections { get; }
    public ObservableCollection<NodeConnectionViewModel> Connections { get; }
+   public ObservableCollection<NodeFrameViewModelBase> Frames { get; }
    public INodeHandler OutputNode { get; }
    public INodeHandler OutputNode { get; }
    public StructureTree StructureTree { get; }
    public StructureTree StructureTree { get; }
    public bool TryTraverse(Func<INodeHandler, bool> func);
    public bool TryTraverse(Func<INodeHandler, bool> func);
    public void AddNode(INodeHandler node);
    public void AddNode(INodeHandler node);
    public void RemoveNode(Guid nodeId);
    public void RemoveNode(Guid nodeId);
+   public void AddFrame(Guid frameId, IEnumerable<Guid> nodeIds);
+   public void AddZone(Guid frameId, string internalName, Guid startId, Guid endId);
+   public void RemoveFrame(Guid frameId);
    public void SetConnection(NodeConnectionViewModel connection);
    public void SetConnection(NodeConnectionViewModel connection);
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnection(Guid nodeId, string property);
    public void RemoveConnections(Guid nodeId);
    public void RemoveConnections(Guid nodeId);

+ 3 - 1
src/PixiEditor.AvaloniaUI/Models/Handlers/INodeHandler.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
+using System.ComponentModel;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.AvaloniaUI.Models.Structures;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -6,10 +7,11 @@ using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 namespace PixiEditor.AvaloniaUI.Models.Handlers;
 
 
-public interface INodeHandler
+public interface INodeHandler : INotifyPropertyChanged
 {
 {
     public Guid Id { get; }
     public Guid Id { get; }
     public string NodeName { get; set; }
     public string NodeName { get; set; }
+    public string InternalName { get; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Inputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public Surface ResultPreview { get; set; }
     public Surface ResultPreview { get; set; }

+ 5 - 0
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -3,6 +3,7 @@ using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.AvaloniaUI.Models.DocumentPassthroughActions;
 using PixiEditor.ChangeableDocument;
 using PixiEditor.ChangeableDocument;
+using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.ChangeInfos;
 using PixiEditor.ChangeableDocument.ChangeInfos;
@@ -131,6 +132,10 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
                     AddWholeCanvasToEveryImagePreview();
                     break;
                     break;
+                case PropertyValueUpdated_ChangeInfo:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToEveryImagePreview();
+                    break;
             }
             }
         }
         }
     }
     }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Styles/PixiEditor.Controls.axaml

@@ -13,6 +13,7 @@
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml"/>
+                <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeFrameView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml"/>
                 <MergeResourceInclude Source="avares://PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml"/>

+ 21 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeFrameView.axaml

@@ -0,0 +1,21 @@
+<ResourceDictionary xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
+                    xmlns:visuals="clr-namespace:PixiEditor.AvaloniaUI.Views.Visuals"
+                    xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input">
+    <ControlTheme TargetType="nodes:NodeFrameView" x:Key="{x:Type nodes:NodeFrameView}">
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Grid Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}">
+                        <Rectangle Fill="{TemplateBinding Background}"
+                                   Stroke="{TemplateBinding BorderBrush}"
+                                   StrokeThickness="2" RadiusX="10" RadiusY="10"
+                                   Width="{Binding Size.X, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
+                                   Height="{Binding Size.Y, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}" />
+                    </Grid>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </ControlTheme>
+</ResourceDictionary>

+ 51 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeGraphView.axaml

@@ -1,6 +1,7 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes">
+                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
+                    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
     <ControlTheme TargetType="nodes:NodeGraphView" x:Key="{x:Type nodes:NodeGraphView}">
     <ControlTheme TargetType="nodes:NodeGraphView" x:Key="{x:Type nodes:NodeGraphView}">
         <Setter Property="ZoomMode" Value="Move" />
         <Setter Property="ZoomMode" Value="Move" />
         <Setter Property="Template">
         <Setter Property="Template">
@@ -39,6 +40,7 @@
                                         Node="{Binding}"
                                         Node="{Binding}"
                                         DisplayName="{Binding NodeName}"
                                         DisplayName="{Binding NodeName}"
                                         Inputs="{Binding Inputs}"
                                         Inputs="{Binding Inputs}"
+                                        BorderBrush="{Binding InternalName, Converter={converters:NodeInternalNameToStyleConverter}, ConverterParameter='BorderBrush'}"
                                         Outputs="{Binding Outputs}"
                                         Outputs="{Binding Outputs}"
                                         IsSelected="{Binding IsSelected}"
                                         IsSelected="{Binding IsSelected}"
                                         SelectNodeCommand="{Binding SelectNodeCommand,
                                         SelectNodeCommand="{Binding SelectNodeCommand,
@@ -87,6 +89,54 @@
                                 </DataTemplate>
                                 </DataTemplate>
                             </ItemsControl.ItemTemplate>
                             </ItemsControl.ItemTemplate>
                         </ItemsControl>
                         </ItemsControl>
+                    <ItemsControl
+                        ZIndex="-1"
+                        Name="PART_Frames"
+                        ItemsSource="{Binding NodeGraph.Frames, 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:NodeFrameView
+                                        TopLeft="{Binding TopLeft}"
+                                        BottomRight="{Binding BottomRight}"
+                                        Size="{Binding Size}">
+                                        <nodes:NodeFrameView.Background>
+                                            <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
+                                                 <Binding Path="InternalName" Converter="{converters:NodeInternalNameToStyleConverter}" ConverterParameter="BackgroundBrush" />
+                                                 <DynamicResource ResourceKey="NodeFrameBackgroundBrush"/>
+                                            </MultiBinding>
+                                        </nodes:NodeFrameView.Background>
+                                        <nodes:NodeFrameView.BorderBrush>
+                                            <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
+                                                <Binding Path="InternalName" Converter="{converters:NodeInternalNameToStyleConverter}" ConverterParameter="BorderBrush" />
+                                                <DynamicResource ResourceKey="NodeFrameBorderBrush"/>
+                                            </MultiBinding>
+                                        </nodes:NodeFrameView.BorderBrush>
+                                    </nodes:NodeFrameView>
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                            <ItemsControl.ItemContainerTheme>
+                                <ControlTheme TargetType="ContentPresenter">
+                                    <Setter Property="Canvas.Left" Value="{Binding TopLeft.X}" />
+                                    <Setter Property="Canvas.Top" Value="{Binding TopLeft.Y}" />
+                                </ControlTheme>
+                            </ItemsControl.ItemContainerTheme>
+                        </ItemsControl>
                 </Grid>
                 </Grid>
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>

+ 6 - 5
src/PixiEditor.AvaloniaUI/Styles/Templates/NodePropertyViewTemplate.axaml

@@ -6,23 +6,24 @@
         <Setter Property="ClipToBounds" Value="False" />
         <Setter Property="ClipToBounds" Value="False" />
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
-                <Grid Margin="-5, 2">
-                    <Grid.ColumnDefinitions>10*, *, 10*</Grid.ColumnDefinitions>
+                <Grid Margin="-5, 2" ColumnDefinitions="15, *, 15" MinHeight="18">
                     <properties:NodeSocket Name="PART_InputSocket"
                     <properties:NodeSocket Name="PART_InputSocket"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
                                            Node="{Binding DataContext.Node, RelativeSource={RelativeSource TemplatedParent}}"
                                            Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}"
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}"
+                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}"
                                            IsVisible="{Binding DataContext.IsInput, 
                                            IsVisible="{Binding DataContext.IsInput, 
                     RelativeSource={RelativeSource TemplatedParent}}">
                     RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>
                         <properties:NodeSocket.IsInput>
                             <x:Boolean>True</x:Boolean>
                             <x:Boolean>True</x:Boolean>
                         </properties:NodeSocket.IsInput>
                         </properties:NodeSocket.IsInput>
                     </properties:NodeSocket>
                     </properties:NodeSocket>
-                    <ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}" />
+                    <ContentPresenter Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Top" Content="{TemplateBinding Content}" />
                     <properties:NodeSocket Grid.Column="2" Name="PART_OutputSocket"
                     <properties:NodeSocket Grid.Column="2" Name="PART_OutputSocket"
                                            Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
                                            Label="{Binding DataContext.DisplayName, RelativeSource={RelativeSource TemplatedParent}}"
-                                           IsVisible="{Binding !DataContext.IsInput,
-                    RelativeSource={RelativeSource TemplatedParent}}"
+                                           HorizontalAlignment="Right"
+                                           IsFunc="{Binding DataContext.IsFunc, RelativeSource={RelativeSource TemplatedParent}}"
+                                           IsVisible="{Binding !DataContext.IsInput,RelativeSource={RelativeSource TemplatedParent}}"
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}">
                                            SocketBrush="{Binding DataContext.SocketBrush, RelativeSource={RelativeSource TemplatedParent}}">
                         <properties:NodeSocket.IsInput>
                         <properties:NodeSocket.IsInput>
                             <x:Boolean>False</x:Boolean>
                             <x:Boolean>False</x:Boolean>

+ 11 - 6
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml

@@ -1,15 +1,20 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-                    xmlns:nodes="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes"
-                    xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
-                    xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions">
+                    xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties">
     <ControlTheme TargetType="properties:NodeSocket" x:Key="{x:Type properties:NodeSocket}">
     <ControlTheme TargetType="properties:NodeSocket" x:Key="{x:Type properties:NodeSocket}">
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
                 <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                 <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
-                    <TextBlock VerticalAlignment="Center" Margin="0, 0, 2, 0" ui:Translator.Key="{TemplateBinding Label}" IsVisible="{Binding !IsInput, RelativeSource={RelativeSource TemplatedParent}}"/>
-                    <Ellipse Width="10" Height="10" Fill="{TemplateBinding SocketBrush}" Name="PART_ConnectPort"/>
-                    <TextBlock VerticalAlignment="Center" Margin="2, 0, 0, 0" ui:Translator.Key="{TemplateBinding Label}" IsVisible="{Binding IsInput, RelativeSource={RelativeSource TemplatedParent}}"/>
+                    <Grid Name="PART_ConnectPort">
+                        <Ellipse Width="10" Height="10" 
+                                 Fill="{TemplateBinding SocketBrush}" 
+                                 IsVisible="{Binding !IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
+                        <Rectangle Width="10" Height="10"
+                                   RadiusX="1" RadiusY="1"
+                                   Fill="{TemplateBinding SocketBrush}"
+                                   RenderTransform="rotate(45deg)"
+                                   IsVisible="{Binding IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
+                    </Grid>
                 </StackPanel>
                 </StackPanel>
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>

+ 5 - 10
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeView.axaml

@@ -34,28 +34,23 @@
                                            FontWeight="Bold" />
                                            FontWeight="Bold" />
                             </Border>
                             </Border>
                             <Border Grid.Row="1" Background="{DynamicResource ThemeControlMidBrush}">
                             <Border Grid.Row="1" Background="{DynamicResource ThemeControlMidBrush}">
-                                <Grid>
-                                    <Grid.ColumnDefinitions>
-                                        <ColumnDefinition Width="0.5*" />
-                                        <ColumnDefinition Width="0.5*" />
-                                    </Grid.ColumnDefinitions>
-
-                                    <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
+                                <StackPanel>
+                                    <ItemsControl ItemsSource="{TemplateBinding Outputs}"
+                                                  ClipToBounds="False">
                                         <ItemsControl.ItemContainerTheme>
                                         <ItemsControl.ItemContainerTheme>
                                             <ControlTheme TargetType="ContentPresenter">
                                             <ControlTheme TargetType="ContentPresenter">
                                                 <Setter Property="DataContext" Value="." />
                                                 <Setter Property="DataContext" Value="." />
                                             </ControlTheme>
                                             </ControlTheme>
                                         </ItemsControl.ItemContainerTheme>
                                         </ItemsControl.ItemContainerTheme>
                                     </ItemsControl>
                                     </ItemsControl>
-                                    <ItemsControl Grid.Column="1" ItemsSource="{TemplateBinding Outputs}"
-                                                  ClipToBounds="False">
+                                    <ItemsControl ItemsSource="{TemplateBinding Inputs}" ClipToBounds="False">
                                         <ItemsControl.ItemContainerTheme>
                                         <ItemsControl.ItemContainerTheme>
                                             <ControlTheme TargetType="ContentPresenter">
                                             <ControlTheme TargetType="ContentPresenter">
                                                 <Setter Property="DataContext" Value="." />
                                                 <Setter Property="DataContext" Value="." />
                                             </ControlTheme>
                                             </ControlTheme>
                                         </ItemsControl.ItemContainerTheme>
                                         </ItemsControl.ItemContainerTheme>
                                     </ItemsControl>
                                     </ItemsControl>
-                                </Grid>
+                                </StackPanel>
                             </Border>
                             </Border>
                             <Border CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
                             <Border CornerRadius="0, 0, 4.5, 4.5" Grid.Row="2" ClipToBounds="True">
                                 <visuals:SurfaceControl Width="200" Height="200"
                                 <visuals:SurfaceControl Width="200" Height="200"

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

@@ -5,6 +5,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 
 
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 namespace PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -14,6 +15,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
     public DocumentViewModel DocumentViewModel { get; }
     public DocumentViewModel DocumentViewModel { get; }
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
+    public ObservableCollection<NodeFrameViewModelBase> Frames { get; } = new();
     public StructureTree StructureTree { get; } = new();
     public StructureTree StructureTree { get; } = new();
     public INodeHandler? OutputNode { get; private set; }
     public INodeHandler? OutputNode { get; private set; }
 
 
@@ -48,6 +50,32 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         StructureTree.Update(this);
         StructureTree.Update(this);
     }
     }
 
 
+    public void AddFrame(Guid frameId, IEnumerable<Guid> nodes)
+    {
+        var frame = new NodeFrameViewModel(frameId, AllNodes.Where(x => nodes.Contains(x.Id)));
+        
+        Frames.Add(frame);
+    }
+
+    public void AddZone(Guid frameId, string internalName, Guid startId, Guid endId)
+    {
+        var start = AllNodes.First(x => x.Id == startId);
+        var end = AllNodes.First(x => x.Id == endId);
+        
+        var zone = new NodeZoneViewModel(frameId, internalName, start, end);
+        
+        Frames.Add(zone);
+    }
+
+    public void RemoveFrame(Guid guid)
+    {
+        var frame = Frames.FirstOrDefault(x => x.Id == guid);
+
+        if (frame == null) return;
+
+        Frames.Remove(frame);
+    }
+
     public void SetConnection(NodeConnectionViewModel connection)
     public void SetConnection(NodeConnectionViewModel connection)
     {
     {
         var existingInputConnection = Connections.FirstOrDefault(x => x.InputProperty == connection.InputProperty);
         var existingInputConnection = Connections.FirstOrDefault(x => x.InputProperty == connection.InputProperty);
@@ -147,6 +175,11 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
         Internals.ActionAccumulator.AddActions(new NodePosition_Action(node.Id, newPos));
         Internals.ActionAccumulator.AddActions(new NodePosition_Action(node.Id, newPos));
     }
     }
 
 
+    public void UpdatePropertyValue(INodeHandler node, string property, object? value)
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new UpdatePropertyValue_Action(node.Id, property, value));
+    }
+    
     public void EndChangeNodePosition()
     public void EndChangeNodePosition()
     {
     {
         Internals.ActionAccumulator.AddFinishedActions(new EndNodePosition_Action());
         Internals.ActionAccumulator.AddFinishedActions(new EndNodePosition_Action());
@@ -154,7 +187,29 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler
 
 
     public void CreateNode(Type nodeType)
     public void CreateNode(Type nodeType)
     {
     {
-        Internals.ActionAccumulator.AddFinishedActions(new CreateNode_Action(nodeType, Guid.NewGuid()));
+        IAction change;
+        
+        if (nodeType == typeof(ModifyImageLeftNode) || nodeType == typeof(ModifyImageRightNode))
+        {
+            change = new CreateModifyImageNodePair_Action(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid());
+        }
+        else
+        {
+            change = new CreateNode_Action(nodeType, Guid.NewGuid());
+        }
+        
+        Internals.ActionAccumulator.AddFinishedActions(change);
+    }
+
+    // TODO: Remove this
+    public void CreateNodeFrameAroundEverything()
+    {
+        CreateNodeFrame(AllNodes);
+    }
+    
+    public void CreateNodeFrame(IEnumerable<INodeHandler> nodes)
+    {
+        Internals.ActionAccumulator.AddFinishedActions(new CreateNodeFrame_Action(Guid.NewGuid(), nodes.Select(x => x.Id)));
     }
     }
 
 
     public void ConnectProperties(INodePropertyHandler? start, INodePropertyHandler? end)
     public void ConnectProperties(INodePropertyHandler? start, INodePropertyHandler? end)

+ 42 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeFrameViewModel.cs

@@ -0,0 +1,42 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+internal sealed class NodeFrameViewModel : NodeFrameViewModelBase
+{
+    public NodeFrameViewModel(Guid id, IEnumerable<INodeHandler> nodes) : base(id, nodes)
+    {
+        CalculateBounds();
+    }
+
+    protected override void CalculateBounds()
+    {
+        
+        // TODO: Use the GetBounds like in NodeZoneViewModel
+        if (Nodes.Count == 0)
+        {
+            if (TopLeft == BottomRight)
+            {
+                BottomRight = TopLeft + new VecD(100, 100);
+            }
+            
+            return;
+        }
+        
+        var minX = Nodes.Min(n => n.PositionBindable.X) - 30;
+        var minY = Nodes.Min(n => n.PositionBindable.Y) - 45;
+        
+        var maxX = Nodes.Max(n => n.PositionBindable.X) + 130;
+        var maxY = Nodes.Max(n => n.PositionBindable.Y) + 130;
+
+        TopLeft = new VecD(minX, minY);
+        BottomRight = new VecD(maxX, maxY);
+
+        Size = BottomRight - TopLeft;
+    }
+}

+ 93 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeFrameViewModelBase.cs

@@ -0,0 +1,93 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+public abstract class NodeFrameViewModelBase : ObservableObject
+{
+    private Guid id;
+    private VecD topLeft;
+    private VecD bottomRight;
+    private VecD size;
+    
+    public ObservableCollection<INodeHandler> Nodes { get; }
+
+    public string InternalName { get; init; }
+    
+    public Guid Id
+    {
+        get => id;
+        set => SetProperty(ref id, value);
+    }
+    
+    public VecD TopLeft
+    {
+        get => topLeft;
+        set => SetProperty(ref topLeft, value);
+    }
+
+    public VecD BottomRight
+    {
+        get => bottomRight;
+        set => SetProperty(ref bottomRight, value);
+    }
+
+    public VecD Size
+    {
+        get => size;
+        set => SetProperty(ref size, value);
+    }
+
+    public NodeFrameViewModelBase(Guid id, IEnumerable<INodeHandler> nodes)
+    {
+        Id = id;
+        Nodes = new ObservableCollection<INodeHandler>(nodes);
+
+        Nodes.CollectionChanged += OnCollectionChanged;
+        AddHandlers(Nodes);
+    }
+
+    private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        var action = e.Action;
+        if (action != NotifyCollectionChangedAction.Add && action != NotifyCollectionChangedAction.Remove && action != NotifyCollectionChangedAction.Replace && action != NotifyCollectionChangedAction.Reset)
+        {
+            return;
+        }
+        
+        AddHandlers((IEnumerable<NodeViewModel>)e.NewItems);
+        RemoveHandlers((IEnumerable<NodeViewModel>)e.OldItems);
+    }
+
+    private void AddHandlers(IEnumerable<INodeHandler> nodes)
+    {
+        foreach (var node in nodes)
+        {
+            node.PropertyChanged += NodePropertyChanged;
+        }
+    }
+
+    private void RemoveHandlers(IEnumerable<INodeHandler> nodes)
+    {
+        foreach (var node in nodes)
+        {
+            node.PropertyChanged -= NodePropertyChanged;
+        }
+    }
+
+    private void NodePropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(INodeHandler.PositionBindable))
+        {
+            return;
+        }
+        
+        CalculateBounds();
+    }
+
+    protected abstract void CalculateBounds();
+}

+ 49 - 12
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodePropertyViewModel.cs

@@ -12,24 +12,31 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
 {
 {
     private string propertyName;
     private string propertyName;
     private string displayName;
     private string displayName;
-    private object value;
+    private object? _value;
     private INodeHandler node;
     private INodeHandler node;
     private bool isInput;
     private bool isInput;
+    private bool isFunc;
     private IBrush socketBrush;
     private IBrush socketBrush;
     
     
     private ObservableCollection<INodePropertyHandler> connectedInputs = new();
     private ObservableCollection<INodePropertyHandler> connectedInputs = new();
     private INodePropertyHandler? connectedOutput;
     private INodePropertyHandler? connectedOutput;
-    
+
     public string DisplayName
     public string DisplayName
     {
     {
         get => displayName;
         get => displayName;
         set => SetProperty(ref displayName, value);
         set => SetProperty(ref displayName, value);
     }
     }
     
     
-    public object Value
+    public object? Value
     {
     {
-        get => value;
-        set => SetProperty(ref value, value);
+        get => _value;
+        set
+        {
+            if (SetProperty(ref _value, value))
+            {
+                ViewModelMain.Current.NodeGraphManager.UpdatePropertyValue((node, PropertyName, value));
+            }
+        }
     }
     }
     
     
     public bool IsInput
     public bool IsInput
@@ -38,6 +45,12 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
         set => SetProperty(ref isInput, value);
         set => SetProperty(ref isInput, value);
     }
     }
 
 
+    public bool IsFunc
+    {
+        get => isFunc;
+        set => SetProperty(ref isFunc, value);
+    }
+
     public INodePropertyHandler? ConnectedOutput
     public INodePropertyHandler? ConnectedOutput
     {
     {
         get => connectedOutput;
         get => connectedOutput;
@@ -74,7 +87,14 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
     {
     {
         Node = node;
         Node = node;
         PropertyType = propertyType;
         PropertyType = propertyType;
-        if (Application.Current.Styles.TryGetResource($"{PropertyType.Name}SocketBrush", App.Current.ActualThemeVariant, out object brush))
+        var targetType = propertyType;
+
+        if (propertyType.IsAssignableTo(typeof(Delegate)))
+        {
+            targetType = propertyType.GetMethod("Invoke").ReturnType;
+        }
+
+        if (Application.Current.Styles.TryGetResource($"{targetType.Name}SocketBrush", App.Current.ActualThemeVariant, out object brush))
         {
         {
             if (brush is IBrush brushValue)
             if (brush is IBrush brushValue)
             {
             {
@@ -96,27 +116,44 @@ internal abstract class NodePropertyViewModel : ViewModelBase, INodePropertyHand
 
 
     public static NodePropertyViewModel? CreateFromType(Type type, INodeHandler node)
     public static NodePropertyViewModel? CreateFromType(Type type, INodeHandler node)
     {
     {
-        string name = type.Name;
-        name += "PropertyViewModel";
+        Type propertyType = type;
+        
+        if (type.IsAssignableTo(typeof(Delegate)))
+        {
+            propertyType = type.GetMethod("Invoke").ReturnType;
+        }
+        
+        string name = $"{propertyType.Name}PropertyViewModel";
         
         
         Type viewModelType = Type.GetType($"PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties.{name}");
         Type viewModelType = Type.GetType($"PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties.{name}");
         if (viewModelType == null)
         if (viewModelType == null)
         {
         {
+            if (propertyType.IsEnum)
+            {
+                return new GenericEnumPropertyViewModel(node, type, propertyType);
+            }
+            
             return new GenericPropertyViewModel(node, type);
             return new GenericPropertyViewModel(node, type);
         }
         }
         
         
         return (NodePropertyViewModel)Activator.CreateInstance(viewModelType, node, type);
         return (NodePropertyViewModel)Activator.CreateInstance(viewModelType, node, type);
     }
     }
+
+    public void InternalSetValue(object? value) => SetProperty(ref _value, value);
 }
 }
 
 
 internal abstract class NodePropertyViewModel<T> : NodePropertyViewModel
 internal abstract class NodePropertyViewModel<T> : NodePropertyViewModel
 {
 {
-    private T nodeValue;
-    
     public new T Value
     public new T Value
     {
     {
-        get => nodeValue;
-        set => SetProperty(ref nodeValue, value);
+        get
+        {
+            if (base.Value == null)
+                return default;
+
+            return (T)base.Value;
+        }
+        set => base.Value = value;
     }
     }
     
     
     public NodePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
     public NodePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)

+ 2 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeViewModel.cs

@@ -35,6 +35,8 @@ internal class NodeViewModel : ObservableObject, INodeHandler
         set => SetProperty(ref nodeName, value);
         set => SetProperty(ref nodeName, value);
     }
     }
 
 
+    public string InternalName { get; init; }
+
     public VecD PositionBindable
     public VecD PositionBindable
     {
     {
         get => position;
         get => position;

+ 74 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/NodeZoneViewModel.cs

@@ -0,0 +1,74 @@
+using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes;
+
+public sealed class NodeZoneViewModel : NodeFrameViewModelBase
+{
+    private INodeHandler start;
+    private INodeHandler end;
+    
+    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id, [start, end])
+    {
+        InternalName = internalName;
+        
+        this.start = start;
+        this.end = end;
+        
+        CalculateBounds();
+    }
+
+    protected override void CalculateBounds()
+    {
+        if (Nodes.Count == 0)
+        {
+            if (TopLeft == BottomRight)
+            {
+                BottomRight = TopLeft + new VecD(100, 100);
+            }
+            
+            return;
+        }
+
+        var bounds = GetBounds();
+        
+        var minX = bounds.Min(n => n.X);
+        var minY = bounds.Min(n => n.Y);
+        
+        var maxX = bounds.Max(n => n.Right);
+        var maxY = bounds.Max(n => n.Bottom);
+
+        TopLeft = new VecD(minX, minY);
+        BottomRight = new VecD(maxX, maxY);
+
+        Size = BottomRight - TopLeft;
+    }
+
+    private List<RectD> GetBounds()
+    {
+        var list = new List<RectD>();
+
+        const int defaultXOffset = -30;
+        const int defaultYOffset = -45;
+        
+        // TODO: Use the actual node height
+        foreach (var node in Nodes)
+        {
+            if (node == start)
+            {
+                list.Add(new RectD(node.PositionBindable + new VecD(100, defaultYOffset), new VecD(100, 400)));
+                continue;
+            }
+
+            if (node == end)
+            {
+                list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(100, 400)));
+                continue;
+            }
+            
+            list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(200, 400)));
+        }
+        
+        return list;
+    }
+}

+ 8 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/BooleanPropertyViewModel.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class BooleanPropertyViewModel : NodePropertyViewModel<bool>
+{
+    public BooleanPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 10 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/ColorPropertyViewModel.cs

@@ -0,0 +1,10 @@
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class ColorPropertyViewModel : NodePropertyViewModel<Color>
+{
+    public ColorPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 8 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/DoublePropertyViewModel.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class DoublePropertyViewModel : NodePropertyViewModel<double>
+{
+    public DoublePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 13 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/GenericEnumPropertyViewModel.cs

@@ -0,0 +1,13 @@
+using PixiEditor.AvaloniaUI.Models.Handlers;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class GenericEnumPropertyViewModel : NodePropertyViewModel
+{
+    public GenericEnumPropertyViewModel(INodeHandler node, Type propertyType, Type enumType) : base(node, propertyType)
+    {
+        Values = Enum.GetValues(enumType);
+    }
+
+    public Array Values { get; }
+}

+ 35 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Nodes/Properties/VecIPropertyViewModel.cs

@@ -0,0 +1,35 @@
+using System.ComponentModel;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.ViewModels.Nodes.Properties;
+
+internal class VecIPropertyViewModel : NodePropertyViewModel<VecI>
+{
+    public VecIPropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+        PropertyChanged += OnPropertyChanged;
+    }
+
+    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+    {
+        if (e.PropertyName != nameof(Value))
+        {
+            return;
+        }
+        
+        OnPropertyChanged(nameof(XValue));
+        OnPropertyChanged(nameof(YValue));
+    }
+
+    public int XValue
+    {
+        get => Value.X;
+        set => Value = new VecI(value, Value.Y);
+    }
+    
+    public int YValue
+    {
+        get => Value.Y;
+        set => Value = new VecI(Value.X, value);
+    }
+}

+ 12 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/NodeGraphManagerViewModel.cs

@@ -10,6 +10,12 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     {
     {
     }
     }
 
 
+    [Command.Debug("PixiEditor.NodeGraph.CreateNodeFrameAroundEverything", "Create node frame", "Create node frame")]
+    public void CreateNodeFrameAroundEverything()
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.CreateNodeFrameAroundEverything();
+    }
+
     [Command.Internal("PixiEditor.NodeGraph.CreateNode")]
     [Command.Internal("PixiEditor.NodeGraph.CreateNode")]
     public void CreateNode(Type nodeType)
     public void CreateNode(Type nodeType)
     {
     {
@@ -27,6 +33,12 @@ internal class NodeGraphManagerViewModel : SubViewModel<ViewModelMain>
     {
     {
         Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.SetNodePosition(args.node, args.newPos);
         Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.SetNodePosition(args.node, args.newPos);
     }
     }
+
+    [Command.Internal("PixiEditor.NodeGraph.UpdateValue")]
+    public void UpdatePropertyValue((INodeHandler node, string property, object value) args)
+    {
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.NodeGraph.UpdatePropertyValue(args.node, args.property, args.value);
+    }
     
     
     [Command.Internal("PixiEditor.NodeGraph.EndChangeNodePos")]
     [Command.Internal("PixiEditor.NodeGraph.EndChangeNodePos")]
     public void EndChangeNodePos()
     public void EndChangeNodePos()

+ 33 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeFrameView.cs

@@ -0,0 +1,33 @@
+using Avalonia;
+using Avalonia.Controls.Primitives;
+using PixiEditor.Numerics;
+using Point = PixiEditor.Numerics.Point;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes;
+
+public class NodeFrameView : TemplatedControl
+{
+    public static readonly StyledProperty<VecD> TopLeftProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(TopLeft));
+    
+    public VecD TopLeft
+    {
+        get => GetValue(TopLeftProperty);
+        set => SetValue(TopLeftProperty, value);
+    }
+    
+    public static readonly StyledProperty<VecD> BottomRightProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(BottomRight));
+    
+    public VecD BottomRight
+    {
+        get => GetValue(BottomRightProperty);
+        set => SetValue(BottomRightProperty, value);
+    }
+    
+    public static readonly StyledProperty<VecD> SizeProperty = AvaloniaProperty.Register<ConnectionLine, VecD>(nameof(Size));
+    
+    public VecD Size
+    {
+        get => GetValue(SizeProperty);
+        set => SetValue(SizeProperty, value);
+    }
+}

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/BooleanPropertyView.axaml

@@ -0,0 +1,14 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.BooleanPropertyView">
+    <StackPanel Orientation="Horizontal" HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <CheckBox Margin="0,0,4,0" IsVisible="{Binding IsInput}" IsChecked="{Binding Value}"/>
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/BooleanPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class BooleanPropertyView : NodePropertyView
+{
+    public BooleanPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 16 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml

@@ -0,0 +1,16 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:colorPicker="clr-namespace:ColorPicker;assembly=ColorPicker.AvaloniaUI"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.ColorPropertyView">
+    <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <colorPicker:PortableColorPicker Width="40" Height="20" IsVisible="{Binding IsInput}" SelectedColor="{Binding Value, Mode=TwoWay}" />
+    </Grid>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/ColorPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class ColorPropertyView : NodePropertyView
+{
+    public ColorPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 16 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/DoublePropertyView.axaml

@@ -0,0 +1,16 @@
+<properties:NodePropertyView x:TypeArguments="system:Double" xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:system="clr-namespace:System;assembly=System.Runtime"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.DoublePropertyView">
+    <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <input:NumberInput HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding IsInput}" Value="{Binding Value, Mode=TwoWay}" />
+    </Grid>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/DoublePropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class DoublePropertyView : NodePropertyView
+{
+    public DoublePropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 16 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericEnumPropertyView.axaml

@@ -0,0 +1,16 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.GenericEnumPropertyView">
+    <Grid HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <ComboBox HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding IsInput}"
+                  SelectedValue="{Binding Value, Mode=TwoWay}" ItemsSource="{Binding Values}" />
+    </Grid>
+</properties:NodePropertyView>

+ 13 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs

@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class GenericEnumPropertyView : NodePropertyView
+{
+    public GenericEnumPropertyView()
+    {
+        InitializeComponent();
+    }
+}

+ 8 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/GenericPropertyView.axaml

@@ -2,7 +2,15 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
              xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
              xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.GenericPropertyView">
              x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.GenericPropertyView">
+    <Grid>
+        <TextBlock
+            VerticalAlignment="Center"
+            ui:Translator.Key="{Binding DisplayName}"
+            HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Left'}}"/>
+    </Grid>
 </properties:NodePropertyView>
 </properties:NodePropertyView>

+ 7 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs

@@ -11,6 +11,7 @@ namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 public class NodeSocket : TemplatedControl
 public class NodeSocket : TemplatedControl
 {
 {
     public static readonly StyledProperty<bool> IsInputProperty = AvaloniaProperty.Register<NodeSocket, bool>("IsInput");
     public static readonly StyledProperty<bool> IsInputProperty = AvaloniaProperty.Register<NodeSocket, bool>("IsInput");
+    public static readonly StyledProperty<bool> IsFuncProperty = AvaloniaProperty.Register<NodeSocket, bool>(nameof(IsFunc));
     public static readonly StyledProperty<string> LabelProperty = AvaloniaProperty.Register<NodeSocket, string>("Label");
     public static readonly StyledProperty<string> LabelProperty = AvaloniaProperty.Register<NodeSocket, string>("Label");
 
 
     public static readonly StyledProperty<IBrush> SocketBrushProperty = AvaloniaProperty.Register<NodeSocket, IBrush>(
     public static readonly StyledProperty<IBrush> SocketBrushProperty = AvaloniaProperty.Register<NodeSocket, IBrush>(
@@ -37,6 +38,12 @@ public class NodeSocket : TemplatedControl
         set { SetValue(IsInputProperty, value); }
         set { SetValue(IsInputProperty, value); }
     }
     }
 
 
+    public bool IsFunc
+    {
+        get => GetValue(IsFuncProperty);
+        set => SetValue(IsFuncProperty, value);
+    }
+
     public string Label
     public string Label
     {
     {
         get { return (string)GetValue(LabelProperty); }
         get { return (string)GetValue(LabelProperty); }

+ 18 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml

@@ -0,0 +1,18 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.AvaloniaUI.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:Class="PixiEditor.AvaloniaUI.Views.Nodes.Properties.VecIPropertyView">
+    <StackPanel HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <StackPanel IsVisible="{Binding IsInput}">
+            <input:NumberInput MinWidth="100" Value="{Binding XValue, Mode=TwoWay}" Margin="0,2" />
+            <input:NumberInput MinWidth="100" Value="{Binding YValue, Mode=TwoWay}" />
+        </StackPanel>
+    </StackPanel>
+</properties:NodePropertyView>

+ 14 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/VecIPropertyView.axaml.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.AvaloniaUI.Views.Nodes.Properties;
+
+public partial class VecIPropertyView : NodePropertyView
+{
+    public VecIPropertyView()
+    {
+        InitializeComponent();
+    }
+}
+

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNodeFrame_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record CreateNodeFrame_ChangeInfo(Guid Id, IEnumerable<Guid> NodeIds) : IChangeInfo;

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNodeZone_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record CreateNodeZone_ChangeInfo(Guid Id, string internalName, Guid StartId, Guid EndId) : IChangeInfo;

+ 8 - 4
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/CreateNode_ChangeInfo.cs

@@ -6,6 +6,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
 
 
 public record CreateNode_ChangeInfo(
 public record CreateNode_ChangeInfo(
+    string InternalName,
     string NodeName,
     string NodeName,
     VecD Position,
     VecD Position,
     Guid Id,
     Guid Id,
@@ -19,10 +20,13 @@ public record CreateNode_ChangeInfo(
             .ToImmutableArray();
             .ToImmutableArray();
     }
     }
 
 
-    public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node)
+    public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node) =>
+        CreateFromNode(node, node.GetType().Name.Replace("Node", ""));
+    
+    public static CreateNode_ChangeInfo CreateFromNode(IReadOnlyNode node, string name)
     {
     {
-        return new CreateNode_ChangeInfo(node.GetType().Name.Replace("Node", ""), node.Position, node.Id,
-            CreatePropertyInfos(node.InputProperties, true, node.Id),
-            CreatePropertyInfos(node.OutputProperties, false, node.Id));
+        return new CreateNode_ChangeInfo(node.InternalName, name, node.Position,
+            node.Id,
+            CreatePropertyInfos(node.InputProperties, true, node.Id), CreatePropertyInfos(node.OutputProperties, false, node.Id));
     }
     }
 }
 }

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/DeleteNodeFrame_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record DeleteNodeFrame_ChangeInfo(Guid Id) : IChangeInfo;

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/NodeGraph/PropertyValueUpdated_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+public record PropertyValueUpdated_ChangeInfo(Guid NodeId, string Property, object Value) : IChangeInfo;

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateFolder_ChangeInfo.cs

@@ -8,6 +8,7 @@ namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
 public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
 {
 {
     public CreateFolder_ChangeInfo(
     public CreateFolder_ChangeInfo(
+        string internalName,
         Guid parentGuid,
         Guid parentGuid,
         float opacity,
         float opacity,
         bool isVisible,
         bool isVisible,
@@ -19,7 +20,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
         bool maskIsVisible,
         bool maskIsVisible,
         ImmutableArray<NodePropertyInfo> Inputs,
         ImmutableArray<NodePropertyInfo> Inputs,
         ImmutableArray<NodePropertyInfo> Outputs
         ImmutableArray<NodePropertyInfo> Outputs
-    ) : base(parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+    ) : base(internalName, parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
         maskIsVisible, Inputs, Outputs)
         maskIsVisible, Inputs, Outputs)
     {
     {
     }
     }
@@ -27,6 +28,7 @@ public record class CreateFolder_ChangeInfo : CreateStructureMember_ChangeInfo
     internal static CreateFolder_ChangeInfo FromFolder(Guid parentGuid, FolderNode folder)
     internal static CreateFolder_ChangeInfo FromFolder(Guid parentGuid, FolderNode folder)
     {
     {
         return new CreateFolder_ChangeInfo(
         return new CreateFolder_ChangeInfo(
+            folder.InternalName,
             parentGuid,
             parentGuid,
             folder.Opacity.Value,
             folder.Opacity.Value,
             folder.IsVisible.Value,
             folder.IsVisible.Value,

+ 3 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateLayer_ChangeInfo.cs

@@ -10,6 +10,7 @@ namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
 {
 {
     public CreateLayer_ChangeInfo(
     public CreateLayer_ChangeInfo(
+        string internalName,
         Guid parentGuid,
         Guid parentGuid,
         float opacity,
         float opacity,
         bool isVisible,
         bool isVisible,
@@ -22,7 +23,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
         bool lockTransparency,
         bool lockTransparency,
         ImmutableArray<NodePropertyInfo> inputs,
         ImmutableArray<NodePropertyInfo> inputs,
         ImmutableArray<NodePropertyInfo> outputs) :
         ImmutableArray<NodePropertyInfo> outputs) :
-        base(parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
+        base(internalName, parentGuid, opacity, isVisible, clipToMemberBelow, name, blendMode, guidValue, hasMask,
             maskIsVisible, inputs, outputs)
             maskIsVisible, inputs, outputs)
     {
     {
         LockTransparency = lockTransparency;
         LockTransparency = lockTransparency;
@@ -33,6 +34,7 @@ public record class CreateLayer_ChangeInfo : CreateStructureMember_ChangeInfo
     internal static CreateLayer_ChangeInfo FromLayer(Guid parentGuid, LayerNode layer)
     internal static CreateLayer_ChangeInfo FromLayer(Guid parentGuid, LayerNode layer)
     {
     {
         return new CreateLayer_ChangeInfo(
         return new CreateLayer_ChangeInfo(
+            layer.InternalName,
             parentGuid,
             parentGuid,
             layer.Opacity.Value,
             layer.Opacity.Value,
             layer.IsVisible.Value,
             layer.IsVisible.Value,

+ 2 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Structure/CreateStructureMember_ChangeInfo.cs

@@ -7,6 +7,7 @@ using PixiEditor.Numerics;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 
 
 public abstract record class CreateStructureMember_ChangeInfo(
 public abstract record class CreateStructureMember_ChangeInfo(
+    string InternalName,
     Guid ParentGuid,
     Guid ParentGuid,
     float Opacity,
     float Opacity,
     bool IsVisible,
     bool IsVisible,
@@ -18,7 +19,7 @@ public abstract record class CreateStructureMember_ChangeInfo(
     bool MaskIsVisible,
     bool MaskIsVisible,
     ImmutableArray<NodePropertyInfo> InputProperties,
     ImmutableArray<NodePropertyInfo> InputProperties,
     ImmutableArray<NodePropertyInfo> OutputProperties
     ImmutableArray<NodePropertyInfo> OutputProperties
-) : CreateNode_ChangeInfo(Name, new VecD(0, 0), Id, InputProperties, OutputProperties)
+) : CreateNode_ChangeInfo(InternalName, Name, new VecD(0, 0), Id, InputProperties, OutputProperties)
 {
 {
     public ImmutableArray<NodePropertyInfo> InputProperties { get; init; } = InputProperties;
     public ImmutableArray<NodePropertyInfo> InputProperties { get; init; } = InputProperties;
     public ImmutableArray<NodePropertyInfo> OutputProperties { get; init; } = OutputProperties;
     public ImmutableArray<NodePropertyInfo> OutputProperties { get; init; } = OutputProperties;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs

@@ -5,8 +5,8 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Factories;
 
 
 public class ImageLayerNodeFactory : NodeFactory<ImageLayerNode>
 public class ImageLayerNodeFactory : NodeFactory<ImageLayerNode>
 {
 {
-    public override T CreateNode<T>(IReadOnlyDocument document)
+    public override ImageLayerNode CreateNode(IReadOnlyDocument document)
     {
     {
-        return (T)(object)new ImageLayerNode(document.Size);
+        return new ImageLayerNode(document.Size);
     }
     }
 }
 }

+ 22 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldInputProperty.cs

@@ -0,0 +1,22 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public class FieldInputProperty<T> : InputProperty<Func<FieldContext, T>>, IFieldInputProperty
+{
+    private T? constantNonOverrideValue;
+    
+    internal FieldInputProperty(Node node, string internalName, string displayName, T defaultValue) : base(node, internalName, displayName, null)
+    {
+        constantNonOverrideValue = defaultValue;
+        NonOverridenValue = _ => constantNonOverrideValue;
+    }
+
+    object? IFieldInputProperty.GetFieldConstantValue() => constantNonOverrideValue;
+
+    void IFieldInputProperty.SetFieldConstantValue(object? value)
+    {
+        constantNonOverrideValue = (T)value;
+    }
+}

+ 11 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/FieldOutputProperty.cs

@@ -0,0 +1,11 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+public class FieldOutputProperty<T> : OutputProperty<Func<FieldContext, T>>
+{
+    internal FieldOutputProperty(Node node, string internalName, string displayName, Func<FieldContext, T> defaultValue) : base(node, internalName, displayName, defaultValue)
+    {
+    }
+}

+ 8 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/IFieldInputProperty.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.ChangeableDocument.Changeables.Graph;
+
+internal interface IFieldInputProperty
+{
+    object? GetFieldConstantValue();
+    
+    void SetFieldConstantValue(object? value);
+}

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/FieldContext.cs

@@ -0,0 +1,5 @@
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+
+public record struct FieldContext(VecD Position, VecI Size);

+ 2 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/IReadOnlyNode.cs

@@ -11,6 +11,8 @@ public interface IReadOnlyNode
     public IReadOnlyCollection<IOutputProperty> OutputProperties { get; }
     public IReadOnlyCollection<IOutputProperty> OutputProperties { get; }
     public VecD Position { get; }
     public VecD Position { get; }
     public Image? CachedResult { get; }
     public Image? CachedResult { get; }
+    
+    public string InternalName { get; }
 
 
     public Image? Execute(KeyFrameTime frame);
     public Image? Execute(KeyFrameTime frame);
     public bool Validate();
     public bool Validate();

+ 8 - 15
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeFactory.cs

@@ -3,28 +3,21 @@ using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph;
 
 
-public abstract class NodeFactory
+public interface INodeFactory
 {
 {
     public Type NodeType { get; }
     public Type NodeType { get; }
 
 
-    public NodeFactory(Type nodeType)
-    {
-        NodeType = nodeType;
-    }
-
-    public abstract Node CreateNode(IReadOnlyDocument document);
+    public Node CreateNode(IReadOnlyDocument document);
 }
 }
 
 
-public abstract class NodeFactory<T> : NodeFactory where T : Node
+public abstract class NodeFactory<T> : INodeFactory where T : Node
 {
 {
-    public NodeFactory() : base(typeof(T))
-    {
-    }
-
-    public abstract T CreateNode<T>(IReadOnlyDocument document) where T : Node;
+    public Type NodeType { get; } = typeof(T);
+    
+    public abstract T CreateNode(IReadOnlyDocument document);
 
 
-    public override Node CreateNode(IReadOnlyDocument document)
+    Node INodeFactory.CreateNode(IReadOnlyDocument document)
     {
     {
-        return CreateNode<T>(document);
+        return CreateNode(document);
     }
     }
 }
 }

+ 48 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/CombineColorNode.cs

@@ -0,0 +1,48 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class CombineColorNode : Node
+{
+    public FieldOutputProperty<Color> Color { get; }
+    
+    public FieldInputProperty<double> R { get; }
+    
+    public FieldInputProperty<double> G { get; }
+    
+    public FieldInputProperty<double> B { get; }
+    
+    public FieldInputProperty<double> A { get; }
+
+    public CombineColorNode()
+    {
+        Color = CreateFieldOutput(nameof(Color), "COLOR", GetColor);
+        
+        R = CreateFieldInput("R", "R", 0d);
+        G = CreateFieldInput("G", "G", 0d);
+        B = CreateFieldInput("B", "B", 0d);
+        A = CreateFieldInput("A", "A", 0d);
+    }
+
+    private Color GetColor(FieldContext ctx)
+    {
+        var r = R.Value(ctx) * 255;
+        var g = G.Value(ctx) * 255;
+        var b = B.Value(ctx) * 255;
+        var a = A.Value(ctx) * 255;
+
+        return new Color((byte)r, (byte)g, (byte)b, (byte)a);
+    }
+    
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return null;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => new SeparateVecDNode();
+}

+ 41 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/EmptyImageNode.cs

@@ -0,0 +1,41 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class CreateImageNode : Node
+{
+    private Paint _paint = new();
+    
+    public OutputProperty<Image> Output { get; }
+
+    public InputProperty<VecI> Size { get; }
+    
+    public InputProperty<Color> Fill { get; }
+
+    public CreateImageNode()
+    {
+        Output = CreateOutput<Image>(nameof(Output), "EMPTY_IMAGE", null);
+        Size = CreateInput(nameof(Size), "SIZE", new VecI(32, 32));
+        Fill = CreateInput(nameof(Fill), "FILL", new Color(0, 0, 0, 255));
+    }
+    
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        using var surface = new Surface(Size.Value);
+
+        _paint.Color = Fill.Value;
+        surface.DrawingSurface.Canvas.DrawPaint(_paint);
+
+        Output.Value = surface.DrawingSurface.Snapshot();
+
+        return Output.Value;
+    }
+
+    public override bool Validate() => Size.Value is { X: > 0, Y: > 0 };
+
+    public override Node CreateCopy() => new CreateImageNode();
+}

+ 29 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSizeNode.cs

@@ -0,0 +1,29 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class ImageSizeNode : Node
+{
+    public InputProperty<Image?> Image { get; }
+    
+    public OutputProperty<VecI> Size { get; }
+    
+    public ImageSizeNode()
+    {
+        Image = CreateInput<Image>(nameof(Image), "IMAGE", null);
+        Size = CreateOutput(nameof(Size), "SIZE", new VecI());
+    }
+    
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        Size.Value = Image.Value?.Size ?? new VecI();
+
+        return null;
+    }
+
+    public override bool Validate() => Image.Value != null;
+
+    public override Node CreateCopy() => new ImageSizeNode();
+}

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageSpaceNode.cs

@@ -0,0 +1,28 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class ImageSpaceNode : Node
+{
+    public FieldOutputProperty<VecD> Position { get; }
+    
+    public FieldOutputProperty<VecI> Size { get; }
+
+    public ImageSpaceNode()
+    {
+        Position = CreateFieldOutput(nameof(Position), "PIXEL_COORDINATE", ctx => ctx.Position);
+        Size = CreateFieldOutput(nameof(Size), "SIZE", ctx => ctx.Size);
+    }
+    
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return null;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => new ImageSpaceNode();
+}

+ 59 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/MathNode.cs

@@ -0,0 +1,59 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class MathNode : Node
+{
+    public FieldOutputProperty<double> Result { get; }
+
+    public InputProperty<MathNodeMode> Mode { get; }
+    
+    public InputProperty<bool> Clamp { get; }
+
+    public FieldInputProperty<double> X { get; }
+    
+    public FieldInputProperty<double> Y { get; }
+    
+    public MathNode()
+    {
+        Result = CreateFieldOutput(nameof(Result), "RESULT", Calculate);
+        Mode = CreateInput(nameof(Mode), "MATH_MODE", MathNodeMode.Add);
+        Clamp = CreateInput(nameof(Clamp), "CLAMP", false);
+        X = CreateFieldInput(nameof(X), "X", 0d);
+        Y = CreateFieldInput(nameof(Y), "Y", 0d);
+    }
+
+    private double Calculate(FieldContext context)
+    {
+        var (x, y) = GetValues(context);
+
+        var result = Mode.Value switch
+        {
+            MathNodeMode.Add => x + y,
+            MathNodeMode.Subtract => x - y,
+            MathNodeMode.Multiply => x * y,
+            MathNodeMode.Divide => x / y
+        };
+
+        if (Clamp.Value)
+        {
+            result = Math.Clamp(result, 0, 1);
+        }
+        
+        return result;
+    }
+
+    private (double x, double y) GetValues(FieldContext context) => (X.Value(context), Y.Value(context));
+    
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return null;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => new MathNode();
+}

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageLeftNode.cs

@@ -0,0 +1,51 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class ModifyImageLeftNode : Node
+{
+    private Pixmap? pixmap;
+    
+    public InputProperty<Image?> Image { get; }
+    
+    public FieldOutputProperty<VecD> Coordinate { get; }
+    
+    public FieldOutputProperty<Color> Color { get; }
+    
+    public ModifyImageLeftNode()
+    {
+        Image = CreateInput<Image>(nameof(Image), "IMAGE", null);
+        Coordinate = CreateFieldOutput(nameof(Coordinate), "COORDINATE", ctx => ctx.Position);
+        Color = CreateFieldOutput(nameof(Color), "COLOR", GetColor);
+    }
+
+    private Color GetColor(FieldContext context)
+    {
+        if (pixmap == null)
+            return new Color();
+        
+        var x = context.Position.X * context.Size.X;
+        var y = context.Position.Y * context.Size.Y;
+        
+        return pixmap.GetPixelColor((int)x, (int)y);
+    }
+
+    internal void PreparePixmap()
+    {
+        pixmap = Image.Value?.PeekPixels();
+    }
+
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return Image.Value;
+    }
+
+    public override bool Validate() => Image.Value != null;
+
+    public override Node CreateCopy() => new ModifyImageLeftNode();
+}

+ 62 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ModifyImageRightNode.cs

@@ -0,0 +1,62 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class ModifyImageRightNode : Node
+{
+    private ModifyImageLeftNode startNode;
+    
+    private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
+    
+    public FieldInputProperty<Color> Color { get; }
+    
+    public OutputProperty<Image> Output { get; }
+    
+    public ModifyImageRightNode(ModifyImageLeftNode startNode)
+    {
+        this.startNode = startNode;
+        Color = CreateFieldInput(nameof(Color), "COLOR", new Color());
+        Output = CreateOutput<Image>(nameof(Output), "OUTPUT", null);
+    }
+
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        if (startNode.Image.Value is not { Size: var size })
+        {
+            return null;
+        }
+        
+        startNode.PreparePixmap();
+        
+        var width = size.X;
+        var height = size.Y;
+
+        using var surface = new Surface(size);
+
+        for (int y = 0; y < height; y++)
+        {
+            for (int x = 0; x < width; x++)
+            {
+                var context = new FieldContext(new VecD((double)x / width, (double)y / height), new VecI(width, height));
+                var color = Color.Value(context);
+
+                drawingPaint.Color = color;
+                surface.DrawingSurface.Canvas.DrawPixel(x, y, drawingPaint);
+            }
+        }
+
+        Output.Value = surface.DrawingSurface.Snapshot();
+
+        return Output.Value;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => throw new NotImplementedException();
+}

+ 29 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Node.cs

@@ -21,6 +21,13 @@ public abstract class Node : IReadOnlyNode, IDisposable
     public IReadOnlyCollection<IReadOnlyNode> ConnectedOutputNodes => _connectedNodes;
     public IReadOnlyCollection<IReadOnlyNode> ConnectedOutputNodes => _connectedNodes;
     public Image? CachedResult { get; private set; }
     public Image? CachedResult { get; private set; }
 
 
+    public virtual string InternalName { get; }
+
+    protected Node()
+    {
+        InternalName = $"PixiEditor.{GetType().Name}";
+    }
+    
     IReadOnlyCollection<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyCollection<IInputProperty> IReadOnlyNode.InputProperties => inputs;
     IReadOnlyCollection<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     IReadOnlyCollection<IOutputProperty> IReadOnlyNode.OutputProperties => outputs;
     public VecD Position { get; set; }
     public VecD Position { get; set; }
@@ -129,6 +136,18 @@ public abstract class Node : IReadOnlyNode, IDisposable
         }
         }
     }
     }
 
 
+    protected FieldInputProperty<T> CreateFieldInput<T>(string propName, string displayName, T defaultValue)
+    {
+        var property = new FieldInputProperty<T>(this, propName, displayName, defaultValue);
+        if (InputProperties.Any(x => x.InternalPropertyName == propName))
+        {
+            throw new InvalidOperationException($"Input with name {propName} already exists.");
+        }
+
+        inputs.Add(property);
+        return property;
+    }
+
     protected InputProperty<T> CreateInput<T>(string propName, string displayName, T defaultValue)
     protected InputProperty<T> CreateInput<T>(string propName, string displayName, T defaultValue)
     {
     {
         var property = new InputProperty<T>(this, propName, displayName, defaultValue);
         var property = new InputProperty<T>(this, propName, displayName, defaultValue);
@@ -141,6 +160,16 @@ public abstract class Node : IReadOnlyNode, IDisposable
         return property;
         return property;
     }
     }
 
 
+    protected FieldOutputProperty<T> CreateFieldOutput<T>(string propName, string displayName,
+        Func<FieldContext, T> defaultFunc)
+    {
+        var property = new FieldOutputProperty<T>(this, propName, displayName, defaultFunc);
+        outputs.Add(property);
+        property.Connected += (input, _) => _connectedNodes.Add(input.Node);
+        property.Disconnected += (input, _) => _connectedNodes.Remove(input.Node);
+        return property;
+    }
+
     protected OutputProperty<T> CreateOutput<T>(string propName, string displayName, T defaultValue)
     protected OutputProperty<T> CreateOutput<T>(string propName, string displayName, T defaultValue)
     {
     {
         var property = new OutputProperty<T>(this, propName, displayName, defaultValue);
         var property = new OutputProperty<T>(this, propName, displayName, defaultValue);

+ 37 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateColorNode.cs

@@ -0,0 +1,37 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class SeparateColorNode : Node
+{
+    public FieldInputProperty<Color> Color { get; }
+    
+    public FieldOutputProperty<double> R { get; }
+    
+    public FieldOutputProperty<double> G { get; }
+    
+    public FieldOutputProperty<double> B { get; }
+    
+    public FieldOutputProperty<double> A { get; }
+
+    public SeparateColorNode()
+    {
+        Color = CreateFieldInput(nameof(Color), "COLOR", new Color());
+        R = CreateFieldOutput(nameof(R), "R", ctx => Color.Value(ctx).R / 255d);
+        G = CreateFieldOutput(nameof(G), "G", ctx => Color.Value(ctx).G / 255d);
+        B = CreateFieldOutput(nameof(B), "B", ctx => Color.Value(ctx).B / 255d);
+        A = CreateFieldOutput(nameof(A), "A", ctx => Color.Value(ctx).A / 255d);
+    }
+
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return null;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => new SeparateColorNode();
+}

+ 31 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SeparateVecDNode.cs

@@ -0,0 +1,31 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+public class SeparateVecDNode : Node
+{
+    public FieldInputProperty<VecD> Vector { get; }
+    
+    public FieldOutputProperty<double> X { get; }
+    
+    public FieldOutputProperty<double> Y { get; }
+
+    public SeparateVecDNode()
+    {
+        X = CreateFieldOutput("X", "X", ctx => Vector.Value(ctx).X);
+        Y = CreateFieldOutput("Y", "Y", ctx => Vector.Value(ctx).Y);
+        Vector = CreateFieldInput("Vector", "VECTOR", new VecD(0, 0));
+    }
+
+    protected override Image? OnExecute(KeyFrameTime frameTime)
+    {
+        return null;
+    }
+
+    public override bool Validate() => true;
+
+    public override Node CreateCopy() => new SeparateVecDNode();
+}

+ 61 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateModifyImageNodePair_Change.cs

@@ -0,0 +1,61 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class CreateModifyImageNodePair_Change : Change
+{
+    private Guid startId;
+    private Guid endId;
+    private Guid zoneId;
+    
+    [GenerateMakeChangeAction]
+    public CreateModifyImageNodePair_Change(Guid startId, Guid endId, Guid zoneId)
+    {
+        this.startId = startId;
+        this.endId = endId;
+        this.zoneId = zoneId;
+    }
+
+    public override bool InitializeAndValidate(Document target) => true;
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var start = new ModifyImageLeftNode();
+        var end = new ModifyImageRightNode(start);
+
+        start.Id = startId;
+        end.Id = endId;
+        end.Position = new VecD(100, 0);
+        
+        target.NodeGraph.AddNode(start);
+        target.NodeGraph.AddNode(end);
+        
+        ignoreInUndo = false;
+
+        return new List<IChangeInfo>
+        {
+            CreateNode_ChangeInfo.CreateFromNode(start, "Modify Image Start"),
+            CreateNode_ChangeInfo.CreateFromNode(end, "Modify Image End"),
+            new CreateNodeZone_ChangeInfo(zoneId, "PixiEditor.ModifyImageZone", startId, endId)
+        };
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var startChange = RemoveNode(target, startId);
+        var endChange = RemoveNode(target, endId);
+        var zoneChange = new DeleteNodeFrame_ChangeInfo(zoneId);
+
+        return new List<IChangeInfo> { startChange, endChange, zoneChange };
+    }
+
+    private static DeleteNode_ChangeInfo RemoveNode(Document target, Guid id)
+    {
+        Node node = target.FindNodeOrThrow<Node>(id);
+        target.NodeGraph.RemoveNode(node);
+
+        return new DeleteNode_ChangeInfo(id);
+    }
+}

+ 32 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNodeFrame_Change.cs

@@ -0,0 +1,32 @@
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class CreateNodeFrame_Change : Change
+{
+    private Guid id;
+    private IEnumerable<Guid> nodeIds;
+    
+    [GenerateMakeChangeAction]
+    public CreateNodeFrame_Change(Guid id, IEnumerable<Guid> nodeIds)
+    {
+        this.id = id;
+        this.nodeIds = nodeIds;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        ignoreInUndo = false;
+        return new CreateNodeFrame_ChangeInfo(id, nodeIds);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return new DeleteNodeFrame_ChangeInfo(id);
+    }
+}

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/CreateNode_Change.cs

@@ -13,7 +13,7 @@ internal class CreateNode_Change : Change
 {
 {
     private Type nodeType;
     private Type nodeType;
     private Guid id;
     private Guid id;
-    private static Dictionary<Type, NodeFactory> allFactories;
+    private static Dictionary<Type, INodeFactory> allFactories;
     
     
     [GenerateMakeChangeAction]
     [GenerateMakeChangeAction]
     public CreateNode_Change(Type nodeType, Guid id)
     public CreateNode_Change(Type nodeType, Guid id)
@@ -23,11 +23,11 @@ internal class CreateNode_Change : Change
 
 
         if (allFactories == null)
         if (allFactories == null)
         {
         {
-            allFactories = new Dictionary<Type, NodeFactory>();
-            var factoryTypes = Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsSubclassOf(typeof(NodeFactory)) && !x.IsAbstract && !x.IsInterface).ToImmutableArray();
+            allFactories = new Dictionary<Type, INodeFactory>();
+            var factoryTypes = Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsSubclassOf(typeof(INodeFactory)) && !x.IsAbstract && !x.IsInterface).ToImmutableArray();
             foreach (var factoryType in factoryTypes)
             foreach (var factoryType in factoryTypes)
             {
             {
-                NodeFactory factory = (NodeFactory)Activator.CreateInstance(factoryType);
+                INodeFactory factory = (INodeFactory)Activator.CreateInstance(factoryType);
                 allFactories.Add(factory.NodeType, factory);
                 allFactories.Add(factory.NodeType, factory);
             }
             }
         }
         }
@@ -44,7 +44,7 @@ internal class CreateNode_Change : Change
             id = Guid.NewGuid();
             id = Guid.NewGuid();
 
 
         Node node = null;
         Node node = null;
-        if (allFactories.TryGetValue(nodeType, out NodeFactory factory))
+        if (allFactories.TryGetValue(nodeType, out INodeFactory factory))
         {
         {
             node = factory.CreateNode(target);
             node = factory.CreateNode(target);
         }
         }

+ 67 - 0
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/UpdateProperty_Change.cs

@@ -0,0 +1,67 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph;
+using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
+
+namespace PixiEditor.ChangeableDocument.Changes.NodeGraph;
+
+internal class UpdatePropertyValue_Change : Change
+{
+    private readonly Guid _nodeId;
+    private readonly string _propertyName;
+    private readonly object? _value;
+    private object? previousValue;
+    
+    [GenerateMakeChangeAction]
+    public UpdatePropertyValue_Change(Guid nodeId, string property, object? value)
+    {
+        _nodeId = nodeId;
+        _propertyName = property;
+        _value = value;
+    }
+    
+    public override bool InitializeAndValidate(Document target) => true;
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
+        var property = node.GetInputProperty(_propertyName);
+
+        previousValue = GetValue(property);
+        SetValue(property, _value);
+
+        ignoreInUndo = false;
+        
+        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, _value);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var node = target.NodeGraph.Nodes.First(x => x.Id == _nodeId);
+        var property = node.GetInputProperty(_propertyName);
+        SetValue(property, previousValue);
+        
+        return new PropertyValueUpdated_ChangeInfo(_nodeId, _propertyName, previousValue);
+    }
+
+    private static void SetValue(InputProperty property, object? value)
+    {
+        if (property is IFieldInputProperty fieldInput)
+        {
+            fieldInput.SetFieldConstantValue(value);
+        }
+        else
+        {
+            property.NonOverridenValue = value;
+        }
+    }
+    
+
+    private static object? GetValue(InputProperty property)
+    {
+        if (property is IFieldInputProperty fieldInput)
+        {
+            return fieldInput.GetFieldConstantValue();
+        }
+
+        return property.NonOverridenValue;
+    }
+}

+ 9 - 0
src/PixiEditor.ChangeableDocument/Enums/MathNodeMode.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+
+public enum MathNodeMode
+{
+    Add,
+    Subtract,
+    Multiply,
+    Divide
+}

+ 4 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IPixmapImplementation.cs

@@ -1,6 +1,8 @@
 using System;
 using System;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
 
 
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 
 
@@ -8,6 +10,8 @@ public interface IPixmapImplementation
 {
 {
     public void Dispose(IntPtr objectPointer);
     public void Dispose(IntPtr objectPointer);
 
 
+    public Color GetPixelColor(IntPtr objectPointer, VecI position);
+    
     public IntPtr GetPixels(IntPtr objectPointer);
     public IntPtr GetPixels(IntPtr objectPointer);
 
 
     public Span<T> GetPixelSpan<T>(Pixmap pixmap)
     public Span<T> GetPixelSpan<T>(Pixmap pixmap)

+ 2 - 1
src/PixiEditor.DrawingApi.Core/Bridge/Operations/IImageImplementation.cs

@@ -1,4 +1,5 @@
 using System;
 using System;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
@@ -21,6 +22,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public int GetHeight(IntPtr objectPointer);
         public int GetHeight(IntPtr objectPointer);
         public object GetNativeImage(IntPtr objectPointer);
         public object GetNativeImage(IntPtr objectPointer);
         public Image Clone(Image image);
         public Image Clone(Image image);
-        public Pixmap PeekPixels(Image image);
+        public Pixmap PeekPixels(IntPtr objectPointer);
     }
     }
 }
 }

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Surface/ImageData/Image.cs

@@ -61,6 +61,11 @@ namespace PixiEditor.DrawingApi.Core.Surface.ImageData
             return DrawingBackendApi.Current.ImageImplementation.Encode(this, format, quality);
             return DrawingBackendApi.Current.ImageImplementation.Encode(this, format, quality);
         }
         }
 
 
+        public Pixmap PeekPixels()
+        {
+            return DrawingBackendApi.Current.ImageImplementation.PeekPixels(ObjectPointer);
+        }
+
         public object Clone()
         public object Clone()
         {
         {
             return DrawingBackendApi.Current.ImageImplementation.Clone(this);
             return DrawingBackendApi.Current.ImageImplementation.Clone(this);

+ 9 - 0
src/PixiEditor.DrawingApi.Core/Surface/Pixmap.cs

@@ -1,6 +1,8 @@
 using System;
 using System;
 using PixiEditor.DrawingApi.Core.Bridge;
 using PixiEditor.DrawingApi.Core.Bridge;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
 
 
 namespace PixiEditor.DrawingApi.Core.Surface;
 namespace PixiEditor.DrawingApi.Core.Surface;
 
 
@@ -39,6 +41,13 @@ public class Pixmap : NativeObject
         DrawingBackendApi.Current.PixmapImplementation.Dispose(ObjectPointer);
         DrawingBackendApi.Current.PixmapImplementation.Dispose(ObjectPointer);
     }
     }
 
 
+    public Color GetPixelColor(int x, int y) => GetPixelColor(new VecI(x, y));
+    
+    public Color GetPixelColor(VecI position)
+    {
+        return DrawingBackendApi.Current.PixmapImplementation.GetPixelColor(ObjectPointer, position);
+    }
+
     public IntPtr GetPixels()
     public IntPtr GetPixels()
     {
     {
         return DrawingBackendApi.Current.PixmapImplementation.GetPixels(ObjectPointer);
         return DrawingBackendApi.Current.PixmapImplementation.GetPixels(ObjectPointer);

+ 9 - 3
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs

@@ -12,13 +12,12 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
     public class SkiaImageImplementation : SkObjectImplementation<SKImage>, IImageImplementation
     public class SkiaImageImplementation : SkObjectImplementation<SKImage>, IImageImplementation
     {
     {
         private readonly SkObjectImplementation<SKData> _imgImplementation;
         private readonly SkObjectImplementation<SKData> _imgImplementation;
+        private readonly SkiaPixmapImplementation _pixmapImplementation;
         private SkObjectImplementation<SKSurface>? _surfaceImplementation;
         private SkObjectImplementation<SKSurface>? _surfaceImplementation;
         private SkiaPixmapImplementation _pixmapImplementation;
         private SkiaPixmapImplementation _pixmapImplementation;
         private SkiaColorSpaceImplementation colorSpaceImpl;
         private SkiaColorSpaceImplementation colorSpaceImpl;
         
         
-        public SkiaImageImplementation(
-            SkObjectImplementation<SKData> imgDataImplementation, 
-            SkiaPixmapImplementation pixmapImplementation)
+        public SkiaImageImplementation(SkObjectImplementation<SKData> imgDataImplementation, SkiaPixmapImplementation pixmapImplementation)
         {
         {
             _imgImplementation = imgDataImplementation;
             _imgImplementation = imgDataImplementation;
             _pixmapImplementation = pixmapImplementation;
             _pixmapImplementation = pixmapImplementation;
@@ -132,6 +131,13 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new Image(clone.Handle);
             return new Image(clone.Handle);
         }
         }
 
 
+        public Pixmap PeekPixels(IntPtr objectPointer)
+        {
+            var nativePixmap = ManagedInstances[objectPointer].PeekPixels();
+
+            return _pixmapImplementation.CreateFrom(nativePixmap);
+        }
+
         public object GetNativeImage(IntPtr objectPointer)
         public object GetNativeImage(IntPtr objectPointer)
         {
         {
             return ManagedInstances[objectPointer];
             return ManagedInstances[objectPointer];

+ 7 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaPixmapImplementation.cs

@@ -1,7 +1,9 @@
 using System;
 using System;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Numerics;
 using SkiaSharp;
 using SkiaSharp;
 
 
 namespace PixiEditor.DrawingApi.Skia.Implementations
 namespace PixiEditor.DrawingApi.Skia.Implementations
@@ -21,6 +23,11 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances.TryRemove(objectPointer, out _);
             ManagedInstances.TryRemove(objectPointer, out _);
         }
         }
 
 
+        public Color GetPixelColor(IntPtr objectPointer, VecI position)
+        {
+            return ManagedInstances[objectPointer].GetPixelColor(position.X, position.Y).ToBackendColor();
+        }
+
         public IntPtr GetPixels(IntPtr objectPointer)
         public IntPtr GetPixels(IntPtr objectPointer)
         {
         {
             return ManagedInstances[objectPointer].GetPixels();
             return ManagedInstances[objectPointer].GetPixels();

+ 9 - 9
src/PixiEditor.DrawingApi.Skia/SkiaDrawingBackend.cs

@@ -28,15 +28,6 @@ namespace PixiEditor.DrawingApi.Skia
             SkiaImgDataImplementation dataImpl = new SkiaImgDataImplementation();
             SkiaImgDataImplementation dataImpl = new SkiaImgDataImplementation();
             ImgDataImplementation = dataImpl;
             ImgDataImplementation = dataImpl;
             
             
-            SkiaColorSpaceImplementation colorSpaceImpl = new SkiaColorSpaceImplementation();
-            ColorSpaceImplementation = colorSpaceImpl;
-
-            SkiaPixmapImplementation pixmapImpl = new SkiaPixmapImplementation(colorSpaceImpl);
-            PixmapImplementation = pixmapImpl;
-            
-            SkiaImageImplementation imgImpl = new SkiaImageImplementation(dataImpl, pixmapImpl);
-            ImageImplementation = imgImpl;
-            
             SkiaColorFilterImplementation colorFilterImpl = new SkiaColorFilterImplementation();
             SkiaColorFilterImplementation colorFilterImpl = new SkiaColorFilterImplementation();
             ColorFilterImplementation = colorFilterImpl;
             ColorFilterImplementation = colorFilterImpl;
             
             
@@ -51,6 +42,15 @@ namespace PixiEditor.DrawingApi.Skia
             
             
             MatrixImplementation = new SkiaMatrixImplementation();
             MatrixImplementation = new SkiaMatrixImplementation();
             
             
+            SkiaColorSpaceImplementation colorSpaceImpl = new SkiaColorSpaceImplementation();
+            ColorSpaceImplementation = colorSpaceImpl;
+
+            SkiaPixmapImplementation pixmapImpl = new SkiaPixmapImplementation(colorSpaceImpl);
+            PixmapImplementation = pixmapImpl;
+            
+            SkiaImageImplementation imgImpl = new SkiaImageImplementation(dataImpl, pixmapImpl);
+            ImageImplementation = imgImpl;
+
             SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl);
             SkiaBitmapImplementation bitmapImpl = new SkiaBitmapImplementation(imgImpl);
             BitmapImplementation = bitmapImpl;
             BitmapImplementation = bitmapImpl;
             
             

+ 23 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -42,6 +42,17 @@
             <Color x:Key="ImageSocketColor">#99c47a</Color>
             <Color x:Key="ImageSocketColor">#99c47a</Color>
             <Color x:Key="BoolSocketColor">#68abdf</Color>
             <Color x:Key="BoolSocketColor">#68abdf</Color>
             <Color x:Key="FloatSocketColor">#ffc66d</Color>
             <Color x:Key="FloatSocketColor">#ffc66d</Color>
+            <!-- TODO: How do we wanna handle floats and doubles? -->
+            <Color x:Key="DoubleSocketColor">#efb66d</Color>
+            <Color x:Key="ColorSocketColor">#99e4aa</Color>
+            <Color x:Key="VecDSocketColor">#c984ca</Color>
+            <Color x:Key="VecISocketColor">#c9b4ca</Color>
+            
+            <Color x:Key="PixiEditorModifyImageNodeBorderColor">#68abdf</Color>
+            <Color x:Key="PixiEditorModifyImageNodeBackgroundColor">#4068abdf</Color>
+
+            <Color x:Key="NodeFrameBorderColor">#101010</Color>
+            <Color x:Key="NodeFrameBackgroundColor">#40101010</Color>
 
 
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
             <system:Double x:Key="ThemeDisabledOpacity">0.4</system:Double>
 
 
@@ -81,6 +92,18 @@
             <SolidColorBrush x:Key="ImageSocketBrush" Color="{StaticResource ImageSocketColor}"/>
             <SolidColorBrush x:Key="ImageSocketBrush" Color="{StaticResource ImageSocketColor}"/>
             <SolidColorBrush x:Key="BooleanSocketBrush" Color="{StaticResource BoolSocketColor}"/>
             <SolidColorBrush x:Key="BooleanSocketBrush" Color="{StaticResource BoolSocketColor}"/>
             <SolidColorBrush x:Key="SingleSocketBrush" Color="{StaticResource FloatSocketColor}"/>
             <SolidColorBrush x:Key="SingleSocketBrush" Color="{StaticResource FloatSocketColor}"/>
+            <SolidColorBrush x:Key="DoubleSocketBrush" Color="{StaticResource DoubleSocketColor}"/>
+            <SolidColorBrush x:Key="ColorSocketBrush" Color="{StaticResource ColorSocketColor}"/>
+            <SolidColorBrush x:Key="VecDSocketBrush" Color="{StaticResource VecDSocketColor}"/>
+            <SolidColorBrush x:Key="VecISocketBrush" Color="{StaticResource VecISocketColor}"/>
+            
+            <SolidColorBrush x:Key="PixiEditorModifyImageLeftNodeBorderBrush" Color="{StaticResource PixiEditorModifyImageNodeBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorModifyImageRightNodeBorderBrush" Color="{StaticResource PixiEditorModifyImageNodeBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBorderBrush" Color="{StaticResource PixiEditorModifyImageNodeBorderColor}"/>
+            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBackgroundBrush" Color="{StaticResource PixiEditorModifyImageNodeBackgroundColor}"/>
+            
+            <SolidColorBrush x:Key="NodeFrameBorderBrush" Color="{StaticResource NodeFrameBorderColor}"/>
+            <SolidColorBrush x:Key="NodeFrameBackgroundBrush" Color="{StaticResource NodeFrameBackgroundColor}"/>
 
 
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>