瀏覽代碼

Merge branch 'master' into development

Krzysztof Krysiński 1 周之前
父節點
當前提交
16209b9a08
共有 38 個文件被更改,包括 753 次插入298 次删除
  1. 0 81
      src/Custom.ruleset
  2. 2 8
      src/Directory.Build.props
  3. 2 0
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs
  4. 0 1
      src/PixiEditor.sln
  5. 14 1
      src/PixiEditor/Data/Localization/Languages/en.json
  6. 2 1
      src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs
  7. 3 2
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  8. 16 15
      src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs
  9. 6 0
      src/PixiEditor/Models/EnumTranslations.cs
  10. 11 7
      src/PixiEditor/Models/Handlers/INodeHandler.cs
  11. 1 0
      src/PixiEditor/Models/Handlers/IToolsHandler.cs
  12. 4 3
      src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs
  13. 4 4
      src/PixiEditor/Models/Rendering/PreviewPainter.cs
  14. 144 0
      src/PixiEditor/Models/Structures/ObservableHashSet.cs
  15. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  16. 4 7
      src/PixiEditor/Styles/Templates/NodeFrameView.axaml
  17. 4 10
      src/PixiEditor/Styles/Templates/NodeGraphView.axaml
  18. 2 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  19. 81 0
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  20. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs
  21. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ModifyImageRightNodeViewModel.cs
  22. 2 2
      src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs
  23. 2 1
      src/PixiEditor/ViewModels/Document/StructureTree.cs
  24. 6 0
      src/PixiEditor/ViewModels/Nodes/IPairNodeEndViewModel.cs
  25. 6 0
      src/PixiEditor/ViewModels/Nodes/IPairNodeStartViewModel.cs
  26. 2 28
      src/PixiEditor/ViewModels/Nodes/NodeFrameViewModel.cs
  27. 23 21
      src/PixiEditor/ViewModels/Nodes/NodeFrameViewModelBase.cs
  28. 99 24
      src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs
  29. 222 35
      src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs
  30. 19 0
      src/PixiEditor/ViewModels/Nodes/Traverse.cs
  31. 4 0
      src/PixiEditor/ViewModels/PixiObservableObject.cs
  32. 17 0
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  33. 9 0
      src/PixiEditor/ViewModels/UserPreferences/Settings/SceneSettings.cs
  34. 16 1
      src/PixiEditor/ViewModels/UserPreferences/SettingsGroup.cs
  35. 9 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  36. 6 21
      src/PixiEditor/Views/Nodes/NodeFrameView.cs
  37. 7 0
      src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml
  38. 0 20
      src/stylecop.json

+ 0 - 81
src/Custom.ruleset

@@ -13,85 +13,4 @@
     <Rule Id="CA1303" Action="None" />
     <Rule Id="CA1303" Action="None" />
     <Rule Id="CA1416" Action="None" />
     <Rule Id="CA1416" Action="None" />
   </Rules>
   </Rules>
-  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
-    <Rule Id="SA0001" Action="None" />
-    <Rule Id="SA1000" Action="None" />
-    <Rule Id="SA1005" Action="None" />
-    <Rule Id="SA1008" Action="None" />
-    <Rule Id="SA1009" Action="None" />
-    <Rule Id="SA1011" Action="None" />
-    <Rule Id="SA1023" Action="None" />
-    <Rule Id="SA1028" Action="None" />
-    <Rule Id="SA1101" Action="None" />
-    <Rule Id="SA1110" Action="None" />
-    <Rule Id="SA1111" Action="None" />
-    <Rule Id="SA1112" Action="None" />
-    <Rule Id="SA1117" Action="None" />
-    <Rule Id="SA1119" Action="None" />
-    <Rule Id="SA1121" Action="None" />
-    <Rule Id="SA1122" Action="None" />
-    <Rule Id="SA1124" Action="None" />
-    <Rule Id="SA1127" Action="None" />
-    <Rule Id="SA1128" Action="None" />
-    <Rule Id="SA1129" Action="None" />
-    <Rule Id="SA1130" Action="None" />
-    <Rule Id="SA1132" Action="None" />
-    <Rule Id="SA1135" Action="None" />
-    <Rule Id="SA1136" Action="None" />
-    <Rule Id="SA1139" Action="None" />
-    <Rule Id="SA1200" Action="None" />
-    <Rule Id="SA1201" Action="None" />
-    <Rule Id="SA1202" Action="None" />
-    <Rule Id="SA1204" Action="None" />
-    <Rule Id="SA1207" Action="None" />
-    <Rule Id="SA1208" Action="None" />
-    <Rule Id="SA1209" Action="None" />
-    <Rule Id="SA1210" Action="None" />
-    <Rule Id="SA1211" Action="None" />
-    <Rule Id="SA1214" Action="None" />
-    <Rule Id="SA1216" Action="None" />
-    <Rule Id="SA1217" Action="None" />
-    <Rule Id="SA1303" Action="None" />
-    <Rule Id="SA1304" Action="None" />
-    <Rule Id="SA1307" Action="None" />
-    <Rule Id="SA1309" Action="None" />
-    <Rule Id="SA1310" Action="None" />
-    <Rule Id="SA1311" Action="None" />
-    <Rule Id="SA1313" Action="None" />
-    <Rule Id="SA1316" Action="None" />
-    <Rule Id="SA1400" Action="None" />
-    <Rule Id="SA1401" Action="None" />
-    <Rule Id="SA1402" Action="None" />
-    <Rule Id="SA1405" Action="None" />
-    <Rule Id="SA1406" Action="None" />
-    <Rule Id="SA1407" Action="None" />
-    <Rule Id="SA1408" Action="None" />
-    <Rule Id="SA1410" Action="None" />
-    <Rule Id="SA1411" Action="None" />
-    <Rule Id="SA1413" Action="None" />
-    <Rule Id="SA1501" Action="None" />
-    <Rule Id="SA1502" Action="None" />
-    <Rule Id="SA1503" Action="None" />
-    <Rule Id="SA1505" Action="None" />
-    <Rule Id="SA1507" Action="None" />
-    <Rule Id="SA1508" Action="None" />
-    <Rule Id="SA1512" Action="None" />
-    <Rule Id="SA1513" Action="None" />
-    <Rule Id="SA1515" Action="None" />
-    <Rule Id="SA1516" Action="None" />
-    <Rule Id="SA1518" Action="None" />
-    <Rule Id="SA1600" Action="None" />
-    <Rule Id="SA1601" Action="None" />
-    <Rule Id="SA1602" Action="None" />
-    <Rule Id="SA1604" Action="None" />
-    <Rule Id="SA1605" Action="None" />
-    <Rule Id="SA1606" Action="None" />
-    <Rule Id="SA1607" Action="None" />
-    <Rule Id="SA1623" Action="None" />
-    <Rule Id="SA1629" Action="None" />
-    <Rule Id="SA1633" Action="None" />
-    <Rule Id="SA1642" Action="None" />
-    <Rule Id="SA1643" Action="None" />
-    <Rule Id="SA1648" Action="None" />
-  </Rules>
 </RuleSet>
 </RuleSet>

+ 2 - 8
src/Directory.Build.props

@@ -1,15 +1,9 @@
 <Project>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
-        <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
+        <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
 		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
 		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
-    <ItemGroup>
-        <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
-    </ItemGroup>
-    <ItemGroup>
-        <AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
-    </ItemGroup>
-
+  
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>
     <RuntimeIdentifier>win-x64</RuntimeIdentifier>
   </PropertyGroup>
   </PropertyGroup>

+ 2 - 0
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/PixiEditorSettings.cs

@@ -28,6 +28,8 @@ public static class PixiEditorSettings
     {
     {
         public static SyncedSetting<bool> EnableSharedToolbar { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
         public static SyncedSetting<bool> EnableSharedToolbar { get; } = SyncedSetting.NonOwned<bool>(PixiEditor);
 
 
+        public static SyncedSetting<bool> SelectionTintingEnabled { get; } = SyncedSetting.NonOwned(PixiEditor, true);
+
         public static SyncedSetting<RightClickMode> RightClickMode { get; } =
         public static SyncedSetting<RightClickMode> RightClickMode { get; } =
             SyncedSetting.NonOwned<RightClickMode>(PixiEditor);
             SyncedSetting.NonOwned<RightClickMode>(PixiEditor);
 
 

+ 0 - 1
src/PixiEditor.sln

@@ -11,7 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildConfiguration", "Build
 	ProjectSection(SolutionItems) = preProject
 	ProjectSection(SolutionItems) = preProject
 		Custom.ruleset = Custom.ruleset
 		Custom.ruleset = Custom.ruleset
 		Directory.Build.props = Directory.Build.props
 		Directory.Build.props = Directory.Build.props
-		stylecop.json = stylecop.json
 	EndProjectSection
 	EndProjectSection
 EndProject
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{6A9DA760-1E47-414C-B8E8-3B4927F18131}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{6A9DA760-1E47-414C-B8E8-3B4927F18131}"

+ 14 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1116,5 +1116,18 @@
   "DISABLE_PREVIEWS": "Disable Previews",
   "DISABLE_PREVIEWS": "Disable Previews",
   "MAX_BILINEAR_CANVAS_SIZE": "Max Bilinear Canvas Size",
   "MAX_BILINEAR_CANVAS_SIZE": "Max Bilinear Canvas Size",
   "MAX_BILINEAR_CANVAS_SIZE_DESC": "Maximum canvas size for bilinear filtering. Set to 0 to disable bilinear filtering. Bilinear filtering improves the quality of the canvas, but can cause performance issues on large canvases.",
   "MAX_BILINEAR_CANVAS_SIZE_DESC": "Maximum canvas size for bilinear filtering. Set to 0 to disable bilinear filtering. Bilinear filtering improves the quality of the canvas, but can cause performance issues on large canvases.",
-  "INVERT_MASK": "Invert mask"
+  "INVERT_MASK": "Invert mask",
+  "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
+  "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
+  "TINT_SELECTION": "Selection tinting",
+  "PAINT_BRUSH_SHAPE_CIRCLE": "Circle",
+  "PAINT_BRUSH_SHAPE_SQUARE": "Square",
+  "BRIGHTNESS_MODE_DEFAULT": "Default",
+  "BRIGHTNESS_MODE_REPEAT": "Repeat",
+  "ROUND_STROKE_CAP": "Round",
+  "BUTT_STROKE_CAP": "Butt",
+  "SQUARE_STROKE_CAP": "Square",
+  "ROUND_STROKE_JOIN": "Round",
+  "MITER_STROKE_JOIN": "Miter",
+  "BEVEL_STROKE_JOIN": "Bevel"
 }
 }

+ 2 - 1
src/PixiEditor/Models/DocumentModels/DocumentStructureHelper.cs

@@ -8,6 +8,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Layers;
 using PixiEditor.Models.Layers;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.UI.Common.Localization;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.Models.DocumentModels;
 namespace PixiEditor.Models.DocumentModels;
 #nullable enable
 #nullable enable
@@ -34,7 +35,7 @@ internal class DocumentStructureHelper
                     count++;
                     count++;
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
         return $"{name} {count}";
         return $"{name} {count}";
     }
     }

+ 3 - 2
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -21,6 +21,7 @@ using PixiEditor.Models.Position;
 using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.Models.DocumentModels.Public;
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
 #nullable enable
@@ -617,10 +618,10 @@ internal class DocumentOperationsModule : IDocumentOperations
             if (!members.Contains(traversedNode.Id))
             if (!members.Contains(traversedNode.Id))
             {
             {
                 parent = traversedNode;
                 parent = traversedNode;
-                return false;
+                return Traverse.Exit;
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         if (parent is null)
         if (parent is null)

+ 16 - 15
src/PixiEditor/Models/DocumentModels/Public/DocumentStructureModule.cs

@@ -1,5 +1,6 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.Models.DocumentModels.Public;
 namespace PixiEditor.Models.DocumentModels.Public;
 #nullable enable
 #nullable enable
@@ -45,10 +46,10 @@ internal class DocumentStructureModule
             if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
             if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
             {
             {
                 parent = traversedNode;
                 parent = traversedNode;
-                return false;
+                return Traverse.Exit;
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         if (parent is null)
         if (parent is null)
@@ -62,10 +63,10 @@ internal class DocumentStructureModule
                 if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
                 if (!guids.Contains(traversedNode.Id) && traversedNode is IStructureMemberHandler)
                 {
                 {
                     parent = traversedNode;
                     parent = traversedNode;
-                    return false;
+                    return Traverse.Exit;
                 }
                 }
 
 
-                return true;
+                return Traverse.Further;
             });
             });
         }
         }
 
 
@@ -110,7 +111,7 @@ internal class DocumentStructureModule
         {
         {
             if (node is IStructureMemberHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
             if (node is IStructureMemberHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
                 parents.Add(parent);
                 parents.Add(parent);
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         return parents;
         return parents;
@@ -187,7 +188,7 @@ internal class DocumentStructureModule
                 toFill.Add(strNode);
                 toFill.Add(strNode);
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
     }
     }
 
 
@@ -197,10 +198,10 @@ internal class DocumentStructureModule
         startNode.TraverseForwards(node =>
         startNode.TraverseForwards(node =>
         {
         {
             if (node == startNode)
             if (node == startNode)
-                return true;
+                return Traverse.Further;
 
 
             result = node;
             result = node;
-            return false;
+            return Traverse.Exit;
         });
         });
 
 
         return result;
         return result;
@@ -218,13 +219,13 @@ internal class DocumentStructureModule
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             {
             {
                 if (node is IFolderHandler && !includeFolders)
                 if (node is IFolderHandler && !includeFolders)
-                    return true;
+                    return Traverse.Further;
 
 
                 result = structureMemberNode;
                 result = structureMemberNode;
-                return false;
+                return Traverse.Exit;
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         return result;
         return result;
@@ -242,13 +243,13 @@ internal class DocumentStructureModule
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             if (node != member && node is IStructureMemberHandler structureMemberNode)
             {
             {
                 if (node is IFolderHandler && !includeFolders)
                 if (node is IFolderHandler && !includeFolders)
-                    return true;
+                    return Traverse.Further;
 
 
                 result = structureMemberNode;
                 result = structureMemberNode;
-                return false;
+                return Traverse.Exit;
             }
             }
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         return result;
         return result;
@@ -268,7 +269,7 @@ internal class DocumentStructureModule
             if (node is IStructureMemberHandler structureMemberNode)
             if (node is IStructureMemberHandler structureMemberNode)
                 children.Add(structureMemberNode);
                 children.Add(structureMemberNode);
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         return children;
         return children;

+ 6 - 0
src/PixiEditor/Models/EnumTranslations.cs

@@ -11,6 +11,9 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Matrix;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Handlers.Toolbars;
+using PixiEditor.Models.Tools;
+using PixiEditor.Views.Overlays.BrushShapeOverlay;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 
 [assembly: LocalizeEnum<StrokeCap>(StrokeCap.Butt, "BUTT_STROKE_CAP")]
 [assembly: LocalizeEnum<StrokeCap>(StrokeCap.Butt, "BUTT_STROKE_CAP")]
@@ -118,3 +121,6 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Exclusion, "EXCLUSION_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Exclusion, "EXCLUSION_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Erase, "ERASE_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.Erase, "ERASE_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.LinearDodge, "LINEAR_DODGE_BLEND_MODE")]
 [assembly: LocalizeEnum<BlendMode>(BlendMode.LinearDodge, "LINEAR_DODGE_BLEND_MODE")]
+
+[assembly: LocalizeEnum<PaintBrushShape>(PaintBrushShape.Circle, "PAINT_BRUSH_SHAPE_CIRCLE")]
+[assembly: LocalizeEnum<PaintBrushShape>(PaintBrushShape.Square, "PAINT_BRUSH_SHAPE_SQUARE")]

+ 11 - 7
src/PixiEditor/Models/Handlers/INodeHandler.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.ComponentModel;
+using Avalonia;
 using Avalonia.Media;
 using Avalonia.Media;
 using ChunkyImageLib;
 using ChunkyImageLib;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -8,6 +9,7 @@ using Drawie.Backend.Core;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Rendering;
 using PixiEditor.Models.Structures;
 using PixiEditor.Models.Structures;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.Models.Handlers;
 namespace PixiEditor.Models.Handlers;
 
 
@@ -22,15 +24,17 @@ public interface INodeHandler : INotifyPropertyChanged, IDisposable
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public ObservableRangeCollection<INodePropertyHandler> Outputs { get; }
     public PreviewPainter? ResultPainter { get; set; }
     public PreviewPainter? ResultPainter { get; set; }
     public VecD PositionBindable { get; set; }
     public VecD PositionBindable { get; set; }
+    public Rect UiSize { get; set; }
     public bool IsNodeSelected { get; set; }
     public bool IsNodeSelected { get; set; }
     public string Icon { get; }
     public string Icon { get; }
-    public void TraverseBackwards(Func<INodeHandler, bool> func);
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func);
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func);
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func);
+    public void TraverseBackwards(Func<INodeHandler, Traverse> func);
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, Traverse> func);
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func);
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, Traverse> func);
+    public HashSet<NodeFrameViewModelBase> Frames { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> InputPropertyMap { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap { get; }
     public IReadOnlyDictionary<string, INodePropertyHandler> OutputPropertyMap { get; }
 }
 }

+ 1 - 0
src/PixiEditor/Models/Handlers/IToolsHandler.cs

@@ -19,6 +19,7 @@ internal interface IToolsHandler : IHandler
     public ICollection<IToolSetHandler> AllToolSets { get; }
     public ICollection<IToolSetHandler> AllToolSets { get; }
     public RightClickMode RightClickMode { get; set; }
     public RightClickMode RightClickMode { get; set; }
     public bool EnableSharedToolbar { get; set; }
     public bool EnableSharedToolbar { get; set; }
+    public bool SelectionTintingEnabled { get; set; }
     public event EventHandler<SelectedToolEventArgs> SelectedToolChanged;
     public event EventHandler<SelectedToolEventArgs> SelectedToolChanged;
     public void SetupTools(IServiceProvider services, ToolSetsConfig toolSetConfig);
     public void SetupTools(IServiceProvider services, ToolSetsConfig toolSetConfig);
     public void SetupToolsTooltipShortcuts();
     public void SetupToolsTooltipShortcuts();

+ 4 - 3
src/PixiEditor/Models/Rendering/MemberPreviewUpdater.cs

@@ -8,6 +8,7 @@ using PixiEditor.Helpers;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.DocumentModels;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.ChangeableDocument.Rendering;
 
 
 namespace PixiEditor.Models.Rendering;
 namespace PixiEditor.Models.Rendering;
@@ -288,16 +289,16 @@ internal class MemberPreviewUpdater
         nodeVm.TraverseForwards(next =>
         nodeVm.TraverseForwards(next =>
         {
         {
             if (next is not INodeHandler nextVm)
             if (next is not INodeHandler nextVm)
-                return true;
+                return Traverse.Further;
 
 
             var nextNode = allNodes.FirstOrDefault(x => x.Id == next.Id);
             var nextNode = allNodes.FirstOrDefault(x => x.Id == next.Id);
 
 
             if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
             if (nextNode is null || actualRepaintedNodes.Contains(next.Id))
-                return true;
+                return Traverse.Further;
 
 
             RequestRepaintNode(nextNode, nextVm);
             RequestRepaintNode(nextNode, nextVm);
             actualRepaintedNodes.Add(next.Id);
             actualRepaintedNodes.Add(next.Id);
-            return true;
+            return Traverse.Further;
         });
         });
     }
     }
 
 

+ 4 - 4
src/PixiEditor/Models/Rendering/PreviewPainter.cs

@@ -173,7 +173,7 @@ public class PreviewPainter : IDisposable
             VecI bounds = painterInstance.RequestRenderBounds?.Invoke() ?? VecI.Zero;
             VecI bounds = painterInstance.RequestRenderBounds?.Invoke() ?? VecI.Zero;
 
 
             ChunkResolution finalResolution = FindResolution(bounds);
             ChunkResolution finalResolution = FindResolution(bounds);
-            SamplingOptions samplingOptions = FindSamplingOptions(bounds);
+            SamplingOptions samplingOptions = FindSamplingOptions(matrix);
 
 
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
             renderTexture.DrawingSurface.Canvas.SetMatrix(matrix ?? Matrix3X3.Identity);
             renderTexture.DrawingSurface.Canvas.Scale((float)finalResolution.InvertedMultiplier());
             renderTexture.DrawingSurface.Canvas.Scale((float)finalResolution.InvertedMultiplier());
@@ -257,10 +257,10 @@ public class PreviewPainter : IDisposable
         return ChunkResolution.Full;
         return ChunkResolution.Full;
     }
     }
 
 
-    private SamplingOptions FindSamplingOptions(VecI bounds)
+    private SamplingOptions FindSamplingOptions(Matrix3X3? matrix)
     {
     {
-        var density = DocumentSize.X / (double)bounds.X;
-        return density > 1
+        Matrix3X3 mtx = matrix ?? Matrix3X3.Identity;
+        return mtx.ScaleX < 1f || mtx.ScaleY < 1f
             ? SamplingOptions.Bilinear
             ? SamplingOptions.Bilinear
             : SamplingOptions.Default;
             : SamplingOptions.Default;
     }
     }

+ 144 - 0
src/PixiEditor/Models/Structures/ObservableHashSet.cs

@@ -0,0 +1,144 @@
+using System.Collections;
+using System.Collections.Immutable;
+using System.Collections.Specialized;
+using System.Runtime.Serialization;
+
+namespace PixiEditor.Models.Structures;
+
+public class ObservableHashSet<T> : ISet<T>, IReadOnlySet<T>, IDeserializationCallback, ISerializable, INotifyCollectionChanged
+{
+    private readonly HashSet<T> setImplementation;
+
+    public ObservableHashSet()
+    {
+        setImplementation = new HashSet<T>();
+    }
+
+    public ObservableHashSet(IEnumerable<T> collection)
+    {
+        setImplementation = new HashSet<T>(collection);
+    }
+    
+    public bool Add(T item)
+    {
+        var isAdded = setImplementation.Add(item);
+
+        if (isAdded)
+        {
+            CallCollectionChanged(NotifyCollectionChangedAction.Add, item);
+        }
+        
+        return isAdded;
+    }
+
+    void ICollection<T>.Add(T item) => Add(item);
+
+    public void Clear()
+    {
+        setImplementation.Clear();
+        CallCollectionChanged(NotifyCollectionChangedAction.Reset, Array.Empty<T>(), setImplementation.ToList());
+    }
+
+    public bool Remove(T item)
+    {
+        var isRemoved = setImplementation.Remove(item);
+
+        if (isRemoved)
+        {
+            CallCollectionChanged(NotifyCollectionChangedAction.Remove, item);
+        }
+        
+        return isRemoved;
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void ExceptWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void IntersectWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    /// <summary>
+    /// Not implemented
+    /// </summary>
+    /// <exception cref="NotSupportedException">This method is not implemented.</exception>
+    public void SymmetricExceptWith(IEnumerable<T> other)
+    {
+        throw new NotSupportedException();
+    }
+
+    public void UnionWith(IEnumerable<T> other)
+    {
+        var allOther = other.ToImmutableHashSet();
+        var addedOnly = allOther.Except(setImplementation);
+        
+        setImplementation.UnionWith(allOther);
+        CallCollectionChanged(
+            NotifyCollectionChangedAction.Reset, 
+            addedOnly.ToList());
+    }
+
+    public void ReplaceBy(IEnumerable<T> other)
+    {
+        var otherOriginal = other.ToHashSet();
+        var original = setImplementation.ToImmutableHashSet();
+        var removed = original.Except(otherOriginal);
+
+        setImplementation.Clear();
+        setImplementation.UnionWith(otherOriginal);
+        CallCollectionChanged(NotifyCollectionChangedAction.Replace, otherOriginal.ToList(), removed.ToList());
+    }
+
+    public bool IsProperSubsetOf(IEnumerable<T> other) => setImplementation.IsProperSubsetOf(other);
+
+    public bool IsProperSupersetOf(IEnumerable<T> other) => setImplementation.IsProperSupersetOf(other);
+
+    public bool IsSubsetOf(IEnumerable<T> other) => setImplementation.IsSubsetOf(other);
+
+    public bool IsSupersetOf(IEnumerable<T> other) => setImplementation.IsSupersetOf(other);
+
+    public bool Overlaps(IEnumerable<T> other) => setImplementation.Overlaps(other);
+
+    public bool SetEquals(IEnumerable<T> other) => setImplementation.SetEquals(other);
+
+    public bool Contains(T item) => setImplementation.Contains(item);
+
+    public void CopyTo(T[] array, int arrayIndex) => setImplementation.CopyTo(array, arrayIndex);
+
+    public IEnumerator<T> GetEnumerator() => setImplementation.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)setImplementation).GetEnumerator();
+
+    public int Count => setImplementation.Count;
+
+    bool ICollection<T>.IsReadOnly => ((ISet<T>)setImplementation).IsReadOnly;
+
+    void IDeserializationCallback.OnDeserialization(object? sender) => setImplementation.OnDeserialization(sender);
+
+    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) => setImplementation.GetObjectData(info, context);
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action));
+    
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, IList added, IList removed) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, added, removed));
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, IList changed) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, changed));
+
+    private void CallCollectionChanged(NotifyCollectionChangedAction action, T item) =>
+        CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action, item));
+
+    public event NotifyCollectionChangedEventHandler? CollectionChanged;
+}

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.10")]
-[assembly: AssemblyFileVersion("2.0.1.10")]
+[assembly: AssemblyVersion("2.0.1.11")]
+[assembly: AssemblyFileVersion("2.0.1.11")]

+ 4 - 7
src/PixiEditor/Styles/Templates/NodeFrameView.axaml

@@ -7,13 +7,10 @@
         <Setter Property="Template">
         <Setter Property="Template">
             <Setter.Value>
             <Setter.Value>
                 <ControlTemplate>
                 <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>
+                    <Path Data="{Binding Geometry, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeFrameView}}"
+                          Fill="{TemplateBinding Background}"
+                          Stroke="{TemplateBinding BorderBrush}"
+                          StrokeThickness="2" />
                 </ControlTemplate>
                 </ControlTemplate>
             </Setter.Value>
             </Setter.Value>
         </Setter>
         </Setter>

+ 4 - 10
src/PixiEditor/Styles/Templates/NodeGraphView.axaml

@@ -59,7 +59,8 @@
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                     SocketDropCommand="{Binding SocketDropCommand,
                                     SocketDropCommand="{Binding SocketDropCommand,
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
                                         RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodeGraphView}}"
-                                    ResultPreview="{Binding ResultPainter}" />
+                                    ResultPreview="{Binding ResultPainter}"
+                                    Bounds="{Binding UiSize, Mode=OneWayToSource}" />
                             </DataTemplate>
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                         </ItemsControl.ItemTemplate>
                         <ItemsControl.ItemContainerTheme>
                         <ItemsControl.ItemContainerTheme>
@@ -109,9 +110,8 @@
                         <ItemsControl.ItemTemplate>
                         <ItemsControl.ItemTemplate>
                             <DataTemplate>
                             <DataTemplate>
                                 <nodes:NodeFrameView
                                 <nodes:NodeFrameView
-                                    TopLeft="{Binding TopLeft}"
-                                    BottomRight="{Binding BottomRight}"
-                                    Size="{Binding Size}">
+                                    Geometry="{Binding Geometry}"
+                                    ClipToBounds="False">
                                     <nodes:NodeFrameView.Background>
                                     <nodes:NodeFrameView.Background>
                                         <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
                                         <MultiBinding Converter="{converters:UnsetSkipMultiConverter}">
                                             <Binding Path="InternalName"
                                             <Binding Path="InternalName"
@@ -131,12 +131,6 @@
                                 </nodes:NodeFrameView>
                                 </nodes:NodeFrameView>
                             </DataTemplate>
                             </DataTemplate>
                         </ItemsControl.ItemTemplate>
                         </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>
                     </ItemsControl>
                 </Grid>
                 </Grid>
             </ControlTemplate>
             </ControlTemplate>

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

@@ -392,6 +392,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             factory.ResourceLocator = null;
             factory.ResourceLocator = null;
         }
         }
 
 
+        viewModel.NodeGraph.FinalizeCreation();
+
         return viewModel;
         return viewModel;
 
 
 
 

+ 81 - 0
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -19,6 +19,8 @@ namespace PixiEditor.ViewModels.Document;
 
 
 internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposable
 internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposable
 {
 {
+    private bool isFullyCreated;
+    
     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();
@@ -108,6 +110,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         connection.OutputProperty.ConnectedInputs.Add(connection.InputProperty);
         connection.OutputProperty.ConnectedInputs.Add(connection.InputProperty);
 
 
         Connections.Add(connection);
         Connections.Add(connection);
+        
+        UpdatesFramesPartOf(connection.InputNode);
+        UpdatesFramesPartOf(connection.OutputNode);
 
 
         StructureTree.Update(this);
         StructureTree.Update(this);
     }
     }
@@ -123,6 +128,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
             Connections.Remove(connection);
             Connections.Remove(connection);
         }
         }
 
 
+        UpdatesFramesPartOf(connection.InputNode);
+        UpdatesFramesPartOf(connection.OutputNode);
+        
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         if (node != null)
         if (node != null)
         {
         {
@@ -136,6 +144,79 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         StructureTree.Update(this);
         StructureTree.Update(this);
     }
     }
 
 
+    public void UpdatesFramesPartOf(INodeHandler node)
+    {
+        if (!isFullyCreated)
+            return;
+
+        var lastKnownFramesPartOf = node.Frames.OfType<NodeZoneViewModel>().ToHashSet();
+        var startLookup = Frames.OfType<NodeZoneViewModel>().ToDictionary(x => x.Start);
+        var currentlyPartOf = new HashSet<NodeZoneViewModel>();
+        
+        node.TraverseBackwards(x =>
+        {
+            if (x is IPairNodeEndViewModel)
+                return Traverse.NoFurther;
+
+            if (x is not IPairNodeStartViewModel)
+                return Traverse.Further;
+
+            var zone = startLookup[x];
+            currentlyPartOf.Add(zone);
+
+            return Traverse.Further;
+        });
+
+        foreach (var frame in currentlyPartOf)
+        {
+            frame.Nodes.Add(node);
+            node.Frames.Add(frame);
+        }
+
+        lastKnownFramesPartOf.ExceptWith(currentlyPartOf);
+        foreach (var removedFrom in lastKnownFramesPartOf)
+        {
+            removedFrom.Nodes.Remove(node);
+            node.Frames.Remove(removedFrom);
+        }
+    }
+
+    public void FinalizeCreation()
+    {
+        if (isFullyCreated)
+            return;
+        
+        isFullyCreated = true;
+        
+        foreach (var nodeZoneViewModel in Frames.OfType<NodeZoneViewModel>())
+        {
+            UpdateNodesPartOf(nodeZoneViewModel);
+        }
+    }
+
+    private static void UpdateNodesPartOf(NodeZoneViewModel zone)
+    {
+        var currentlyPartOf = new HashSet<INodeHandler>([zone.Start, zone.End]);
+
+        foreach (var node in zone.Start
+                     .Outputs
+                     .SelectMany(x => x.ConnectedInputs)
+                     .Select(x => x.Node))
+        {
+            node.TraverseForwards((x) =>
+            {
+                if (x is IPairNodeEndViewModel)
+                    return Traverse.NoFurther;
+
+                currentlyPartOf.Add(x);
+
+                return Traverse.Further;
+            });
+        }
+
+        zone.Nodes.ReplaceBy(currentlyPartOf);
+    }
+
     public void RemoveConnections(Guid nodeId)
     public void RemoveConnections(Guid nodeId)
     {
     {
         var connections = Connections
         var connections = Connections

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs

@@ -5,4 +5,4 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 
 [NodeViewModel("MODIFY_IMAGE_LEFT_NODE", "IMAGE", PixiPerfectIcons.PutImage)]
 [NodeViewModel("MODIFY_IMAGE_LEFT_NODE", "IMAGE", PixiPerfectIcons.PutImage)]
-internal class ModifyImageLeftNodeViewModel : NodeViewModel<ModifyImageLeftNode>;
+internal class ModifyImageLeftNodeViewModel : NodeViewModel<ModifyImageLeftNode>, IPairNodeStartViewModel;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ModifyImageRightNodeViewModel.cs

@@ -4,4 +4,4 @@ using PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 namespace PixiEditor.ViewModels.Document.Nodes;
 
 
 [NodeViewModel("MODIFY_IMAGE_RIGHT_NODE", "IMAGE", null)]
 [NodeViewModel("MODIFY_IMAGE_RIGHT_NODE", "IMAGE", null)]
-internal class ModifyImageRightNodeViewModel : NodeViewModel<ModifyImageRightNode>;
+internal class ModifyImageRightNodeViewModel : NodeViewModel<ModifyImageRightNode>, IPairNodeEndViewModel;

+ 2 - 2
src/PixiEditor/ViewModels/Document/Nodes/StructureMemberViewModel.cs

@@ -60,10 +60,10 @@ internal abstract class StructureMemberViewModel<T> : NodeViewModel<T>, IStructu
                 if (node is IFolderHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
                 if (node is IFolderHandler parent && input is { PropertyName: FolderNode.ContentInternalName })
                 {
                 {
                     visible = parent.IsVisibleBindable;
                     visible = parent.IsVisibleBindable;
-                    return visible;
+                    return visible ? Traverse.Further : Traverse.Exit;
                 }
                 }
 
 
-                return true;
+                return Traverse.Further;
             });
             });
 
 
             return visible;
             return visible;

+ 2 - 1
src/PixiEditor/ViewModels/Document/StructureTree.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.ViewModels.Document;
 namespace PixiEditor.ViewModels.Document;
 
 
@@ -60,7 +61,7 @@ internal class StructureTree
 
 
             _memberMap.TryAdd(node, lastRoot);
             _memberMap.TryAdd(node, lastRoot);
 
 
-            return true;
+            return Traverse.Further;
         });
         });
 
 
         List<IStructureMemberHandler> toRemove = new();
         List<IStructureMemberHandler> toRemove = new();

+ 6 - 0
src/PixiEditor/ViewModels/Nodes/IPairNodeEndViewModel.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public interface IPairNodeEndViewModel
+{
+    
+}

+ 6 - 0
src/PixiEditor/ViewModels/Nodes/IPairNodeStartViewModel.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public interface IPairNodeStartViewModel
+{
+    
+}

+ 2 - 28
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModel.cs

@@ -1,9 +1,4 @@
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-using System.ComponentModel;
-using CommunityToolkit.Mvvm.ComponentModel;
-using PixiEditor.Models.Handlers;
-using Drawie.Numerics;
+using PixiEditor.Models.Handlers;
 
 
 namespace PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Nodes;
 
 
@@ -16,27 +11,6 @@ internal sealed class NodeFrameViewModel : NodeFrameViewModelBase
 
 
     protected override void 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;
+        throw new NotImplementedException();
     }
     }
 }
 }

+ 23 - 21
src/PixiEditor/ViewModels/Nodes/NodeFrameViewModelBase.cs

@@ -1,20 +1,23 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.ComponentModel;
+using Avalonia.Media;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using Drawie.Numerics;
+using PixiEditor.Models.Structures;
 
 
 namespace PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Nodes;
 
 
 public abstract class NodeFrameViewModelBase : ObservableObject
 public abstract class NodeFrameViewModelBase : ObservableObject
 {
 {
     private Guid id;
     private Guid id;
+    private StreamGeometry geometry;
     private VecD topLeft;
     private VecD topLeft;
     private VecD bottomRight;
     private VecD bottomRight;
     private VecD size;
     private VecD size;
     
     
-    public ObservableCollection<INodeHandler> Nodes { get; }
+    public ObservableHashSet<INodeHandler> Nodes { get; }
 
 
     public string InternalName { get; init; }
     public string InternalName { get; init; }
     
     
@@ -24,28 +27,16 @@ public abstract class NodeFrameViewModelBase : ObservableObject
         set => SetProperty(ref id, value);
         set => SetProperty(ref id, value);
     }
     }
     
     
-    public VecD TopLeft
+    public StreamGeometry Geometry
     {
     {
-        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);
+        get => geometry;
+        set => SetProperty(ref geometry, value);
     }
     }
 
 
     public NodeFrameViewModelBase(Guid id, IEnumerable<INodeHandler> nodes)
     public NodeFrameViewModelBase(Guid id, IEnumerable<INodeHandler> nodes)
     {
     {
         Id = id;
         Id = id;
-        Nodes = new ObservableCollection<INodeHandler>(nodes);
+        Nodes = new(nodes);
 
 
         Nodes.CollectionChanged += OnCollectionChanged;
         Nodes.CollectionChanged += OnCollectionChanged;
         AddHandlers(Nodes);
         AddHandlers(Nodes);
@@ -54,13 +45,22 @@ public abstract class NodeFrameViewModelBase : ObservableObject
     private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
     {
     {
         var action = e.Action;
         var action = e.Action;
-        if (action != NotifyCollectionChangedAction.Add && action != NotifyCollectionChangedAction.Remove && action != NotifyCollectionChangedAction.Replace && action != NotifyCollectionChangedAction.Reset)
+        if (action is
+            not NotifyCollectionChangedAction.Add and
+            not NotifyCollectionChangedAction.Remove and
+            not NotifyCollectionChangedAction.Replace and
+            not NotifyCollectionChangedAction.Reset)
         {
         {
             return;
             return;
         }
         }
+
+        CalculateBounds();
+        
+        if (e.NewItems != null)
+            AddHandlers(e.NewItems.Cast<INodeHandler>());
         
         
-        AddHandlers((IEnumerable<NodeViewModel>)e.NewItems);
-        RemoveHandlers((IEnumerable<NodeViewModel>)e.OldItems);
+        if (e.OldItems != null)
+            RemoveHandlers(e.OldItems.Cast<INodeHandler>());
     }
     }
 
 
     private void AddHandlers(IEnumerable<INodeHandler> nodes)
     private void AddHandlers(IEnumerable<INodeHandler> nodes)
@@ -81,7 +81,9 @@ public abstract class NodeFrameViewModelBase : ObservableObject
 
 
     private void NodePropertyChanged(object? sender, PropertyChangedEventArgs e)
     private void NodePropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
     {
-        if (e.PropertyName != nameof(INodeHandler.PositionBindable))
+        if (e.PropertyName is
+            not nameof(INodeHandler.PositionBindable) and
+            not nameof(INodeHandler.UiSize))
         {
         {
             return;
             return;
         }
         }

+ 99 - 24
src/PixiEditor/ViewModels/Nodes/NodeViewModel.cs

@@ -1,6 +1,7 @@
 using System.Collections.ObjectModel;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
 using System.Collections.Specialized;
 using System.ComponentModel;
 using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using System.Reflection;
 using Avalonia;
 using Avalonia;
 using Avalonia.Media;
 using Avalonia.Media;
@@ -25,6 +26,7 @@ namespace PixiEditor.ViewModels.Nodes;
 internal abstract class NodeViewModel : ObservableObject, INodeHandler
 internal abstract class NodeViewModel : ObservableObject, INodeHandler
 {
 {
     private LocalizedString displayName;
     private LocalizedString displayName;
+    private Rect size;
     private IBrush? categoryBrush;
     private IBrush? categoryBrush;
     private string? nodeNameBindable;
     private string? nodeNameBindable;
     private VecD position;
     private VecD position;
@@ -111,6 +113,12 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
+    public Rect UiSize
+    {
+        get => size;
+        set => SetProperty(ref size, value);
+    }
+
     public ObservableRangeCollection<INodePropertyHandler> Inputs
     public ObservableRangeCollection<INodePropertyHandler> Inputs
     {
     {
         get => inputs;
         get => inputs;
@@ -218,7 +226,11 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
 
 
     public string Icon => icon ??= GetType().GetCustomAttribute<NodeViewModelAttribute>().Icon;
     public string Icon => icon ??= GetType().GetCustomAttribute<NodeViewModelAttribute>().Icon;
 
 
-    public void TraverseBackwards(Func<INodeHandler, bool> func)
+    [DoesNotReturn]
+    private void ThrowInvalidTraverseResult(Traverse traverse) =>
+        throw new IndexOutOfRangeException($"Invalid Traverse Option '{traverse}'");
+
+    public void TraverseBackwards(Func<INodeHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
@@ -233,9 +245,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
                 continue;
             }
             }
 
 
-            if (!func(node))
+            var result = func(node);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var inputProperty in node.Inputs)
             foreach (var inputProperty in node.Inputs)
@@ -248,7 +269,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, bool> func)
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
@@ -263,9 +284,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
                 continue;
             }
             }
 
 
-            if (!func(node.Item1, node.Item2))
+            var result = func(node.Item1, node.Item2);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var inputProperty in node.Item1.Inputs)
             foreach (var inputProperty in node.Item1.Inputs)
@@ -278,7 +308,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
+    public void TraverseBackwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
@@ -292,10 +322,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-
-            if (!func(node.Item1, node.Item2, node.Item3))
+            var result = func(node.Item1, node.Item2, node.Item3);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var inputProperty in node.Item1.Inputs)
             foreach (var inputProperty in node.Item1.Inputs)
@@ -308,7 +346,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseForwards(Func<INodeHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
         var queueNodes = new Queue<INodeHandler>();
@@ -322,10 +360,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-
-            if (!func(node))
+            
+            var result = func(node);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var outputProperty in node.Outputs)
             foreach (var outputProperty in node.Outputs)
@@ -338,7 +385,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler)>();
@@ -352,10 +399,19 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
             {
             {
                 continue;
                 continue;
             }
             }
-
-            if (!func(node.Item1, node.Item2))
+            
+            var result = func(node.Item1, node.Item2);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var outputProperty in node.Item1.Outputs)
             foreach (var outputProperty in node.Item1.Outputs)
@@ -368,7 +424,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
-    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, bool> func)
+    public void TraverseForwards(Func<INodeHandler, INodeHandler, INodePropertyHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler)>();
@@ -383,9 +439,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
                 continue;
             }
             }
 
 
-            if (!func(node.Item1, node.Item2, node.Item3))
+            var result = func(node.Item1, node.Item2, node.Item3);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var outputProperty in node.Item1.Outputs)
             foreach (var outputProperty in node.Item1.Outputs)
@@ -399,7 +464,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
     }
     }
 
 
     public void TraverseForwards(
     public void TraverseForwards(
-        Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, bool> func)
+        Func<INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler, Traverse> func)
     {
     {
         var visited = new HashSet<INodeHandler>();
         var visited = new HashSet<INodeHandler>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
         var queueNodes = new Queue<(INodeHandler, INodeHandler, INodePropertyHandler, INodePropertyHandler)>();
@@ -414,9 +479,18 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
                 continue;
                 continue;
             }
             }
 
 
-            if (!func(node.Item1, node.Item2, node.Item3, node.Item4))
+            var result = func(node.Item1, node.Item2, node.Item3, node.Item4);
+            switch (result)
             {
             {
-                return;
+                case Traverse.NoFurther:
+                    continue;
+                case Traverse.Exit:
+                    return;
+                case Traverse.Further:
+                    break;
+                default:
+                    ThrowInvalidTraverseResult(result);
+                    break;
             }
             }
 
 
             foreach (var outputProperty in node.Item1.Outputs)
             foreach (var outputProperty in node.Item1.Outputs)
@@ -429,6 +503,7 @@ internal abstract class NodeViewModel : ObservableObject, INodeHandler
         }
         }
     }
     }
 
 
+    public HashSet<NodeFrameViewModelBase> Frames { get; } = [];
 
 
     public virtual void Dispose()
     public virtual void Dispose()
     {
     {

+ 222 - 35
src/PixiEditor/ViewModels/Nodes/NodeZoneViewModel.cs

@@ -1,20 +1,26 @@
-using PixiEditor.Models.Handlers;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using Avalonia;
+using Avalonia.Media;
+using PixiEditor.Models.Handlers;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.ViewModels.Nodes;
 namespace PixiEditor.ViewModels.Nodes;
 
 
 public sealed class NodeZoneViewModel : NodeFrameViewModelBase
 public sealed class NodeZoneViewModel : NodeFrameViewModelBase
 {
 {
-    private INodeHandler start;
-    private INodeHandler end;
+    public INodeHandler Start { get; }
     
     
-    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id, [start, end])
+    public INodeHandler End { get; }
+
+    public NodeZoneViewModel(Guid id, string internalName, INodeHandler start, INodeHandler end) : base(id,
+        [start, end])
     {
     {
         InternalName = internalName;
         InternalName = internalName;
-        
-        this.start = start.Metadata.IsPairNodeStart ? start : end;
-        this.end = start.Metadata.IsPairNodeStart ? end : start;
-        
+
+        this.Start = start.Metadata.IsPairNodeStart ? start : end;
+        this.End = start.Metadata.IsPairNodeStart ? end : start;
+
         CalculateBounds();
         CalculateBounds();
     }
     }
 
 
@@ -22,53 +28,234 @@ public sealed class NodeZoneViewModel : NodeFrameViewModelBase
     {
     {
         if (Nodes.Count == 0)
         if (Nodes.Count == 0)
         {
         {
-            if (TopLeft == BottomRight)
+            return;
+        }
+
+        var points = GetBoundPoints();
+
+        Geometry = BuildRoundedHullGeometry(points, 25);
+    }
+
+    private static StreamGeometry BuildRoundedHullGeometry(List<VecD> points, double cornerRadius)
+    {
+        const double startBoostDeg = 100;
+        const double maxBoost = 2.5;
+
+        var span = CollectionsMarshal.AsSpan(points);
+
+        var pool = ArrayPool<VecD>.Shared;
+        var hullBuf = pool.Rent(Math.Max(3, span.Length));
+
+        try
+        {
+            var hullCount = ConvexHull(span, hullBuf.AsSpan());
+            var hull = hullBuf.AsSpan(0, hullCount);
+
+            var geometry = new StreamGeometry();
+            if (hull.IsEmpty) return geometry;
+
+            using var ctx = geometry.Open();
+            
+            if (hull.Length <= 2 || cornerRadius <= 0)
             {
             {
-                BottomRight = TopLeft + new VecD(100, 100);
+                ctx.BeginFigure(new Point(hull[0].X, hull[0].Y), isFilled: true);
+                for (var i = 1; i < hull.Length; i++)
+                    ctx.LineTo(new Point(hull[i].X, hull[i].Y));
+                ctx.EndFigure(isClosed: true);
+                return geometry;
             }
             }
-            
-            return;
+
+            var n = hull.Length;
+
+            var enter = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var exit = n <= 256 ? stackalloc VecD[n] : pool.Rent(n).AsSpan(0, n);
+            var rented = n > 256;
+
+            try
+            {
+                for (var i = 0; i < n; i++)
+                {
+                    var prev = hull[(i - 1 + n) % n];
+                    var current = hull[i];
+                    var next = hull[(i + 1) % n];
+
+                    var directionIn = (current - prev).Normalize();
+                    var directionOut = (next - current).Normalize();
+                    var lenIn = (prev - current).Length;
+                    var lenOut = (current - next).Length;
+
+                    var a = (prev - current).Normalize();
+                    var b = (next - current).Normalize();
+                    var dot = Math.Clamp(a.X * b.X + a.Y * b.Y, -1, 1);
+                    var theta = Math.Acos(dot); // radians
+
+                    // Boost wide angles a bit (same curve, fewer ops)
+                    var thetaDeg = theta * (180.0 / Math.PI);
+                    var tNorm = Math.Clamp((thetaDeg - startBoostDeg) / (180.0 - startBoostDeg), 0, 1);
+                    var s = tNorm * tNorm * (3 - 2 * tNorm); // smoothstep
+                    var radiusHere = cornerRadius * (1 + (maxBoost - 1) * s);
+
+                    var t = (theta > 1e-6) ? radiusHere / Math.Tan(theta / 2.0) : 0;
+                    var tMax = Math.Min(lenIn, lenOut) * 0.5;
+                    t = Math.Min(t, tMax);
+
+                    if (t <= 1e-6)
+                    {
+                        enter[i] = current;
+                        exit[i] = current;
+                    }
+                    else
+                    {
+                        enter[i] = current + directionIn * -t;
+                        exit[i] = current + directionOut * t;
+                    }
+                }
+
+                ctx.BeginFigure(new Point(enter[0].X, enter[0].Y), isFilled: true);
+
+                for (var i = 0; i < n; i++)
+                {
+                    ctx.QuadraticBezierTo(
+                        new Point(hull[i].X, hull[i].Y),
+                        new Point(exit[i].X, exit[i].Y));
+
+                    var nextEnter = enter[(i + 1) % n];
+                    ctx.LineTo(new Point(nextEnter.X, nextEnter.Y));
+                }
+
+                ctx.EndFigure(isClosed: true);
+            }
+            finally
+            {
+                if (rented)
+                {
+                    pool.Return(
+                        MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(enter), n).ToArray());
+
+                    pool.Return(MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(exit), n)
+                        .ToArray());
+                }
+            }
+
+            return geometry;
+        }
+        finally
+        {
+            pool.Return(hullBuf);
         }
         }
+    }
+
+    private static int ConvexHull(ReadOnlySpan<VecD> input, Span<VecD> hull)
+    {
+        var n = input.Length;
+        if (n <= 1)
+        {
+            if (n == 1) hull[0] = input[0];
+            return n;
+        }
+
+        var pool = ArrayPool<VecD>.Shared;
+        var pts = pool.Rent(n);
+
+        try
+        {
+            input.CopyTo(pts);
+            Array.Sort(pts, 0, n, VecDComparer.Instance);
+
+            var m = 0;
+            for (var i = 0; i < n; i++)
+            {
+                if (m == 0 || !pts[i].Equals(pts[m - 1]))
+                    pts[m++] = pts[i];
+            }
+
+            if (m <= 1)
+            {
+                if (m == 1) hull[0] = pts[0];
+                return m;
+            }
+
+            var k = 0;
+
+            for (var i = 0; i < m; i++)
+            {
+                while (k >= 2 && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
+
+            var t = k + 1;
+            for (var i = m - 2; i >= 0; i--)
+            {
+                while (k >= t && (hull[k - 1] - hull[k - 2]).Cross(pts[i] - hull[k - 2]) <= 0)
+                    k--;
+                hull[k++] = pts[i];
+            }
 
 
-        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);
+            return k - 1;
+        }
+        finally
+        {
+            pool.Return(pts);
+        }
+    }
 
 
-        TopLeft = new VecD(minX, minY);
-        BottomRight = new VecD(maxX, maxY);
+    private sealed class VecDComparer : IComparer<VecD>
+    {
+        public static readonly VecDComparer Instance = new();
 
 
-        Size = BottomRight - TopLeft;
+        public int Compare(VecD a, VecD b)
+        {
+            var cx = a.X.CompareTo(b.X);
+            return cx != 0 ? cx : a.Y.CompareTo(b.Y);
+        }
     }
     }
 
 
-    private List<RectD> GetBounds()
+    private List<VecD> GetBoundPoints()
     {
     {
-        var list = new List<RectD>();
+        var list = new List<VecD>(Nodes.Count * 4);
+
+        const int defaultXOffset = 30;
+        const int defaultYOffset = 45;
 
 
-        const int defaultXOffset = -30;
-        const int defaultYOffset = -45;
-        
-        // TODO: Use the actual node height
         foreach (var node in Nodes)
         foreach (var node in Nodes)
         {
         {
-            if (node == start)
+            var pos = node.PositionBindable;
+            var size = new VecD(node.UiSize.Size.Width, node.UiSize.Size.Height);
+
+            if (node == Start)
             {
             {
-                list.Add(new RectD(node.PositionBindable + new VecD(100, defaultYOffset), new VecD(100, 400)));
+                var twoThirdsX = size.X * (2.0 / 3.0);
+
+                list.Add(pos + new VecD(twoThirdsX, -defaultYOffset));
+                list.Add(pos + new VecD(twoThirdsX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(size.X + defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(size.X + defaultXOffset, defaultYOffset + size.Y));
                 continue;
                 continue;
             }
             }
 
 
-            if (node == end)
+            if (node == End)
             {
             {
-                list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(100, 400)));
+                var oneThirdX = size.X / 3.0;
+
+                list.Add(pos + new VecD(oneThirdX, -defaultYOffset));
+                list.Add(pos + new VecD(oneThirdX, defaultYOffset + size.Y));
+
+                list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+                list.Add(pos + new VecD(-defaultXOffset, defaultYOffset + size.Y));
                 continue;
                 continue;
             }
             }
-            
-            list.Add(new RectD(node.PositionBindable + new VecD(defaultXOffset, defaultYOffset), new VecD(200, 400)));
+
+            var right = defaultXOffset + size.X;
+            var bottom = defaultYOffset + size.Y;
+
+            list.Add(pos + new VecD(-defaultXOffset, -defaultYOffset));
+            list.Add(pos + new VecD(right, -defaultYOffset));
+            list.Add(pos + new VecD(-defaultXOffset, bottom));
+            list.Add(pos + new VecD(right, bottom));
         }
         }
-        
+
         return list;
         return list;
     }
     }
 }
 }

+ 19 - 0
src/PixiEditor/ViewModels/Nodes/Traverse.cs

@@ -0,0 +1,19 @@
+namespace PixiEditor.ViewModels.Nodes;
+
+public enum Traverse
+{
+    /// <summary>
+    /// Go further in this direction, meaning any further child connections will not be enqueued.
+    /// </summary>
+    Further,
+    
+    /// <summary>
+    /// Don't go further in this direction, meaning all further child connections will be enqueued.
+    /// </summary>
+    NoFurther,
+    
+    /// <summary>
+    /// Completely stop traversing in any direction, meaning this will drop all enqueued child connections.
+    /// </summary>
+    Exit
+}

+ 4 - 0
src/PixiEditor/ViewModels/PixiObservableObject.cs

@@ -1,5 +1,6 @@
 using System.ComponentModel;
 using System.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands;
 
 
 namespace PixiEditor.ViewModels;
 namespace PixiEditor.ViewModels;
@@ -11,4 +12,7 @@ public class PixiObservableObject : ObservableObject
         base.OnPropertyChanged(e);
         base.OnPropertyChanged(e);
         CommandController.Current.NotifyPropertyChanged(e.PropertyName);
         CommandController.Current.NotifyPropertyChanged(e.PropertyName);
     }
     }
+
+    protected void SubscribeSettingsValueChanged<T>(Setting<T> settingStore, string propertyName) =>
+        settingStore.ValueChanged += (_, _) => OnPropertyChanged(propertyName);
 }
 }

+ 17 - 0
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -63,6 +63,19 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         }
         }
     }
     }
 
 
+    public bool SelectionTintingEnabled
+    {
+        get => PixiEditorSettings.Tools.SelectionTintingEnabled.Value;
+        set
+        {
+            if (SelectionTintingEnabled == value)
+                return;
+
+            PixiEditorSettings.Tools.SelectionTintingEnabled.Value = value;
+            OnPropertyChanged();
+        }
+    }
+
     private Cursor? toolCursor;
     private Cursor? toolCursor;
 
 
     public Cursor? ToolCursor
     public Cursor? ToolCursor
@@ -118,6 +131,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
     {
         owner.DocumentManagerSubViewModel.ActiveDocumentChanged += ActiveDocumentChanged;
         owner.DocumentManagerSubViewModel.ActiveDocumentChanged += ActiveDocumentChanged;
         PixiEditorSettings.Tools.PrimaryToolset.ValueChanged += PrimaryToolsetOnValueChanged;
         PixiEditorSettings.Tools.PrimaryToolset.ValueChanged += PrimaryToolsetOnValueChanged;
+        SubscribeSettingsValueChanged(PixiEditorSettings.Tools.SelectionTintingEnabled, nameof(SelectionTintingEnabled));
     }
     }
 
 
     private void PrimaryToolsetOnValueChanged(Setting<string> setting, string? newPrimaryToolset)
     private void PrimaryToolsetOnValueChanged(Setting<string> setting, string? newPrimaryToolset)
@@ -174,6 +188,9 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         OnPropertyChanged(nameof(NonSelectedToolSets));
         OnPropertyChanged(nameof(NonSelectedToolSets));
     }
     }
 
 
+    [Command.Basic("PixiEditor.Tools.ToggleSelectionTinting", "TOGGLE_TINTING_SELECTION", "TOGGLE_TINTING_SELECTION_DESCRIPTIVE", AnalyticsTrack = true)]
+    public void ToggleTintSelection() => SelectionTintingEnabled = !SelectionTintingEnabled;
+
     public void SetupToolsTooltipShortcuts()
     public void SetupToolsTooltipShortcuts()
     {
     {
         foreach (IToolHandler tool in allTools)
         foreach (IToolHandler tool in allTools)

+ 9 - 0
src/PixiEditor/ViewModels/UserPreferences/Settings/SceneSettings.cs

@@ -3,6 +3,7 @@ using Avalonia.Media;
 using CommunityToolkit.Mvvm.Input;
 using CommunityToolkit.Mvvm.Input;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
 
 
 namespace PixiEditor.ViewModels.UserPreferences.Settings;
 namespace PixiEditor.ViewModels.UserPreferences.Settings;
@@ -44,6 +45,12 @@ internal class SceneSettings : SettingsGroup
         set => RaiseAndUpdatePreference(ref _secondaryBackgroundColorHex, value, PreferencesConstants.SecondaryBackgroundColor);
         set => RaiseAndUpdatePreference(ref _secondaryBackgroundColorHex, value, PreferencesConstants.SecondaryBackgroundColor);
     }
     }
 
 
+    public bool SelectionTintingEnabled
+    {
+        get => PixiEditorSettings.Tools.SelectionTintingEnabled.Value;
+        set => RaiseAndUpdatePreference(PixiEditorSettings.Tools.SelectionTintingEnabled, value);
+    }
+
     public Color PrimaryBackgroundColor
     public Color PrimaryBackgroundColor
     {
     {
         get => Color.Parse(PrimaryBackgroundColorHex);
         get => Color.Parse(PrimaryBackgroundColorHex);
@@ -65,5 +72,7 @@ internal class SceneSettings : SettingsGroup
             PrimaryBackgroundColorHex = PreferencesConstants.PrimaryBackgroundColorDefault;
             PrimaryBackgroundColorHex = PreferencesConstants.PrimaryBackgroundColorDefault;
             SecondaryBackgroundColorHex = PreferencesConstants.SecondaryBackgroundColorDefault;
             SecondaryBackgroundColorHex = PreferencesConstants.SecondaryBackgroundColorDefault;
         });
         });
+        
+        SubscribeValueChanged(PixiEditorSettings.Tools.SelectionTintingEnabled, nameof(SelectionTintingEnabled));
     }
     }
 }
 }

+ 16 - 1
src/PixiEditor/ViewModels/UserPreferences/SettingsGroup.cs

@@ -1,10 +1,11 @@
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using CommunityToolkit.Mvvm.ComponentModel;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Extensions.CommonApi.UserPreferences.Settings;
 
 
 namespace PixiEditor.ViewModels.UserPreferences;
 namespace PixiEditor.ViewModels.UserPreferences;
 
 
-internal class SettingsGroup : ObservableObject
+internal class SettingsGroup : PixiObservableObject
 {
 {
     protected static T GetPreference<T>(string name)
     protected static T GetPreference<T>(string name)
     {
     {
@@ -27,4 +28,18 @@ internal class SettingsGroup : ObservableObject
         SetProperty(ref backingStore, value, propertyName: name);
         SetProperty(ref backingStore, value, propertyName: name);
         IPreferences.Current.UpdatePreference(name, value);
         IPreferences.Current.UpdatePreference(name, value);
     }
     }
+    
+    protected void RaiseAndUpdatePreference<T>(Setting<T> settingStore, T value, [CallerMemberName] string name = "")
+    {
+        if (EqualityComparer<T>.Default.Equals(settingStore.Value, value))
+            return;
+
+        settingStore.Value = value;
+        OnPropertyChanged(name);
+    }
+
+    protected void SubscribeValueChanged<T>(Setting<T> settingStore, string propertyName)
+    {
+        settingStore.ValueChanged += (_, _) => OnPropertyChanged(propertyName);
+    }
 }
 }

+ 9 - 1
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -155,6 +155,13 @@ internal class ViewportOverlays
             Mode = BindingMode.OneWay
             Mode = BindingMode.OneWay
         };
         };
 
 
+        Binding selectionTintingEnabled = new()
+        {
+            Source = ViewModelMain.Current,
+            Path = "ToolsSubViewModel.SelectionTintingEnabled",
+            Mode = BindingMode.OneWay
+        };
+
         MultiBinding showFillBinding = new()
         MultiBinding showFillBinding = new()
         {
         {
             Converter = new AllTrueConverter(),
             Converter = new AllTrueConverter(),
@@ -162,7 +169,8 @@ internal class ViewportOverlays
             Bindings = new List<IBinding>()
             Bindings = new List<IBinding>()
             {
             {
                 toolIsSelectionBinding,
                 toolIsSelectionBinding,
-                isTransformingBinding
+                isTransformingBinding,
+                selectionTintingEnabled
             }
             }
         };
         };
 
 

+ 6 - 21
src/PixiEditor/Views/Nodes/NodeFrameView.cs

@@ -1,32 +1,17 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
+using Avalonia.Media;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace PixiEditor.Views.Nodes;
 namespace PixiEditor.Views.Nodes;
 
 
 public class NodeFrameView : TemplatedControl
 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
+    public static readonly StyledProperty<StreamGeometry> GeometryProperty = AvaloniaProperty.Register<NodeFrameView, StreamGeometry>(nameof(Geometry));
+
+    public StreamGeometry Geometry
     {
     {
-        get => GetValue(SizeProperty);
-        set => SetValue(SizeProperty, value);
+        get => GetValue(GeometryProperty);
+        set => SetValue(GeometryProperty, value);
     }
     }
 }
 }

+ 7 - 0
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -421,6 +421,13 @@
                                 d:Content="Reset"
                                 d:Content="Reset"
                                 Background="{DynamicResource ThemeAccentBrush}"
                                 Background="{DynamicResource ThemeAccentBrush}"
                                 ui:Translator.Key="RESET" />
                                 ui:Translator.Key="RESET" />
+                            
+                            <TextBlock ui:Translator.Key="SELECTION" Classes="h5" />
+
+                            <CheckBox Classes="leftOffset" Width="200" HorizontalAlignment="Left"
+                                      ui:Translator.Key="TINT_SELECTION"
+                                      IsChecked="{Binding SettingsSubViewModel.Scene.SelectionTintingEnabled}" />
+
                         </controls:FixedSizeStackPanel>
                         </controls:FixedSizeStackPanel>
                     </ScrollViewer>
                     </ScrollViewer>
                     <ScrollViewer>
                     <ScrollViewer>

+ 0 - 20
src/stylecop.json

@@ -1,20 +0,0 @@
-{
-  "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
-  "settings": {
-    "indentation": {
-      "indentationSize": 4
-    },
-    "maintainabilityRules": {
-      "topLevelTypes": [ "class", "interface", "enum", "struct" ]
-    },
-    "readabilityRules": {
-      "allowBuiltInTypeAliases": false
-    },
-    "documentationRules": {
-      "xmlHeader": false,
-      "documentInterfaces": false,
-      "documentExposedElements": false,
-      "documentInternalElements": false
-    }
-  }
-}