mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Nodes - Added object output pins and a new list predicate node
This commit is contained in:
1 parent
9106359515
commit
b19854ee47
21 files changed
+492
-143
No files matched your search
@@ -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<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
|
||||
{
|
||||
private readonly Dictionary<Func<DataModelEventArgs, object>, 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<Func<DataModelEventArgs, object>, OutputPin>();
|
||||
|
||||
_objectOutputPins = new ObjectOutputPins(this);
|
||||
|
||||
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
|
||||
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
||||
|
||||
@@ -48,20 +44,14 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
public override void Evaluate()
|
||||
{
|
||||
object? pathValue = _dataModelPath?.GetValue();
|
||||
|
||||
|
||||
// If the path is a data model event, evaluate the event
|
||||
if (pathValue is IDataModelEvent dataModelEvent)
|
||||
{
|
||||
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
||||
TriggerCount.Value = dataModelEvent.TriggerCount;
|
||||
|
||||
foreach ((Func<DataModelEventArgs, object> 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<DataModelPathEntity, DataModelEventNodeCu
|
||||
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_valueChangeCount++;
|
||||
_lastTrigger = DateTime.Now;
|
||||
|
||||
|
||||
_oldValuePin.Value = _lastValue;
|
||||
_newValuePin.Value = pathValue;
|
||||
|
||||
|
||||
_lastValue = pathValue;
|
||||
|
||||
TimeSinceLastTrigger.Value = 0;
|
||||
@@ -91,19 +81,20 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||
if (_dataModelPath != null)
|
||||
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
|
||||
|
||||
|
||||
if (old != null)
|
||||
{
|
||||
old.PathValidated -= DataModelPathOnPathValidated;
|
||||
old.Dispose();
|
||||
}
|
||||
|
||||
|
||||
UpdateOutputPins();
|
||||
}
|
||||
|
||||
private void UpdateOutputPins()
|
||||
{
|
||||
object? pathValue = _dataModelPath?.GetValue();
|
||||
|
||||
if (pathValue is IDataModelEvent dataModelEvent)
|
||||
CreateEventPins(dataModelEvent);
|
||||
else
|
||||
@@ -114,25 +105,10 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
{
|
||||
if (_dataModelEvent == dataModelEvent)
|
||||
return;
|
||||
|
||||
|
||||
ClearPins();
|
||||
_dataModelEvent = dataModelEvent;
|
||||
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<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(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<DataModelPathEntity, DataModelEventNodeCu
|
||||
Type? propertyType = _dataModelPath?.GetPropertyType();
|
||||
if (propertyType == null)
|
||||
return;
|
||||
|
||||
|
||||
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
||||
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
||||
_lastValue = null;
|
||||
_valueChangeCount = 0;
|
||||
}
|
||||
|
||||
|
||||
private void ClearPins()
|
||||
{
|
||||
List<IPin> 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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<ListOperator, ListOperatorNodeCustomViewModel>
|
||||
{
|
||||
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<IList>();
|
||||
InputValue = CreateInputPin<object>();
|
||||
|
||||
Ouput = CreateOutputPin<bool>();
|
||||
Output = CreateOutputPin<bool>();
|
||||
}
|
||||
|
||||
public InputPin<IList> InputList { get; }
|
||||
public InputPin<object> InputValue { get; }
|
||||
public OutputPin<bool> Ouput { get; }
|
||||
public OutputPin<bool> Output { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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<object>().Any(v => v.Equals(input));
|
||||
Output.Value = InputList.Value.Cast<object>().Any(v => v.Equals(input));
|
||||
else if (Storage == ListOperator.All)
|
||||
Ouput.Value = InputList.Value.Cast<object>().All(v => v.Equals(input));
|
||||
else if (Storage == ListOperator.All)
|
||||
Ouput.Value = InputList.Value.Cast<object>().All(v => !v.Equals(input));
|
||||
Output.Value = InputList.Value.Cast<object>().All(v => v.Equals(input));
|
||||
else if (Storage == ListOperator.None)
|
||||
Output.Value = InputList.Value.Cast<object>().All(v => !v.Equals(input));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ListOperatorEntity, ListOperatorPredicateNodeCustomViewModel>, 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<IList>();
|
||||
Output = CreateOutputPin<bool>();
|
||||
|
||||
InputList.PinConnected += InputListOnPinConnected;
|
||||
}
|
||||
|
||||
public InputPin<IList> InputList { get; }
|
||||
public OutputPin<bool> Output { get; }
|
||||
public NodeScript<bool>? Script { get; private set; }
|
||||
|
||||
public override void Initialize(INodeScript script)
|
||||
{
|
||||
Storage ??= new ListOperatorEntity();
|
||||
|
||||
lock (_scriptLock)
|
||||
{
|
||||
Script = Storage?.Script != null
|
||||
? new NodeScript<bool>("Is match", "Determines whether the current list item is a match", Storage.Script, script.Context)
|
||||
: new NodeScript<bool>("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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<object>().Any(EvaluateItem);
|
||||
else if (Storage.Operator == ListOperator.All)
|
||||
Output.Value = InputList.Value.Cast<object>().All(EvaluateItem);
|
||||
else if (Storage.Operator == ListOperator.None)
|
||||
Output.Value = InputList.Value.Cast<object>().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<IPin> e)
|
||||
{
|
||||
lock (_scriptLock)
|
||||
{
|
||||
UpdateStartNode();
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Script?.Dispose();
|
||||
Script = null;
|
||||
_startNode = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.List.Screens"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.VisualScripting.Nodes.List.Screens.ListOperatorPredicateNodeCustomView"
|
||||
x:DataType="screens:ListOperatorPredicateNodeCustomViewModel">
|
||||
<StackPanel Spacing="5">
|
||||
<shared:EnumComboBox Value="{CompiledBinding Operator}" Classes="condensed" HorizontalAlignment="Stretch"/>
|
||||
<Button HorizontalAlignment="Stretch" Classes="condensed" Command="{CompiledBinding OpenEditor}">Edit script</Button>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
+19
@@ -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<ListOperatorPredicateNodeCustomViewModel>
|
||||
{
|
||||
public ListOperatorPredicateNodeCustomView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
+43
@@ -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<Unit, Unit> 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<NodeScriptWindowViewModelBase, bool>(("nodeScript", _node.Script));
|
||||
_node.Script.Save();
|
||||
|
||||
_node.Storage ??= new ListOperatorEntity();
|
||||
_node.Storage.Script = _node.Script.Entity;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user