From b19854ee476a7f613799bcebf8c440567fc69e35 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 15 Sep 2022 21:10:22 +0200 Subject: [PATCH] Nodes - Added object output pins and a new list predicate node --- .../Profile/Conditions/EventCondition.cs | 1 - .../{Internal => }/DefaultNode.cs | 13 +- .../Internal/EventConditionEventStartNode.cs | 43 +----- .../EventConditionValueChangedStartNode.cs | 1 - .../Internal/IEventConditionNode.cs | 2 +- src/Artemis.Core/VisualScripting/Node.cs | 24 +-- .../VisualScripting/NodeScript.cs | 8 +- .../VisualScripting/ObjectOutputPins.cs | 142 ++++++++++++++++++ .../NodeScriptWindowViewModelBase.cs | 23 +++ src/Artemis.UI.Shared/Styles/Condensed.axaml | 7 + src/Artemis.UI/Ninject/UiModule.cs | 3 + .../Panels/Properties/PropertiesViewModel.cs | 29 ++-- .../NodeScriptWindowViewModel.cs | 6 +- .../Nodes/DataModel/DataModelEventNode.cs | 85 +++++------ .../Nodes/List/ListOperatorEntity.cs | 9 ++ .../Nodes/List/ListOperatorNode.cs | 18 +-- .../Nodes/List/ListOperatorPredicateNode.cs | 118 +++++++++++++++ .../List/ListOperatorPredicateStartNode.cs | 27 ++++ .../ListOperatorPredicateNodeCustomView.axaml | 14 ++ ...stOperatorPredicateNodeCustomView.axaml.cs | 19 +++ ...istOperatorPredicateNodeCustomViewModel.cs | 43 ++++++ 21 files changed, 492 insertions(+), 143 deletions(-) rename src/Artemis.Core/VisualScripting/{Internal => }/DefaultNode.cs (60%) create mode 100644 src/Artemis.Core/VisualScripting/ObjectOutputPins.cs create mode 100644 src/Artemis.UI.Shared/Services/NodeEditor/NodeScriptWindowViewModelBase.cs create mode 100644 src/Artemis.VisualScripting/Nodes/List/ListOperatorEntity.cs create mode 100644 src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateStartNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml create mode 100644 src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs create mode 100644 src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index d39a0f7f5..cc342e12c 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using Artemis.Core.Internal; -using Artemis.Core.VisualScripting.Internal; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; diff --git a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs b/src/Artemis.Core/VisualScripting/DefaultNode.cs similarity index 60% rename from src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs rename to src/Artemis.Core/VisualScripting/DefaultNode.cs index 103aa4e12..733b4e97b 100644 --- a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs +++ b/src/Artemis.Core/VisualScripting/DefaultNode.cs @@ -1,18 +1,11 @@ using System; -namespace Artemis.Core.Internal; - -/// -/// Represents a kind of node that cannot be deleted inside a . -/// -public interface IDefaultNode : INode -{ -} +namespace Artemis.Core; /// /// Represents a kind of node that cannot be deleted inside a . /// -public abstract class DefaultNode : Node, IDefaultNode +public abstract class DefaultNode : Node { #region Constructors @@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode protected DefaultNode(Guid id, string name, string description = "") : base(name, description) { Id = id; - Name = name; - Description = description; } #endregion diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs index 21a264caa..7ceadc837 100644 --- a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs @@ -1,23 +1,16 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Artemis.Core.Modules; -using Artemis.Core.VisualScripting.Internal; -using Humanizer; namespace Artemis.Core.Internal; internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode { internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305"); - private readonly Dictionary, OutputPin> _propertyPins; + private readonly ObjectOutputPins _objectOutputPins; private IDataModelEvent? _dataModelEvent; public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation") { - _propertyPins = new Dictionary, OutputPin>(); + _objectOutputPins = new ObjectOutputPins(this); } public void CreatePins(IDataModelEvent? dataModelEvent) @@ -25,30 +18,8 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (_dataModelEvent == dataModelEvent) return; - while (Pins.Any()) - RemovePin((Pin) Pins.First()); - _propertyPins.Clear(); - _dataModelEvent = dataModelEvent; - if (dataModelEvent == null) - return; - - foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute)))) - { - // Expect an IDataModelEvent - ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event"); - // Cast it to the actual event type - UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!); - // Access the property - MemberExpression accessor = Expression.Property(eventCast, propertyInfo); - // Cast the property to an object (sadly boxing) - UnaryExpression objectCast = Expression.Convert(accessor, typeof(object)); - // Compile the resulting expression - Func expression = Expression.Lambda>(objectCast, eventParameter).Compile(); - - _propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); - } + _objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType); } public override void Evaluate() @@ -56,12 +27,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (_dataModelEvent?.LastEventArgumentsUntyped == null) return; - foreach ((Func propertyAccessor, OutputPin outputPin) in _propertyPins) - { - if (!outputPin.ConnectedTo.Any()) - continue; - object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!; - outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; - } + _objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped); } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs index abf571979..2b98acbf6 100644 --- a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs @@ -1,5 +1,4 @@ using System; -using Artemis.Core.VisualScripting.Internal; namespace Artemis.Core.Internal; diff --git a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs b/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs index e47ad33a7..87d45f50a 100644 --- a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs @@ -1,4 +1,4 @@ -namespace Artemis.Core.VisualScripting.Internal; +namespace Artemis.Core.Internal; internal interface IEventConditionNode : INode { diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 7e2161580..70e02fa12 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -132,7 +132,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin /// The type of value the pin will hold /// The newly created pin - protected InputPin CreateInputPin(string name = "") + public InputPin CreateInputPin(string name = "") { InputPin pin = new(this, name); _pins.Add(pin); @@ -146,7 +146,7 @@ public abstract class Node : BreakableModel, INode /// The type of value the pin will hold /// The name of the pin /// The newly created pin - protected InputPin CreateInputPin(Type type, string name = "") + public InputPin CreateInputPin(Type type, string name = "") { InputPin pin = new(this, type, name); _pins.Add(pin); @@ -160,7 +160,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin /// The type of value the pin will hold /// The newly created pin - protected OutputPin CreateOutputPin(string name = "") + public OutputPin CreateOutputPin(string name = "") { OutputPin pin = new(this, name); _pins.Add(pin); @@ -174,7 +174,7 @@ public abstract class Node : BreakableModel, INode /// The type of value the pin will hold /// The name of the pin /// The newly created pin - protected OutputPin CreateOutputPin(Type type, string name = "") + public OutputPin CreateOutputPin(Type type, string name = "") { OutputPin pin = new(this, type, name); _pins.Add(pin); @@ -187,7 +187,7 @@ public abstract class Node : BreakableModel, INode /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// - protected OutputPin CreateOrAddOutputPin(Type valueType, string displayName) + public OutputPin CreateOrAddOutputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); @@ -217,7 +217,7 @@ public abstract class Node : BreakableModel, INode /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// - protected InputPin CreateOrAddInputPin(Type valueType, string displayName) + public InputPin CreateOrAddInputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); @@ -247,7 +247,7 @@ public abstract class Node : BreakableModel, INode /// /// The pin to remove /// if the pin was removed; otherwise . - protected bool RemovePin(Pin pin) + public bool RemovePin(Pin pin) { bool isRemoved = _pins.Remove(pin); if (isRemoved) @@ -263,7 +263,7 @@ public abstract class Node : BreakableModel, INode /// Adds an existing to the collection. /// /// The pin to add - protected void AddPin(Pin pin) + public void AddPin(Pin pin) { if (pin.Node != this) throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to."); @@ -281,7 +281,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) + public InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) { InputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); @@ -296,7 +296,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) + public InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) { InputPinCollection pin = new(this, type, name, initialCount); _pinCollections.Add(pin); @@ -311,7 +311,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting output pin collection - protected OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) + public OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) { OutputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); @@ -325,7 +325,7 @@ public abstract class Node : BreakableModel, INode /// /// The pin collection to remove /// if the pin collection was removed; otherwise . - protected bool RemovePinCollection(PinCollection pinCollection) + public bool RemovePinCollection(PinCollection pinCollection) { bool isRemoved = _pinCollections.Remove(pinCollection); if (isRemoved) diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 1c77626f8..e98dafcf3 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript #region Properties & Fields - internal NodeScriptEntity Entity { get; private set; } + /// + /// Gets the entity used to store this script. + /// + public NodeScriptEntity Entity { get; private set; } /// public string Name { get; } @@ -410,7 +413,8 @@ public class NodeScript : NodeScript, INodeScript #region Constructors - internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) + /// + public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) : base(name, description, entity, context) { ExitNode = new ExitNode(name, description); diff --git a/src/Artemis.Core/VisualScripting/ObjectOutputPins.cs b/src/Artemis.Core/VisualScripting/ObjectOutputPins.cs new file mode 100644 index 000000000..fb9c874f7 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/ObjectOutputPins.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Artemis.Core.Modules; +using Humanizer; + +namespace Artemis.Core; + +/// +/// Represents a collection of output pins for a node capable of outputting the properties of an object or value type. +/// +public class ObjectOutputPins +{ + private readonly Dictionary, OutputPin> _propertyPins; + private OutputPin? _valueTypePin; + + /// + /// Creates an instance of the class. + /// + /// The node the object output was created for. + public ObjectOutputPins(Node node) + { + Node = node; + _propertyPins = new Dictionary, OutputPin>(); + } + + /// + /// Gets the node the object output was created for. + /// + public Node Node { get; } + + /// + /// Gets the current type the node's pins are set up for. + /// + public Type? CurrentType { get; private set; } + + /// + /// Gets a read only collection of the pins outputting the object of this object node. + /// + public ReadOnlyCollection Pins => _valueTypePin != null ? new ReadOnlyCollection(new List {_valueTypePin}) : _propertyPins.Values.ToList().AsReadOnly(); + + /// + /// Change the current type and create pins on the node to reflect this. + /// + /// The type to change the collection to. + public void ChangeType(Type? type) + { + if (type == CurrentType) + return; + CurrentType = type; + + // Remove current pins + foreach ((Func? _, OutputPin? pin) in _propertyPins) + Node.RemovePin(pin); + _propertyPins.Clear(); + if (_valueTypePin != null) + { + Node.RemovePin(_valueTypePin); + _valueTypePin = null; + } + + if (type == null) + return; + + // Create new pins + List nodeTypeColors = NodeTypeStore.GetColors(); + if (type.IsClass && type != typeof(string)) + foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + Type propertyType = propertyInfo.PropertyType; + bool toNumeric = Numeric.IsTypeCompatible(propertyType); + + // Skip ignored properties + if (propertyInfo.CustomAttributes.Any(a => a.AttributeType == typeof(DataModelIgnoreAttribute))) + continue; + // Skip incompatible properties + if (!toNumeric && !nodeTypeColors.Any(c => c.Type.IsAssignableFrom(propertyType))) + continue; + + // Expect an object + ParameterExpression itemParameter = Expression.Parameter(typeof(object), "item"); + // Cast it to the actual item type + UnaryExpression itemCast = Expression.Convert(itemParameter, propertyInfo.DeclaringType!); + // Access the property + MemberExpression accessor = Expression.Property(itemCast, propertyInfo); + + // Turn into a numeric if needed or access directly + UnaryExpression objectExpression; + if (toNumeric) + { + propertyType = typeof(Numeric); + ConstructorInfo constructor = typeof(Numeric).GetConstructors().First(c => c.GetParameters().First().ParameterType == propertyInfo.PropertyType); + // Cast the property to an object (sadly boxing) + objectExpression = Expression.Convert(Expression.New(constructor, accessor), typeof(object)); + } + else + { + // Cast the property to an object (sadly boxing) + objectExpression = Expression.Convert(accessor, typeof(object)); + } + + // Compile the resulting expression + Func expression = Expression.Lambda>(objectExpression, itemParameter).Compile(); + _propertyPins.Add(expression, Node.CreateOrAddOutputPin(propertyType, propertyInfo.Name.Humanize())); + } + else + // Value types are applied directly to a single pin, however if the type is compatible with Numeric, we use a Numeric pin instead + // the value will then be turned into a numeric in SetCurrentValue + _valueTypePin = Node.CreateOrAddOutputPin(Numeric.IsTypeCompatible(type) ? typeof(Numeric) : type, "Item"); + } + + /// + /// Set the current value to be output onto connected pins. + /// + /// The value to output onto the connected pins. + /// + public void SetCurrentValue(object? value) + { + if (CurrentType == null) + throw new ArtemisCoreException("Cannot apply a value to an object output pins not yet configured for a type."); + if (value != null && CurrentType != value.GetType()) + throw new ArtemisCoreException($"Cannot apply a value of type {value.GetType().FullName} to an object output pins configured for type {CurrentType.FullName}"); + + // Apply the object to the pin, it must be connected if SetCurrentValue got called + if (_valueTypePin != null) + { + value ??= _valueTypePin.Type.GetDefault(); + _valueTypePin.Value = _valueTypePin.Type == typeof(Numeric) ? new Numeric(value) : value; + return; + } + + // Apply the properties of the object to each connected pin + foreach ((Func? propertyAccessor, OutputPin? outputPin) in _propertyPins) + { + if (outputPin.ConnectedTo.Any()) + outputPin.Value = value != null ? propertyAccessor(value) : outputPin.Type.GetDefault(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/NodeEditor/NodeScriptWindowViewModelBase.cs b/src/Artemis.UI.Shared/Services/NodeEditor/NodeScriptWindowViewModelBase.cs new file mode 100644 index 000000000..5d621f796 --- /dev/null +++ b/src/Artemis.UI.Shared/Services/NodeEditor/NodeScriptWindowViewModelBase.cs @@ -0,0 +1,23 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor; + +/// +/// Represents the base of the node script editor window view model. +/// +public abstract class NodeScriptWindowViewModelBase : DialogViewModelBase +{ + /// + /// Creates a new instance of the class. + /// + /// The node script being edited. + protected NodeScriptWindowViewModelBase(NodeScript nodeScript) + { + NodeScript = nodeScript; + } + + /// + /// Gets the node script being edited. + /// + public NodeScript NodeScript { get; init; } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Styles/Condensed.axaml b/src/Artemis.UI.Shared/Styles/Condensed.axaml index 62a6c8fb9..9ceb0c926 100644 --- a/src/Artemis.UI.Shared/Styles/Condensed.axaml +++ b/src/Artemis.UI.Shared/Styles/Condensed.axaml @@ -103,4 +103,11 @@ + + + \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/UiModule.cs b/src/Artemis.UI/Ninject/UiModule.cs index 0c9816eb7..41a355fbb 100644 --- a/src/Artemis.UI/Ninject/UiModule.cs +++ b/src/Artemis.UI/Ninject/UiModule.cs @@ -2,8 +2,10 @@ using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.InstanceProviders; using Artemis.UI.Screens; +using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Platform; using Avalonia.Shared.PlatformSupport; @@ -57,6 +59,7 @@ public class UIModule : NinjectModule .BindToFactory(); }); + Kernel.Bind().To(); Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider()); // Bind all UI services as singletons diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs index 0c172200c..6471d49c8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesViewModel.cs @@ -76,18 +76,23 @@ public class PropertiesViewModel : ActivatableViewModelBase }); // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 - this.WhenAnyValue(vm => vm.ProfileElement) - .Select(p => p is Layer l - ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) - : Observable.Never>()) - .Switch() - .Subscribe(_ => UpdatePropertyGroups()); - this.WhenAnyValue(vm => vm.ProfileElement) - .Select(p => p != null - ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) - : Observable.Never>()) - .Switch() - .Subscribe(_ => UpdatePropertyGroups()); + this.WhenActivated(d => + { + this.WhenAnyValue(vm => vm.ProfileElement) + .Select(p => p is Layer l + ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => UpdatePropertyGroups()) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.ProfileElement) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => UpdatePropertyGroups()) + .DisposeWith(d); + }); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups()); this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel()); } diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs index 19eefefb8..911835000 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; @@ -20,7 +19,7 @@ using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; -public class NodeScriptWindowViewModel : DialogViewModelBase +public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase { private readonly INodeEditorService _nodeEditorService; private readonly INodeService _nodeService; @@ -34,7 +33,7 @@ public class NodeScriptWindowViewModel : DialogViewModelBase INodeVmFactory vmFactory, ISettingsService settingsService, IProfileService profileService, - IWindowService windowService) + IWindowService windowService) : base(nodeScript) { NodeScript = nodeScript; NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false); @@ -77,7 +76,6 @@ public class NodeScriptWindowViewModel : DialogViewModelBase }); } - public NodeScript NodeScript { get; } public NodeScriptViewModel NodeScriptViewModel { get; set; } public NodeEditorHistory History { get; } diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs index 054977e8b..9dec85f4c 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs @@ -1,29 +1,25 @@ -using System.Linq.Expressions; -using System.Reflection; -using Artemis.Core; -using Artemis.Core.Modules; +using Artemis.Core; using Artemis.Storage.Entities.Profile; using Artemis.VisualScripting.Nodes.DataModel.Screens; -using Humanizer; namespace Artemis.VisualScripting.Nodes.DataModel; [Node("Data Model-Event", "Outputs the latest values of a data model event.", "Data Model", OutputType = typeof(object))] public class DataModelEventNode : Node, IDisposable { - private readonly Dictionary, OutputPin> _propertyPins; - private DataModelPath? _dataModelPath; + private readonly ObjectOutputPins _objectOutputPins; private IDataModelEvent? _dataModelEvent; - private OutputPin? _oldValuePin; - private OutputPin? _newValuePin; + private DataModelPath? _dataModelPath; private DateTime _lastTrigger; private object? _lastValue; + private OutputPin? _newValuePin; + private OutputPin? _oldValuePin; private int _valueChangeCount; public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.") { - _propertyPins = new Dictionary, OutputPin>(); - + _objectOutputPins = new ObjectOutputPins(this); + TimeSinceLastTrigger = CreateOutputPin("Time since trigger"); TriggerCount = CreateOutputPin("Trigger count"); @@ -48,20 +44,14 @@ public class DataModelEventNode : Node propertyAccessor, OutputPin outputPin) in _propertyPins) - { - if (!outputPin.ConnectedTo.Any()) - continue; - object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!; - outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; - } + _objectOutputPins.SetCurrentValue(dataModelEvent.LastEventArgumentsUntyped); } // If the path is a regular value, evaluate the current value else if (_oldValuePin != null && _newValuePin != null) @@ -71,13 +61,13 @@ public class DataModelEventNode : Node p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute)))) - { - // Expect an IDataModelEvent - ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event"); - // Cast it to the actual event type - UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!); - // Access the property - MemberExpression accessor = Expression.Property(eventCast, propertyInfo); - // Cast the property to an object (sadly boxing) - UnaryExpression objectCast = Expression.Convert(accessor, typeof(object)); - // Compile the resulting expression - Func expression = Expression.Lambda>(objectCast, eventParameter).Compile(); - - _propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); - } + _objectOutputPins.ChangeType(dataModelEvent.ArgumentsType); } private void CreateValuePins() @@ -142,24 +118,31 @@ public class DataModelEventNode : Node pins = Pins.Skip(2).ToList(); - foreach (IPin pin in pins) - RemovePin((Pin) pin); - - _propertyPins.Clear(); - _oldValuePin = null; - _newValuePin = null; + // Clear the output pins by changing the type to null + _objectOutputPins.ChangeType(null); + + if (_oldValuePin != null) + { + RemovePin(_oldValuePin); + _oldValuePin = null; + } + + if (_newValuePin != null) + { + RemovePin(_newValuePin); + _newValuePin = null; + } } - + private void DataModelPathOnPathValidated(object? sender, EventArgs e) { // Update the output pin now that the type is known and attempt to restore the connection that was likely missing diff --git a/src/Artemis.VisualScripting/Nodes/List/ListOperatorEntity.cs b/src/Artemis.VisualScripting/Nodes/List/ListOperatorEntity.cs new file mode 100644 index 000000000..515f8be2e --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/ListOperatorEntity.cs @@ -0,0 +1,9 @@ +using Artemis.Storage.Entities.Profile.Nodes; + +namespace Artemis.VisualScripting.Nodes.List; + +public class ListOperatorEntity +{ + public NodeScriptEntity? Script { get; set; } + public ListOperator Operator { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/List/ListOperatorNode.cs b/src/Artemis.VisualScripting/Nodes/List/ListOperatorNode.cs index 7a7d4e056..29ee2391a 100644 --- a/src/Artemis.VisualScripting/Nodes/List/ListOperatorNode.cs +++ b/src/Artemis.VisualScripting/Nodes/List/ListOperatorNode.cs @@ -4,37 +4,37 @@ using Artemis.VisualScripting.Nodes.List.Screens; namespace Artemis.VisualScripting.Nodes.List; -[Node("List Operator (Simple)", "Checks if any/all/no value in the input list matches the input value", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))] +[Node("List Operator (Simple)", "Checks if any/all/no values in the input list match the input value", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))] public class ListOperatorNode : Node { - public ListOperatorNode() : base("List Operator", "Checks if any/all/no value in the input list matches the input value") + public ListOperatorNode() : base("List Operator (Simple)", "Checks if any/all/no values in the input list match the input value") { InputList = CreateInputPin(); InputValue = CreateInputPin(); - Ouput = CreateOutputPin(); + Output = CreateOutputPin(); } public InputPin InputList { get; } public InputPin InputValue { get; } - public OutputPin Ouput { get; } + public OutputPin Output { get; } /// public override void Evaluate() { if (InputList.Value == null) { - Ouput.Value = Storage == ListOperator.None; + Output.Value = Storage == ListOperator.None; return; } object? input = InputValue.Value; if (Storage == ListOperator.Any) - Ouput.Value = InputList.Value.Cast().Any(v => v.Equals(input)); + Output.Value = InputList.Value.Cast().Any(v => v.Equals(input)); else if (Storage == ListOperator.All) - Ouput.Value = InputList.Value.Cast().All(v => v.Equals(input)); - else if (Storage == ListOperator.All) - Ouput.Value = InputList.Value.Cast().All(v => !v.Equals(input)); + Output.Value = InputList.Value.Cast().All(v => v.Equals(input)); + else if (Storage == ListOperator.None) + Output.Value = InputList.Value.Cast().All(v => !v.Equals(input)); } } diff --git a/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs new file mode 100644 index 000000000..571eedc83 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateNode.cs @@ -0,0 +1,118 @@ +using System.Collections; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.VisualScripting.Nodes.List.Screens; + +namespace Artemis.VisualScripting.Nodes.List; + +[Node("List Operator (Advanced)", "Checks if any/all/no values in the input list match a condition", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))] +public class ListOperatorPredicateNode : Node, IDisposable +{ + private readonly object _scriptLock = new(); + private ListOperatorPredicateStartNode? _startNode; + + public ListOperatorPredicateNode() : base("List Operator (Advanced)", "Checks if any/all/no values in the input list match a condition") + { + + InputList = CreateInputPin(); + Output = CreateOutputPin(); + + InputList.PinConnected += InputListOnPinConnected; + } + + public InputPin InputList { get; } + public OutputPin Output { get; } + public NodeScript? Script { get; private set; } + + public override void Initialize(INodeScript script) + { + Storage ??= new ListOperatorEntity(); + + lock (_scriptLock) + { + Script = Storage?.Script != null + ? new NodeScript("Is match", "Determines whether the current list item is a match", Storage.Script, script.Context) + : new NodeScript("Is match", "Determines whether the current list item is a match", script.Context); + + // The load action may have created an event node, use that one over the one we have here + INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == ListOperatorPredicateStartNode.NodeId); + if (existingEventNode != null) + _startNode = (ListOperatorPredicateStartNode) existingEventNode; + else + { + _startNode = new ListOperatorPredicateStartNode {X = -200}; + Script.AddNode(_startNode); + } + + UpdateStartNode(); + Script.LoadConnections(); + } + } + + /// + public override void Evaluate() + { + if (Storage == null) + return; + + if (InputList.Value == null) + { + Output.Value = Storage.Operator == ListOperator.None; + return; + } + + lock (_scriptLock) + { + if (Script == null) + return; + + if (Storage.Operator == ListOperator.Any) + Output.Value = InputList.Value.Cast().Any(EvaluateItem); + else if (Storage.Operator == ListOperator.All) + Output.Value = InputList.Value.Cast().All(EvaluateItem); + else if (Storage.Operator == ListOperator.None) + Output.Value = InputList.Value.Cast().All(v => !EvaluateItem(v)); + } + } + + private bool EvaluateItem(object item) + { + if (Script == null || _startNode == null) + return false; + + _startNode.Item = item; + Script.Run(); + return Script.Result; + } + + private void UpdateStartNode() + { + Type? type = InputList.ConnectedTo.FirstOrDefault()?.Type; + // List must be generic or there's no way to tell what objects it contains in advance, that's not supported for now + if (type is not {IsGenericType: true}) + return; + + Type listType = type.GetGenericArguments().Single(); + _startNode?.ChangeType(listType); + } + + private void InputListOnPinConnected(object? sender, SingleValueEventArgs e) + { + lock (_scriptLock) + { + UpdateStartNode(); + } + } + + #region IDisposable + + /// + public void Dispose() + { + Script?.Dispose(); + Script = null; + _startNode = null; + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateStartNode.cs b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateStartNode.cs new file mode 100644 index 000000000..ca37fb237 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/ListOperatorPredicateStartNode.cs @@ -0,0 +1,27 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.List; + +public class ListOperatorPredicateStartNode : DefaultNode +{ + internal static readonly Guid NodeId = new("9A714CF3-8D02-4CC3-A1AC-73833F82D7C6"); + private readonly ObjectOutputPins _objectOutputPins; + + public ListOperatorPredicateStartNode() : base(NodeId, "List item", "Contains the current list item") + { + _objectOutputPins = new ObjectOutputPins(this); + } + + public object? Item { get; set; } + + public override void Evaluate() + { + if (Item != null) + _objectOutputPins.SetCurrentValue(Item); + } + + public void ChangeType(Type? type) + { + _objectOutputPins.ChangeType(type); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml new file mode 100644 index 000000000..dbc6728a7 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs new file mode 100644 index 000000000..ab950da39 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.List.Screens; + +public partial class ListOperatorPredicateNodeCustomView : ReactiveUserControl +{ + public ListOperatorPredicateNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs new file mode 100644 index 000000000..5add4895d --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/List/Screens/ListOperatorPredicateNodeCustomViewModel.cs @@ -0,0 +1,43 @@ +using System.Reactive; +using Artemis.Core; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.VisualScripting; +using ReactiveUI; + +namespace Artemis.VisualScripting.Nodes.List.Screens; + +public class ListOperatorPredicateNodeCustomViewModel : CustomNodeViewModel +{ + private readonly ListOperatorPredicateNode _node; + private readonly IWindowService _windowService; + private ListOperator _operator; + + public ListOperatorPredicateNodeCustomViewModel(ListOperatorPredicateNode node, INodeScript script, IWindowService windowService) : base(node, script) + { + _node = node; + _windowService = windowService; + + OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor); + } + + public ReactiveCommand OpenEditor { get; } + + public ListOperator Operator + { + get => _operator; + set => this.RaiseAndSetIfChanged(ref _operator, value); + } + + private async Task ExecuteOpenEditor() + { + if (_node.Script == null) + return; + + await _windowService.ShowDialogAsync(("nodeScript", _node.Script)); + _node.Script.Save(); + + _node.Storage ??= new ListOperatorEntity(); + _node.Storage.Script = _node.Script.Entity; + } +} \ No newline at end of file