diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs index 454c3a5d2..b0c3a36dd 100644 --- a/Source/Editor/Scripting/ScriptType.cs +++ b/Source/Editor/Scripting/ScriptType.cs @@ -1393,5 +1393,32 @@ namespace FlaxEditor.Scripting return _custom.GetMembers(name, MemberTypes.Method, bindingAttr).FirstOrDefault(); return ScriptMemberInfo.Null; } + + /// + /// Basic check to see if a type could be casted to another type + /// + /// Source type + /// Target type + /// True if the type can be casted + public static bool CanCast(ScriptType from, ScriptType to) + { + if (from == to) + return true; + if (from == Null || to == Null) + return false; + return (from.Type != typeof(void) && from.Type != typeof(FlaxEngine.Object)) && + (to.Type != typeof(void) && to.Type != typeof(FlaxEngine.Object)) && + from.IsAssignableFrom(to); + } + + /// + /// Basic check to see if this type could be casted to another type + /// + /// Target type + /// True if the type can be casted + public bool CanCastTo(ScriptType to) + { + return CanCast(this, to); + } } } diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index cffb2ad6f..e9d73ae68 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -744,6 +744,16 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private sealed class InvokeMethodNode : SurfaceNode @@ -1151,6 +1161,54 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + if (nodeArch.Tag is not ScriptMemberInfo memberInfo) + return false; + + if (!memberInfo.IsStatic) + { + if (VisjectSurface.FullCastCheck(memberInfo.DeclaringType, outputType, hint)) + return true; + } + + var parameters = memberInfo.GetParameters(); + bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid); + if (outputType.IsVoid) + return !isPure; + + foreach (var param in parameters) + { + if (param.IsOut) + continue; + if (VisjectSurface.FullCastCheck(param.Type, outputType, hint)) + return true; + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + if (nodeArch.Tag is not ScriptMemberInfo memberInfo) + return false; + if (VisjectSurface.FullCastCheck(memberInfo.ValueType, inputType, hint)) + return true; + + var parameters = memberInfo.GetParameters(); + bool isPure = (parameters.Length == 0 && !memberInfo.ValueType.IsVoid); + if (inputType.IsVoid) + return !isPure; + + foreach (var param in memberInfo.GetParameters()) + { + if (!param.IsOut) + continue; + if (VisjectSurface.FullCastCheck(param.Type, inputType, hint)) + return true; + } + return false; + } } private sealed class ReturnNode : SurfaceNode @@ -1777,6 +1835,16 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private abstract class FieldNodeBase : SurfaceNode @@ -1913,6 +1981,64 @@ namespace FlaxEditor.Surface.Archetypes Title = "Get " + SurfaceUtils.GetMethodDisplayName((string)Values[1]); UpdateSignature(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + else + { + var isStatic = (bool)nodeArch.DefaultValues[3]; + if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + break; + } + + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint)) + return true; + } + else + { + var typeName = (string)nodeArch.DefaultValues[2]; + if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), inputType, hint)) + return true; + } + break; + } + + return false; + } } private sealed class SetFieldNode : FieldNodeBase @@ -1966,6 +2092,48 @@ namespace FlaxEditor.Surface.Archetypes Title = "Set " + SurfaceUtils.GetMethodDisplayName((string)Values[1]); UpdateSignature(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + if (outputType.IsVoid) + return true; + + var scriptType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + if (scriptType == ScriptType.Null) + return false; + + var members = scriptType.GetMembers((string)nodeArch.DefaultValues[1], MemberTypes.Field, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var member in members) + { + if (!SurfaceUtils.IsValidVisualScriptField(member)) + continue; + + if (member) + { + if (VisjectSurface.FullCastCheck(member.ValueType, outputType, hint)) + return true; + if (!member.IsStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + else + { + var typeName = (string)nodeArch.DefaultValues[2]; + if (VisjectSurface.FullCastCheck(TypeUtils.GetType(typeName), outputType, hint)) + return true; + var isStatic = (bool)nodeArch.DefaultValues[3]; + if (!isStatic && VisjectSurface.FullCastCheck(scriptType, outputType, hint)) + return true; + } + break; + } + + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + return inputType.IsVoid; + } } private abstract class EventBaseNode : SurfaceNode, IFunctionsDependantNode @@ -2184,6 +2352,43 @@ namespace FlaxEditor.Surface.Archetypes base.OnDestroy(); } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + // Event based nodes always have a pulse input, so it's always compatible with void + if (outputType.IsVoid) + return true; + + var eventName = (string)nodeArch.DefaultValues[1]; + var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (member && SurfaceUtils.IsValidVisualScriptEvent(member)) + { + if (!member.IsStatic) + { + if (VisjectSurface.FullCastCheck(eventType, outputType, hint)) + return true; + } + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + // Event based nodes always have a pulse output, so it's always compatible with void + if (inputType.IsVoid) + return true; + + var eventName = (string)nodeArch.DefaultValues[1]; + var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (member && SurfaceUtils.IsValidVisualScriptEvent(member)) + { + if (VisjectSurface.FullCastCheck(member.ValueType, inputType, hint)) + return true; + } + return false; + } } private sealed class BindEventNode : EventBaseNode @@ -2265,6 +2470,8 @@ namespace FlaxEditor.Surface.Archetypes Title = string.Empty, Description = "Overrides the base class method with custom implementation", Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste, + IsInputCompatible = MethodOverrideNode.IsInputCompatible, + IsOutputCompatible = MethodOverrideNode.IsOutputCompatible, Size = new Float2(240, 60), DefaultValues = new object[] { @@ -2277,6 +2484,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Create = (id, context, arch, groupArch) => new InvokeMethodNode(id, context, arch, groupArch), + IsInputCompatible = InvokeMethodNode.IsInputCompatible, + IsOutputCompatible = InvokeMethodNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2317,6 +2526,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Create = (id, context, arch, groupArch) => new VisualScriptFunctionNode(id, context, arch, groupArch), + IsInputCompatible = VisualScriptFunctionNode.IsInputCompatible, + IsOutputCompatible = VisualScriptFunctionNode.IsOutputCompatible, Title = "New Function", Description = "Adds a new function to the script", Flags = NodeFlags.VisualScriptGraph, @@ -2330,6 +2541,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Create = (id, context, arch, groupArch) => new GetFieldNode(id, context, arch, groupArch), + IsInputCompatible = GetFieldNode.IsInputCompatible, + IsOutputCompatible = GetFieldNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2345,6 +2558,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Create = (id, context, arch, groupArch) => new SetFieldNode(id, context, arch, groupArch), + IsInputCompatible = SetFieldNode.IsInputCompatible, + IsOutputCompatible = SetFieldNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(240, 60), @@ -2361,6 +2576,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Create = (id, context, arch, groupArch) => new BindEventNode(id, context, arch, groupArch), + IsInputCompatible = EventBaseNode.IsInputCompatible, + IsOutputCompatible = EventBaseNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(260, 60), @@ -2383,6 +2600,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 10, Create = (id, context, arch, groupArch) => new UnbindEventNode(id, context, arch, groupArch), + IsInputCompatible = EventBaseNode.IsInputCompatible, + IsOutputCompatible = EventBaseNode.IsOutputCompatible, Title = string.Empty, Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(260, 60), diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs index 5a5e9cd5b..e61b8e0df 100644 --- a/Source/Editor/Surface/Archetypes/Packing.cs +++ b/Source/Editor/Surface/Archetypes/Packing.cs @@ -207,6 +207,26 @@ namespace FlaxEditor.Surface.Archetypes AddElement(box); } } + + protected static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + // Event based nodes always have a pulse input, so it's always compatible with void + if (outputType.IsVoid) + return true; + + var eventName = (string)nodeArch.DefaultValues[1]; + var eventType = TypeUtils.GetType((string)nodeArch.DefaultValues[0]); + var member = eventType.GetMember(eventName, MemberTypes.Event, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); + if (member && SurfaceUtils.IsValidVisualScriptEvent(member)) + { + if (!member.IsStatic) + { + if (VisjectSurface.FullCastCheck(eventType, outputType, hint)) + return true; + } + } + return false; + } } private sealed class PackStructureNode : StructureNode @@ -216,6 +236,35 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch, false) { } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + for (var i = 0; i < fieldsLength; i++) + { + if (VisjectSurface.FullCastCheck(fields[i].ValueType, outputType, hint)) + return true; + } + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + if (VisjectSurface.FullCastCheck(type, inputType, hint)) + return true; + } + return false; + } } private sealed class UnpackStructureNode : StructureNode @@ -225,6 +274,35 @@ namespace FlaxEditor.Surface.Archetypes : base(id, context, nodeArch, groupArch, true) { } + + internal static bool IsInputCompatible(NodeArchetype nodeArch, ScriptType outputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + if (VisjectSurface.FullCastCheck(type, outputType, hint)) + return true; + } + return false; + } + + internal static bool IsOutputCompatible(NodeArchetype nodeArch, ScriptType inputType, ConnectionsHint hint) + { + var typeName = (string)nodeArch.DefaultValues[0]; + var type = TypeUtils.GetType(typeName); + if (type) + { + var fields = type.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(x => x.IsField).ToArray(); + var fieldsLength = fields.Length; + for (var i = 0; i < fieldsLength; i++) + { + if (VisjectSurface.FullCastCheck(fields[i].ValueType, inputType, hint)) + return true; + } + } + return false; + } } /// @@ -351,6 +429,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 26, Title = "Pack Structure", Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch), + IsInputCompatible = PackStructureNode.IsInputCompatible, + IsOutputCompatible = PackStructureNode.IsOutputCompatible, Description = "Makes the structure data to from the components.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), @@ -461,6 +541,8 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 36, Title = "Unpack Structure", Create = (id, context, arch, groupArch) => new UnpackStructureNode(id, context, arch, groupArch), + IsInputCompatible = UnpackStructureNode.IsInputCompatible, + IsOutputCompatible = UnpackStructureNode.IsOutputCompatible, Description = "Breaks the structure data to allow extracting components from it.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs index bae62556e..342429fc8 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; -using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -40,6 +39,8 @@ namespace FlaxEditor.Surface.ContextMenu public delegate List ParameterGetterDelegate(); private readonly List _groups = new List(16); + private CheckBox _contextSensitiveToggle; + private bool _contextSensitiveSearchEnabled = true; private readonly TextBox _searchBox; private bool _waitingForInput; private VisjectCMGroup _surfaceParametersGroup; @@ -127,7 +128,7 @@ namespace FlaxEditor.Surface.ContextMenu _parameterSetNodeArchetype = info.ParameterSetNodeArchetype ?? Archetypes.Parameters.Nodes[3]; // Context menu dimensions - Size = new Float2(320, 248); + Size = new Float2(300, 400); var headerPanel = new Panel(ScrollBars.None) { @@ -139,17 +140,41 @@ namespace FlaxEditor.Surface.ContextMenu }; // Title bar + var titleFontReference = new FontReference(Style.Current.FontLarge.Asset, 10); var titleLabel = new Label { - Width = Width - 8, + Width = Width * 0.5f - 8f, Height = 20, X = 4, Parent = headerPanel, Text = "Select Node", - HorizontalAlignment = TextAlignment.Center, - Font = new FontReference(Style.Current.FontLarge.Asset, 10), + HorizontalAlignment = TextAlignment.Near, + Font = titleFontReference, }; + // Context sensitive toggle + var contextSensitiveLabel = new Label + { + Width = Width * 0.5f - 28, + Height = 20, + X = Width * 0.5f, + Parent = headerPanel, + Text = "Context Sensitive", + TooltipText = "Should the nodes be filtered to only show those that can be connected in the current context?", + HorizontalAlignment = TextAlignment.Far, + Font = titleFontReference, + }; + + _contextSensitiveToggle = new CheckBox + { + Width = 20, + Height = 20, + X = Width - 24, + Parent = headerPanel, + Checked = _contextSensitiveSearchEnabled, + }; + _contextSensitiveToggle.StateChanged += OnContextSensitiveToggleStateChanged; + // Search box _searchBox = new SearchBox(false, 2, 22) { @@ -288,6 +313,10 @@ namespace FlaxEditor.Surface.ContextMenu OnSearchFilterChanged(); } } + else if (_contextSensitiveSearchEnabled) + { + group.EvaluateVisibilityWithBox(_selectedBox); + } Profiler.EndEvent(); } @@ -321,6 +350,8 @@ namespace FlaxEditor.Surface.ContextMenu Parent = group }; } + if (_contextSensitiveSearchEnabled) + group.EvaluateVisibilityWithBox(_selectedBox); group.SortChildren(); group.Parent = _groupsPanel; _groups.Add(group); @@ -418,8 +449,26 @@ namespace FlaxEditor.Surface.ContextMenu return; Profiler.BeginEvent("VisjectCM.OnSearchFilterChanged"); - - if (string.IsNullOrEmpty(_searchBox.Text)) + UpdateFilters(); + _searchBox.Focus(); + Profiler.EndEvent(); + } + + private void OnContextSensitiveToggleStateChanged(CheckBox checkBox) + { + // Skip events during setup or init stuff + if (IsLayoutLocked) + return; + + Profiler.BeginEvent("VisjectCM.OnContextSensitiveToggleStateChanged"); + _contextSensitiveSearchEnabled = checkBox.Checked; + UpdateFilters(); + Profiler.EndEvent(); + } + + private void UpdateFilters() + { + if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBox == null) { ResetView(); Profiler.EndEvent(); @@ -430,7 +479,7 @@ namespace FlaxEditor.Surface.ContextMenu LockChildrenRecursive(); for (int i = 0; i < _groups.Count; i++) { - _groups[i].UpdateFilter(_searchBox.Text); + _groups[i].UpdateFilter(_searchBox.Text, _contextSensitiveSearchEnabled ? _selectedBox : null); _groups[i].UpdateItemSort(_selectedBox); } SortGroups(); @@ -443,9 +492,6 @@ namespace FlaxEditor.Surface.ContextMenu PerformLayout(); if (SelectedItem != null) _panel1.ScrollViewTo(SelectedItem); - _searchBox.Focus(); - Profiler.EndEvent(); - Profiler.EndEvent(); } @@ -503,7 +549,11 @@ namespace FlaxEditor.Surface.ContextMenu _searchBox.Clear(); SelectedItem = null; for (int i = 0; i < _groups.Count; i++) + { _groups[i].ResetView(); + if (_contextSensitiveSearchEnabled) + _groups[i].EvaluateVisibilityWithBox(_selectedBox); + } UnlockChildrenRecursive(); SortGroups(); @@ -759,5 +809,12 @@ namespace FlaxEditor.Surface.ContextMenu { return GetPreviousSiblings(item).OfType(); } + + /// + public override void OnDestroy() + { + _contextSensitiveToggle.StateChanged -= OnContextSensitiveToggleStateChanged; + base.OnDestroy(); + } } } diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs index 446840f2c..fd825cdff 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMGroup.cs @@ -66,7 +66,7 @@ namespace FlaxEditor.Surface.ContextMenu { if (_children[i] is VisjectCMItem item) { - item.UpdateFilter(null); + item.UpdateFilter(null, null); item.UpdateScore(null); } } @@ -84,23 +84,41 @@ namespace FlaxEditor.Surface.ContextMenu /// Updates the filter. /// /// The filter text. - public void UpdateFilter(string filterText) + public void UpdateFilter(string filterText, Box selectedBox) { Profiler.BeginEvent("VisjectCMGroup.UpdateFilter"); // Update items bool isAnyVisible = false; + bool groupHeaderMatches = QueryFilterHelper.Match(filterText, HeaderText); for (int i = 0; i < _children.Count; i++) { if (_children[i] is VisjectCMItem item) { - item.UpdateFilter(filterText); + item.UpdateFilter(filterText, selectedBox, groupHeaderMatches); isAnyVisible |= item.Visible; } } - // Update header title - if (QueryFilterHelper.Match(filterText, HeaderText)) + // Update itself + if (isAnyVisible) + { + if (!string.IsNullOrEmpty(filterText)) + Open(false); + Visible = true; + } + else + { + // Hide group if none of the items matched the filter + Visible = false; + } + + Profiler.EndEvent(); + } + + public void EvaluateVisibilityWithBox(Box selectedBox) + { + if (selectedBox == null) { for (int i = 0; i < _children.Count; i++) { @@ -109,14 +127,25 @@ namespace FlaxEditor.Surface.ContextMenu item.Visible = true; } } - isAnyVisible = true; + Visible = true; + return; + } + + Profiler.BeginEvent("VisjectCMGroup.EvaluateVisibilityWithBox"); + + bool isAnyVisible = false; + for (int i = 0; i < _children.Count; i++) + { + if (_children[i] is VisjectCMItem item) + { + item.Visible = item.CanConnectTo(selectedBox); + isAnyVisible |= item.Visible; + } } // Update itself if (isAnyVisible) { - if (!string.IsNullOrEmpty(filterText)) - Open(false); Visible = true; } else diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index b68d529a1..b512f6946 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; using System.Linq; +using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Utilities; namespace FlaxEditor.Surface.ContextMenu { @@ -56,7 +58,7 @@ namespace FlaxEditor.Surface.ContextMenu /// The group archetype. /// The archetype. public VisjectCMItem(VisjectCMGroup group, GroupArchetype groupArchetype, NodeArchetype archetype) - : base(0, 0, 120, 12) + : base(0, 0, 120, 14) { Group = group; _groupArchetype = groupArchetype; @@ -77,7 +79,7 @@ namespace FlaxEditor.Surface.ContextMenu if (!Visible) return; - if (selectedBox != null && CanConnectTo(selectedBox, NodeArchetype)) + if (selectedBox != null && CanConnectTo(selectedBox)) SortScore += 1; if (Data != null) SortScore += 1; @@ -92,35 +94,91 @@ namespace FlaxEditor.Surface.ContextMenu textRect = new Rectangle(22, 0, Width - 24, Height); } - private bool CanConnectTo(Box startBox, NodeArchetype nodeArchetype) + /// + /// Checks if this context menu item can be connected to a given box, before a node is actually spawned. + /// + /// The connected box + /// True if the connected box is compatible with this item + public bool CanConnectTo(Box startBox) { + // Is compatible if box is null for reset reasons if (startBox == null) - return false; - if (!startBox.IsOutput) - return false; // For now, I'm only handing the output box case + return true; - if (nodeArchetype.Elements != null) + if (_archetype == null) + return false; + + bool isCompatible = false; + if (startBox.IsOutput && _archetype.IsInputCompatible != null) { - for (int i = 0; i < nodeArchetype.Elements.Length; i++) - { - if (nodeArchetype.Elements[i].Type == NodeElementType.Input && - startBox.CanUseType(nodeArchetype.Elements[i].ConnectionsType)) - { - return true; - } - } + isCompatible |= _archetype.IsInputCompatible.Invoke(_archetype, startBox.CurrentType, _archetype.ConnectionsHints); } - return false; + else if (!startBox.IsOutput && _archetype.IsOutputCompatible != null) + { + isCompatible |= _archetype.IsOutputCompatible.Invoke(_archetype, startBox.CurrentType, startBox.ParentNode.Archetype.ConnectionsHints); + } + else if (_archetype.Elements != null) + { + // Check compatibility based on the defined elements in the archetype. This handles all the default groups and items + isCompatible = CheckElementsCompatibility(startBox); + } + + return isCompatible; + } + + private bool CheckElementsCompatibility(Box startBox) + { + bool isCompatible = false; + foreach (NodeElementArchetype element in _archetype.Elements) + { + // Ignore all elements that aren't inputs or outputs (e.g. input fields) + if (element.Type != NodeElementType.Output && element.Type != NodeElementType.Input) + continue; + + // Ignore elements with the same direction as the box + if ((startBox.IsOutput && element.Type == NodeElementType.Output) || (!startBox.IsOutput && element.Type == NodeElementType.Input)) + continue; + + ScriptType fromType; + ScriptType toType; + ConnectionsHint hint; + if (startBox.IsOutput) + { + fromType = element.ConnectionsType; + toType = startBox.CurrentType; + hint = _archetype.ConnectionsHints; + } + else + { + fromType = startBox.CurrentType; + toType = element.ConnectionsType; + hint = startBox.ParentNode.Archetype.ConnectionsHints; + } + + isCompatible |= VisjectSurface.FullCastCheck(fromType, toType, hint); + } + + return isCompatible; } /// /// Updates the filter. /// /// The filter text. - public void UpdateFilter(string filterText) + public void UpdateFilter(string filterText, Box selectedBox, bool groupHeaderMatches = false) { + if (selectedBox != null) + { + Visible = CanConnectTo(selectedBox); + if (!Visible) + { + _highlights?.Clear(); + return; + } + } + _isStartsWithMatch = _isFullMatch = false; - if (filterText == null) + if (string.IsNullOrEmpty(filterText)) { // Clear filter _highlights?.Clear(); @@ -184,7 +242,7 @@ namespace FlaxEditor.Surface.ContextMenu Data = data; } - else + else if (!groupHeaderMatches) { // Hide _highlights?.Clear(); diff --git a/Source/Editor/Surface/Elements/Box.cs b/Source/Editor/Surface/Elements/Box.cs index e771fa71c..a03c8f701 100644 --- a/Source/Editor/Surface/Elements/Box.cs +++ b/Source/Editor/Surface/Elements/Box.cs @@ -91,7 +91,7 @@ namespace FlaxEditor.Surface.Elements _currentType = value; // Check if will need to update box connections due to type change - if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !CanCast(prev, _currentType)) + if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !prev.CanCastTo(_currentType)) { // Remove all invalid connections and update those which still can be valid var connections = Connections.ToArray(); @@ -231,58 +231,8 @@ namespace FlaxEditor.Surface.Elements } // Check using connection hints - var connectionsHints = ParentNode.Archetype.ConnectionsHints; - if (Archetype.ConnectionsType == ScriptType.Null && connectionsHints != ConnectionsHint.None) - { - if ((connectionsHints & ConnectionsHint.Anything) == ConnectionsHint.Anything) - return true; - if ((connectionsHints & ConnectionsHint.Value) == ConnectionsHint.Value && type.Type != typeof(void)) - return true; - if ((connectionsHints & ConnectionsHint.Enum) == ConnectionsHint.Enum && type.IsEnum) - return true; - if ((connectionsHints & ConnectionsHint.Array) == ConnectionsHint.Array && type.IsArray) - return true; - if ((connectionsHints & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && type.IsDictionary) - return true; - if ((connectionsHints & ConnectionsHint.Vector) == ConnectionsHint.Vector) - { - var t = type.Type; - if (t == typeof(Vector2) || - t == typeof(Vector3) || - t == typeof(Vector4) || - t == typeof(Float2) || - t == typeof(Float3) || - t == typeof(Float4) || - t == typeof(Double2) || - t == typeof(Double3) || - t == typeof(Double4) || - t == typeof(Int2) || - t == typeof(Int3) || - t == typeof(Int4) || - t == typeof(Color)) - { - return true; - } - } - if ((connectionsHints & ConnectionsHint.Scalar) == ConnectionsHint.Scalar) - { - var t = type.Type; - if (t == typeof(bool) || - t == typeof(char) || - t == typeof(byte) || - t == typeof(short) || - t == typeof(ushort) || - t == typeof(int) || - t == typeof(uint) || - t == typeof(long) || - t == typeof(ulong) || - t == typeof(float) || - t == typeof(double)) - { - return true; - } - } - } + if (VisjectSurface.IsTypeCompatible(Archetype.ConnectionsType, type, ParentNode.Archetype.ConnectionsHints)) + return true; // Check independent and if there is box with bigger potential because it may block current one from changing type var parentArch = ParentNode.Archetype; @@ -296,7 +246,7 @@ namespace FlaxEditor.Surface.Elements var b = ParentNode.GetBox(boxes[i]); // Check if its the same and tested type matches the default value type - if (b == this && CanCast(parentArch.DefaultType, type)) + if (b == this && parentArch.DefaultType.CanCastTo(type)) { // Can return true; @@ -718,17 +668,6 @@ namespace FlaxEditor.Surface.Elements } } - private static bool CanCast(ScriptType oB, ScriptType iB) - { - if (oB == iB) - return true; - if (oB == ScriptType.Null || iB == ScriptType.Null) - return false; - return (oB.Type != typeof(void) && oB.Type != typeof(FlaxEngine.Object)) && - (iB.Type != typeof(void) && iB.Type != typeof(FlaxEngine.Object)) && - oB.IsAssignableFrom(iB); - } - /// public bool AreConnected(IConnectionInstigator other) { @@ -787,7 +726,7 @@ namespace FlaxEditor.Surface.Elements { if (!iB.CanUseType(oB.CurrentType)) { - if (!CanCast(oB.CurrentType, iB.CurrentType)) + if (!oB.CurrentType.CanCastTo(iB.CurrentType)) { // Cannot return false; @@ -798,7 +737,7 @@ namespace FlaxEditor.Surface.Elements { if (!oB.CanUseType(iB.CurrentType)) { - if (!CanCast(oB.CurrentType, iB.CurrentType)) + if (!oB.CurrentType.CanCastTo(iB.CurrentType)) { // Cannot return false; @@ -871,7 +810,7 @@ namespace FlaxEditor.Surface.Elements bool useCaster = false; if (!iB.CanUseType(oB.CurrentType)) { - if (CanCast(oB.CurrentType, iB.CurrentType)) + if (oB.CurrentType.CanCastTo(iB.CurrentType)) useCaster = true; else return; diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index cf78b559c..8ef64ec77 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -35,7 +35,7 @@ namespace FlaxEditor.Surface.Elements { // Calculate control points CalculateBezierControlPoints(start, end, out var control1, out var control2); - + // Draw line Render2D.DrawBezier(start, control1, control2, end, color, thickness); @@ -54,16 +54,16 @@ namespace FlaxEditor.Surface.Elements const float maxControlLength = 150f; var dst = (end - start).Length; var yDst = Mathf.Abs(start.Y - end.Y); - + // Calculate control points var minControlDst = dst * 0.5f; var maxControlDst = Mathf.Max(Mathf.Min(maxControlLength, dst), minControlLength); var controlDst = Mathf.Lerp(minControlDst, maxControlDst, Mathf.Clamp(yDst / minControlLength, 0f, 1f)); - + control1 = new Float2(start.X + controlDst, start.Y); control2 = new Float2(end.X - controlDst, end.Y); } - + /// /// Checks if a point intersects a connection /// @@ -86,11 +86,13 @@ namespace FlaxEditor.Surface.Elements public static bool IntersectsConnection(ref Float2 start, ref Float2 end, ref Float2 point, float distance) { // Pretty much a point in rectangle check - if ((point.X - start.X) * (end.X - point.X) < 0) return false; + if ((point.X - start.X) * (end.X - point.X) < 0) + return false; float offset = Mathf.Sign(end.Y - start.Y) * distance; - if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) return false; - + if ((point.Y - (start.Y - offset)) * ((end.Y + offset) - point.Y) < 0) + return false; + float squaredDistance = distance; CalculateBezierControlPoints(start, end, out var control1, out var control2); diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 6dc923ce2..fe54268e2 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -89,6 +89,11 @@ namespace FlaxEditor.Surface /// The created node object. public delegate SurfaceNode CreateCustomNodeFunc(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch); + /// + /// Checks if the given type is compatible with the given node archetype. Used for custom nodes + /// + public delegate bool IsCompatible(NodeArchetype nodeArch, ScriptType portType, ConnectionsHint hint); + /// /// Unique node type ID within a single group. /// @@ -99,6 +104,16 @@ namespace FlaxEditor.Surface /// public CreateCustomNodeFunc Create; + /// + /// Function for asynchronously loaded nodes to check if input ports are compatible, for filtering. + /// + public IsCompatible IsInputCompatible; + + /// + /// Function for asynchronously loaded nodes to check if output ports are compatible, for filtering. + /// + public IsCompatible IsOutputCompatible; + /// /// Default initial size of the node. /// @@ -184,6 +199,8 @@ namespace FlaxEditor.Surface { TypeID = TypeID, Create = Create, + IsInputCompatible = IsInputCompatible, + IsOutputCompatible = IsOutputCompatible, Size = Size, Flags = Flags, Title = Title, diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 08b9c6d63..5f4c3ef07 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -406,8 +406,8 @@ namespace FlaxEditor.Surface internal static bool IsValidVisualScriptType(ScriptType scriptType) { - if (!scriptType.IsPublic || - scriptType.HasAttribute(typeof(HideInEditorAttribute), true) || + if (!scriptType.IsPublic || + scriptType.HasAttribute(typeof(HideInEditorAttribute), true) || scriptType.HasAttribute(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false)) return false; if (scriptType.IsGenericType) diff --git a/Source/Editor/Surface/VisjectSurface.Connecting.cs b/Source/Editor/Surface/VisjectSurface.Connecting.cs index 2fbff7cb8..dd34f9c7c 100644 --- a/Source/Editor/Surface/VisjectSurface.Connecting.cs +++ b/Source/Editor/Surface/VisjectSurface.Connecting.cs @@ -136,6 +136,88 @@ namespace FlaxEditor.Surface return result; } + /// + /// Checks if a type is compatible with another type and can be casted by using a connection hint + /// + /// Source type + /// Type to check compatibility with + /// Hint to check if casting is possible + /// True if the source type is compatible with the target type + public static bool IsTypeCompatible(ScriptType from, ScriptType to, ConnectionsHint hint) + { + if (from == ScriptType.Null && hint != ConnectionsHint.None) + { + if ((hint & ConnectionsHint.Anything) == ConnectionsHint.Anything) + return true; + if ((hint & ConnectionsHint.Value) == ConnectionsHint.Value && to.Type != typeof(void)) + return true; + if ((hint & ConnectionsHint.Enum) == ConnectionsHint.Enum && to.IsEnum) + return true; + if ((hint & ConnectionsHint.Array) == ConnectionsHint.Array && to.IsArray) + return true; + if ((hint & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary && to.IsDictionary) + return true; + if ((hint & ConnectionsHint.Vector) == ConnectionsHint.Vector) + { + var t = to.Type; + if (t == typeof(Vector2) || + t == typeof(Vector3) || + t == typeof(Vector4) || + t == typeof(Float2) || + t == typeof(Float3) || + t == typeof(Float4) || + t == typeof(Double2) || + t == typeof(Double3) || + t == typeof(Double4) || + t == typeof(Int2) || + t == typeof(Int3) || + t == typeof(Int4) || + t == typeof(Color)) + { + return true; + } + } + if ((hint & ConnectionsHint.Scalar) == ConnectionsHint.Scalar) + { + var t = to.Type; + if (t == typeof(bool) || + t == typeof(char) || + t == typeof(byte) || + t == typeof(short) || + t == typeof(ushort) || + t == typeof(int) || + t == typeof(uint) || + t == typeof(long) || + t == typeof(ulong) || + t == typeof(float) || + t == typeof(double)) + { + return true; + } + } + } + + return false; + } + + /// + /// Checks if a type is compatible with another type and can be casted by using a connection hint + /// + /// Source type + /// Target type + /// Connection hint + /// True if any method of casting or compatibility check succeeds + public static bool FullCastCheck(ScriptType from, ScriptType to, ConnectionsHint hint) + { + // Yes, from and to are switched on purpose + if (CanUseDirectCastStatic(to, from, false)) + return true; + if (IsTypeCompatible(from, to, hint)) + return true; + // Same here + return to.CanCastTo(from); + } + /// /// Checks if can use direct conversion from one type to another. ///