Browse Source

Picker ui rc1 + fixed points list not being clonable

flabbet 11 months ago
parent
commit
6752ae603f

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/NodeInfoAttribute.cs

@@ -10,6 +10,7 @@ public class NodeInfoAttribute : Attribute
     public string? PickerName { get; set; }
     public string? PickerName { get; set; }
     
     
     public string? Category { get; set; }
     public string? Category { get; set; }
+    public string? Icon { get; set; }
 
 
     public NodeInfoAttribute(string uniqueName, string displayName)
     public NodeInfoAttribute(string uniqueName, string displayName)
     {
     {

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

@@ -1,31 +0,0 @@
-using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
-using PixiEditor.ChangeableDocument.Rendering;
-using PixiEditor.DrawingApi.Core;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
-
-[NodeInfo("ImageSpace", "IMAGE_SPACE_NODE", Category = "IMAGE")]
-public class ImageSpaceNode : Node
-{
-    public FuncOutputProperty<VecD> SpacePosition { get; }
-    
-    public FuncOutputProperty<VecI> Size { get; }
-
-    public ImageSpaceNode()
-    {
-        // TODO: Implement this
-        //SpacePosition = CreateFuncOutput(nameof(SpacePosition), "UV", ctx => ctx.Position);
-        Size = CreateFuncOutput(nameof(Size), "SIZE", ctx => ctx.Size);
-    }
-
-
-    protected override Texture? OnExecute(RenderingContext context)
-    {
-        return null;
-    }
-
-
-    public override Node CreateCopy() => new ImageSpaceNode();
-}

+ 7 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Points/PointList.cs

@@ -3,7 +3,7 @@ using PixiEditor.Numerics;
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
 
 
-public class PointList : List<VecD>, ICacheable
+public class PointList : List<VecD>, ICacheable, ICloneable
 {
 {
     public required int HashValue { get; set; }
     public required int HashValue { get; set; }
 
 
@@ -22,4 +22,10 @@ public class PointList : List<VecD>, ICacheable
     public static PointList Empty { get; } = new(0) { HashValue = 0 };
     public static PointList Empty { get; } = new(0) { HashValue = 0 };
 
 
     public int GetCacheHash() => HashValue;
     public int GetCacheHash() => HashValue;
+    public object Clone()
+    {
+        var clone = new PointList(this) { HashValue = HashValue };
+        clone.HashValue = HashValue;
+        return clone;
+    }
 }
 }

+ 1 - 0
src/PixiEditor/App.axaml

@@ -20,6 +20,7 @@
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml" />
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml" />
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml" />
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml" />
         <StyleInclude Source="/Styles/Buttons/CaptionButtonsStyle.axaml" />
         <StyleInclude Source="/Styles/Buttons/CaptionButtonsStyle.axaml" />
+        <StyleInclude Source="/Styles/NodeIcons.axaml"/>
     </Application.Styles>
     </Application.Styles>
     <Application.Resources>
     <Application.Resources>
         <ResourceDictionary>
         <ResourceDictionary>

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

@@ -729,5 +729,13 @@
   "CHANGE_ACTIVE_FRAME_NEXT": "Change active frame to next",
   "CHANGE_ACTIVE_FRAME_NEXT": "Change active frame to next",
   "TOGGLE_ANIMATION": "Toggle animation",
   "TOGGLE_ANIMATION": "Toggle animation",
   "NEW_FROM_CLIPBOARD": "New from clipboard",
   "NEW_FROM_CLIPBOARD": "New from clipboard",
-  "OFFSET": "Offset"
+  "OFFSET": "Offset",
+  "SHAPE": "Shape",
+  "STRUCTURE": "Structure",
+  "NUMBERS": "Numbers",
+  "OPERATIONS": "Operations",
+  "GENERATION": "Generation",
+  "NUMBER": "Number",
+  "ANIMATION": "Animation",
+  "SAMPLE_IMAGE": "Sample Image"
 }
 }

+ 31 - 0
src/PixiEditor/Fonts/NodeIcons.cs

@@ -0,0 +1,31 @@
+using System.Collections.ObjectModel;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Animable;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.CombineSeparate;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.FilterNodes;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Points;
+
+namespace PixiEditor.Fonts;
+
+public static class NodeIcons
+{
+    public static ReadOnlyDictionary<Type, string> IconMap { get; } = new ReadOnlyDictionary<Type, string>(
+        new Dictionary<Type, string>
+        {
+            { typeof(TimeNode), "\uE900" },
+            { typeof(FolderNode), "\ue901" },
+            { typeof(CreateImageNode), "\ue902" },
+            { typeof(MergeNode), "\ue903" },
+            { typeof(ModifyImageLeftNode), "\ue904" },
+            { typeof(ImageLayerNode), "\ue905" },
+            { typeof(RasterizePointsNode), "\ue906" },
+            { typeof(SampleImageNode), "\ue907" },
+            { typeof(CombineColorNode), "\ue908" },
+            { typeof(ApplyFilterNode), "\ue909" },
+            { typeof(DistributePointsNode), "\ue90a" },
+            { typeof(LerpColorNode), "\ue90b" },
+            { typeof(NoiseNode), "\ue90c" },
+            { typeof(EllipseNode), "\ue90d" },
+            { typeof(MathNode), "\ue90e" }
+        });
+}

BIN
src/PixiEditor/Fonts/nodeicons.ttf


+ 1 - 1
src/PixiEditor/Helpers/Nodes/NodeAbbreviation.cs

@@ -1,5 +1,5 @@
 using System.Buffers;
 using System.Buffers;
-using PixiEditor.Models.Nodes;
+using PixiEditor.ViewModels.Nodes;
 
 
 namespace PixiEditor.Helpers.Nodes;
 namespace PixiEditor.Helpers.Nodes;
 
 

+ 22 - 0
src/PixiEditor/Styles/NodeIcons.axaml

@@ -0,0 +1,22 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <!-- Add Controls for Previewer Here -->
+        </Border>
+    </Design.PreviewWith>
+
+    <Styles.Resources>
+        <ResourceDictionary>
+            <FontFamily x:Key="NodeIcons">avares://PixiEditor/Fonts/nodeicons.ttf#nodeicons</FontFamily>
+        </ResourceDictionary>
+    </Styles.Resources>
+    
+    <Style Selector="TextBlock.node-icon">
+        <Setter Property="FontFamily" Value="{DynamicResource NodeIcons}" />
+    </Style>
+
+    <Style Selector="Run.node-icon">
+        <Setter Property="FontFamily" Value="{DynamicResource NodeIcons}" />
+    </Style>
+</Styles>

+ 33 - 13
src/PixiEditor/Styles/Templates/NodePicker.axaml

@@ -4,7 +4,7 @@
                     xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
                     xmlns:visuals="clr-namespace:PixiEditor.Views.Visuals"
                     xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:input="clr-namespace:PixiEditor.Views.Input"
                     xmlns:input="clr-namespace:PixiEditor.Views.Input"
-                    xmlns:nodeModels="clr-namespace:PixiEditor.Models.Nodes">
+                    xmlns:nodes1="clr-namespace:PixiEditor.ViewModels.Nodes">
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
     <ControlTheme TargetType="nodes:NodePicker" x:Key="{x:Type nodes:NodePicker}">
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
@@ -19,10 +19,10 @@
                     </Grid.ColumnDefinitions>
                     </Grid.ColumnDefinitions>
 
 
                     <input:InputBox Grid.ColumnSpan="2"
                     <input:InputBox Grid.ColumnSpan="2"
-                        Text="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
-                        Name="PART_InputBox" />
-                    
-                    <ListBox Grid.Column="0" Grid.Row="1" ItemsSource="{TemplateBinding FilteredCategories}" 
+                                    Text="{Binding SearchQuery, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                    Name="PART_InputBox" />
+
+                    <ListBox Grid.Column="0" Grid.Row="1" ItemsSource="{TemplateBinding FilteredCategories}"
                              SelectedItem="{Binding SelectedCategory, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
                              SelectedItem="{Binding SelectedCategory, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
                         <ListBox.ItemTemplate>
                         <ListBox.ItemTemplate>
                             <DataTemplate>
                             <DataTemplate>
@@ -32,18 +32,38 @@
                             </DataTemplate>
                             </DataTemplate>
                         </ListBox.ItemTemplate>
                         </ListBox.ItemTemplate>
                     </ListBox>
                     </ListBox>
-                    
+
                     <ScrollViewer Grid.Row="1" Grid.Column="1" Name="PART_ScrollViewer"
                     <ScrollViewer Grid.Row="1" Grid.Column="1" Name="PART_ScrollViewer"
                                   Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
                                   Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
                         <ItemsControl MinHeight="200" Name="PART_NodeList"
                         <ItemsControl MinHeight="200" Name="PART_NodeList"
-                                      ItemsSource="{TemplateBinding FilteredNodeTypeInfos}">
+                                      ItemsSource="{TemplateBinding FilteredNodeGroups}">
                             <ItemsControl.ItemTemplate>
                             <ItemsControl.ItemTemplate>
-                                <DataTemplate DataType="nodeModels:NodeTypeInfo">
-                                    <Button
-                                        Command="{Binding SelectNodeCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodePicker}}"
-                                        CommandParameter="{Binding}"
-                                        IsVisible="{Binding !Hidden}"
-                                        ui:Translator.Key="{Binding FinalPickerName}" />
+                                <DataTemplate DataType="nodes1:NodeTypeGroup">
+                                    <StackPanel>
+                                        <TextBlock
+                                            ui:Translator.Key="{Binding Name}"
+                                            FontWeight="Bold"
+                                            Margin="5" />
+                                        <ItemsControl ItemsSource="{Binding NodeTypes}">
+                                            <ItemsControl.ItemTemplate>
+                                                <DataTemplate DataType="nodes1:NodeTypeInfo">
+                                                        <Button
+                                                            Background="Transparent"
+                                                            Command="{Binding SelectNodeCommand, RelativeSource={RelativeSource FindAncestor, AncestorType=nodes:NodePicker}}"
+                                                            CommandParameter="{Binding}"
+                                                            HorizontalContentAlignment="Left"
+                                                            IsVisible="{Binding !Hidden}">
+                                                            <TextBlock Margin="10 0 0 0">
+                                                                <Run Classes="node-icon"
+                                                                     BaselineAlignment="Center"
+                                                                     Text="{Binding Icon}" />
+                                                                <Run ui:Translator.Key="{Binding FinalPickerName}" />
+                                                            </TextBlock>
+                                                        </Button>
+                                                </DataTemplate>
+                                            </ItemsControl.ItemTemplate>
+                                        </ItemsControl>
+                                    </StackPanel>
                                 </DataTemplate>
                                 </DataTemplate>
                             </ItemsControl.ItemTemplate>
                             </ItemsControl.ItemTemplate>
                         </ItemsControl>
                         </ItemsControl>

+ 15 - 0
src/PixiEditor/ViewModels/Nodes/NodeTypeGroup.cs

@@ -0,0 +1,15 @@
+using System.Collections.ObjectModel;
+
+namespace PixiEditor.ViewModels.Nodes;
+
+public class NodeTypeGroup : PixiObservableObject
+{
+    public string Name { get; set; }
+    public ObservableCollection<NodeTypeInfo> NodeTypes { get; set; }
+
+    public NodeTypeGroup(string name, IEnumerable<NodeTypeInfo> nodeTypes)
+    {
+        Name = name;
+        NodeTypes = new ObservableCollection<NodeTypeInfo>(nodeTypes);
+    }    
+}

+ 10 - 1
src/PixiEditor/Models/Nodes/NodeTypeInfo.cs → src/PixiEditor/ViewModels/Nodes/NodeTypeInfo.cs

@@ -1,8 +1,10 @@
 using System.Reflection;
 using System.Reflection;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Fonts;
+using PixiEditor.UI.Common.Fonts;
 
 
-namespace PixiEditor.Models.Nodes;
+namespace PixiEditor.ViewModels.Nodes;
 
 
 public class NodeTypeInfo
 public class NodeTypeInfo
 {
 {
@@ -19,6 +21,8 @@ public class NodeTypeInfo
     public bool Hidden => PickerName is { Length: 0 };
     public bool Hidden => PickerName is { Length: 0 };
     
     
     public Type NodeType { get; }
     public Type NodeType { get; }
+    
+    public string Icon { get; }
 
 
     public NodeTypeInfo(Type type)
     public NodeTypeInfo(Type type)
     {
     {
@@ -31,6 +35,11 @@ public class NodeTypeInfo
         PickerName = attribute.PickerName;
         PickerName = attribute.PickerName;
         Category = attribute.Category ?? "";
         Category = attribute.Category ?? "";
 
 
+        if (NodeIcons.IconMap.TryGetValue(type, out var icon))
+        {
+            Icon = icon;
+        }
+
         FinalPickerName = (PickerName ?? DisplayName);
         FinalPickerName = (PickerName ?? DisplayName);
     }
     }
 }
 }

+ 0 - 1
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -14,7 +14,6 @@ using PixiEditor.ViewModels.Document;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers;
-using PixiEditor.Models.Nodes;
 using PixiEditor.Numerics;
 using PixiEditor.Numerics;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.ViewModels.Nodes;
 using PixiEditor.Views.Nodes.Properties;
 using PixiEditor.Views.Nodes.Properties;

+ 78 - 58
src/PixiEditor/Views/Nodes/NodePicker.cs

@@ -6,7 +6,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input;
 using PixiEditor.Helpers.Nodes;
 using PixiEditor.Helpers.Nodes;
-using PixiEditor.Models.Nodes;
+using PixiEditor.ViewModels.Nodes;
 using PixiEditor.Views.Input;
 using PixiEditor.Views.Input;
 
 
 namespace PixiEditor.Views.Nodes;
 namespace PixiEditor.Views.Nodes;
@@ -29,8 +29,8 @@ public partial class NodePicker : TemplatedControl
         AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(
         AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(
             nameof(AllNodeTypeInfos));
             nameof(AllNodeTypeInfos));
 
 
-    public static readonly StyledProperty<ObservableCollection<NodeTypeInfo>> FilteredNodeTypeInfosProperty =
-        AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeInfo>>(nameof(FilteredNodeTypeInfos));
+    public static readonly StyledProperty<ObservableCollection<NodeTypeGroup>> FilteredNodeGroupsProperty =
+        AvaloniaProperty.Register<NodePicker, ObservableCollection<NodeTypeGroup>>(nameof(FilteredNodeGroups));
 
 
     public static readonly StyledProperty<string> SelectedCategoryProperty =
     public static readonly StyledProperty<string> SelectedCategoryProperty =
         AvaloniaProperty.Register<NodePicker, string>(
         AvaloniaProperty.Register<NodePicker, string>(
@@ -48,10 +48,10 @@ public partial class NodePicker : TemplatedControl
         set => SetValue(AllNodeTypeInfosProperty, value);
         set => SetValue(AllNodeTypeInfosProperty, value);
     }
     }
 
 
-    public ObservableCollection<NodeTypeInfo> FilteredNodeTypeInfos
+    public ObservableCollection<NodeTypeGroup> FilteredNodeGroups
     {
     {
-        get => GetValue(FilteredNodeTypeInfosProperty);
-        set => SetValue(FilteredNodeTypeInfosProperty, value);
+        get => GetValue(FilteredNodeGroupsProperty);
+        set => SetValue(FilteredNodeGroupsProperty, value);
     }
     }
 
 
     public static readonly StyledProperty<ObservableCollection<string>> AllCategoriesProperty =
     public static readonly StyledProperty<ObservableCollection<string>> AllCategoriesProperty =
@@ -90,8 +90,6 @@ public partial class NodePicker : TemplatedControl
         set { SetValue(ScrollOffsetProperty, value); }
         set { SetValue(ScrollOffsetProperty, value); }
     }
     }
 
 
-    private Dictionary<string, int> _categoryIndexes = new();
-
     public static readonly StyledProperty<Vector> ScrollOffsetProperty =
     public static readonly StyledProperty<Vector> ScrollOffsetProperty =
         AvaloniaProperty.Register<NodePicker, Vector>(nameof(ScrollOffset));
         AvaloniaProperty.Register<NodePicker, Vector>(nameof(ScrollOffset));
 
 
@@ -128,7 +126,7 @@ public partial class NodePicker : TemplatedControl
 
 
             int index = (int)(normalizedY * _itemsControl.Items.Count);
             int index = (int)(normalizedY * _itemsControl.Items.Count);
             index = Math.Clamp(index, 0, _itemsControl.Items.Count - 1);
             index = Math.Clamp(index, 0, _itemsControl.Items.Count - 1);
-            string category = FilteredNodeTypeInfos[index].Category;
+            string category = FilteredNodeGroups[index].Name;
             if (string.IsNullOrEmpty(category))
             if (string.IsNullOrEmpty(category))
             {
             {
                 category = MiscCategory;
                 category = MiscCategory;
@@ -149,50 +147,75 @@ public partial class NodePicker : TemplatedControl
 
 
         if (NodeAbbreviation.IsAbbreviation(nodePicker.SearchQuery, out var abbreviationName))
         if (NodeAbbreviation.IsAbbreviation(nodePicker.SearchQuery, out var abbreviationName))
         {
         {
-            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
-                .Where(x => SearchComparer(x, abbreviationName)));
+            nodePicker.FilteredNodeGroups = nodePicker.NodeTypeGroupsFromQuery(abbreviationName);
+            FilterCategories(nodePicker);
         }
         }
         else
         else
         {
         {
             if (string.IsNullOrEmpty(nodePicker.SearchQuery))
             if (string.IsNullOrEmpty(nodePicker.SearchQuery))
             {
             {
                 nodePicker.FilteredCategories = new ObservableCollection<string>(nodePicker.AllCategories);
                 nodePicker.FilteredCategories = new ObservableCollection<string>(nodePicker.AllCategories);
-                UpdateCategoryDict(nodePicker);
+
+                nodePicker.FilteredNodeGroups =
+                    nodePicker.NodeTypeGroupsFromQuery(null);
                 
                 
-                nodePicker.FilteredNodeTypeInfos =
-                    OrderByCategory(nodePicker);
+                FilterCategories(nodePicker);
             }
             }
             else
             else
             {
             {
-                nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
-                    .Where(x => SearchComparer(x, nodePicker.SearchQuery)));
-                
+                nodePicker.FilteredNodeGroups = nodePicker.NodeTypeGroupsFromQuery(nodePicker.SearchQuery);
+
                 FilterCategories(nodePicker);
                 FilterCategories(nodePicker);
             }
             }
         }
         }
-
-        return;
-
-        bool SearchComparer(NodeTypeInfo x, string lookFor) =>
-            x.FinalPickerName.Value.Replace(" ", "")
-                .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
     }
     }
 
 
-    private static void UpdateCategoryDict(NodePicker nodePicker)
-    {
-        nodePicker._categoryIndexes = nodePicker.FilteredCategories
-            .Select((x, i) => (x, i))
-            .ToDictionary(x => x.x, x => x.i);
-    }
 
 
-    private static ObservableCollection<NodeTypeInfo> OrderByCategory(NodePicker nodePicker)
+    private ObservableCollection<NodeTypeGroup> NodeTypeGroupsFromQuery(string? query)
     {
     {
-        return new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos
-            .Where(x => x.Category != null)
-            .OrderBy(
-                x => string.IsNullOrEmpty(x.Category)
-                    ? nodePicker._categoryIndexes[MiscCategory]
-                    : nodePicker._categoryIndexes[x.Category]));
+        var filtered = 
+            (!string.IsNullOrEmpty(query)
+                ? AllNodeTypeInfos.Where(x => SearchComparer(x, query))
+                : AllNodeTypeInfos).ToList();
+
+        if (filtered.Count == 0) return new ObservableCollection<NodeTypeGroup>();
+
+        var groups = new ObservableCollection<NodeTypeGroup>();
+        foreach (var group in groups)
+        {
+            group.NodeTypes.Clear();
+        }
+
+        foreach (var info in filtered)
+        {
+            string category = string.IsNullOrEmpty(info.Category) ? MiscCategory : info.Category;
+            var existingGroup = groups.FirstOrDefault(x => x.Name == category); 
+            if (existingGroup == null)
+            {
+                existingGroup = new NodeTypeGroup(category, new List<NodeTypeInfo>());
+                groups.Add(existingGroup);
+            }
+
+            existingGroup.NodeTypes.Add(info);
+        }
+        
+        var miscGroup = groups.FirstOrDefault(x => x.Name == MiscCategory);
+        if (miscGroup != null)
+        {
+            int index = groups.IndexOf(miscGroup);
+            groups.Move(index, groups.Count - 1);
+        }
+        
+        for (var i = 0; i < groups.Count; i++)
+        {
+            if (groups[i].NodeTypes.Count == 0)
+            {
+                groups.RemoveAt(i);
+                i--;
+            }
+        }
+
+        return groups;
     }
     }
 
 
     private void OnInputBoxKeyDown(object? sender, KeyEventArgs e)
     private void OnInputBoxKeyDown(object? sender, KeyEventArgs e)
@@ -204,9 +227,9 @@ public partial class NodePicker : TemplatedControl
 
 
         var nodes = NodeAbbreviation.FromString(SearchQuery, AllNodeTypeInfos);
         var nodes = NodeAbbreviation.FromString(SearchQuery, AllNodeTypeInfos);
 
 
-        if (nodes == null && FilteredNodeTypeInfos.Count > 0)
+        if (nodes == null && FilteredNodeGroups.Count > 0)
         {
         {
-            SelectNodeCommand.Execute(FilteredNodeTypeInfos[0]);
+            SelectNodeCommand.Execute(FilteredNodeGroups[0]);
         }
         }
         else
         else
         {
         {
@@ -217,37 +240,34 @@ public partial class NodePicker : TemplatedControl
         }
         }
     }
     }
 
 
+    private static bool SearchComparer(NodeTypeInfo x, string lookFor) =>
+        x.FinalPickerName.Value.Replace(" ", "")
+            .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
+
     private static void OnAllNodeTypesChanged(AvaloniaPropertyChangedEventArgs e)
     private static void OnAllNodeTypesChanged(AvaloniaPropertyChangedEventArgs e)
     {
     {
         if (e.Sender is NodePicker nodePicker)
         if (e.Sender is NodePicker nodePicker)
         {
         {
-            nodePicker.FilteredNodeTypeInfos = new ObservableCollection<NodeTypeInfo>(nodePicker.AllNodeTypeInfos);
+            nodePicker.FilteredNodeGroups = nodePicker.NodeTypeGroupsFromQuery(null);
             nodePicker.AllCategories = new ObservableCollection<string>(
             nodePicker.AllCategories = new ObservableCollection<string>(
                 nodePicker.AllNodeTypeInfos.Select(x => x.Category)
                 nodePicker.AllNodeTypeInfos.Select(x => x.Category)
-                    .Where(x => !string.IsNullOrEmpty(x)).Distinct());
-
-            nodePicker.AllCategories.Add(MiscCategory);
-
-            nodePicker.FilteredCategories = new ObservableCollection<string>(nodePicker.AllCategories);
+                    .Where(x => !string.IsNullOrEmpty(x)).Distinct()); 
 
 
-            UpdateCategoryDict(nodePicker); 
-
-            nodePicker.FilteredNodeTypeInfos = OrderByCategory(nodePicker);
+            nodePicker.FilteredNodeGroups = nodePicker.NodeTypeGroupsFromQuery(null); 
+            FilterCategories(nodePicker);
         }
         }
     }
     }
-    
+
     private static void FilterCategories(NodePicker nodePicker)
     private static void FilterCategories(NodePicker nodePicker)
     {
     {
         nodePicker.FilteredCategories = new ObservableCollection<string>(nodePicker.AllCategories
         nodePicker.FilteredCategories = new ObservableCollection<string>(nodePicker.AllCategories
-            .Where(x => nodePicker.FilteredNodeTypeInfos.Any(y => y.Category == x)));
-        
-        bool miscCategoryExists = nodePicker.FilteredNodeTypeInfos.Any(x => string.IsNullOrEmpty(x.Category));
+            .Where(x => nodePicker.FilteredNodeGroups.Any(y => y.Name == x)));
+
+        bool miscCategoryExists = nodePicker.FilteredNodeGroups.Any(x => x.Name == MiscCategory);
         if (miscCategoryExists)
         if (miscCategoryExists)
         {
         {
             nodePicker.FilteredCategories.Add(MiscCategory);
             nodePicker.FilteredCategories.Add(MiscCategory);
         }
         }
-        
-        UpdateCategoryDict(nodePicker); 
     }
     }
 
 
     private static void SelectedCategoryChanged(AvaloniaPropertyChangedEventArgs e)
     private static void SelectedCategoryChanged(AvaloniaPropertyChangedEventArgs e)
@@ -259,15 +279,15 @@ public partial class NodePicker : TemplatedControl
                 return;
                 return;
             }
             }
 
 
-            int indexOfFirstItemInCategory = nodePicker.FilteredNodeTypeInfos
+            int indexOfFirstItemInCategory = nodePicker.FilteredNodeGroups
                 .Select((x, i) => (x, i))
                 .Select((x, i) => (x, i))
-                .FirstOrDefault(x => x.x.Category == nodePicker.SelectedCategory).i;
+                .FirstOrDefault(x => x.x.Name == nodePicker.SelectedCategory).i;
 
 
-            double normalizedY = indexOfFirstItemInCategory / (double)nodePicker.FilteredNodeTypeInfos.Count;
+            double normalizedY = indexOfFirstItemInCategory / (double)nodePicker.FilteredNodeGroups.Count;
 
 
             double y = normalizedY * nodePicker._scrollViewer.ScrollBarMaximum.Y;
             double y = normalizedY * nodePicker._scrollViewer.ScrollBarMaximum.Y;
-            
-            if(double.IsNaN(y)) return;
+
+            if (double.IsNaN(y)) return;
 
 
             nodePicker.ScrollOffset = new Vector(0, y);
             nodePicker.ScrollOffset = new Vector(0, y);
         }
         }