mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Nodes - Fixed string format node not outputting numerics
Data model event node - Renamed to Data Model-Event Value Cycle Data model event node - Added new data model event node that outputs the latest event data Event conditions - Performance improvements
This commit is contained in:
parent
3efb97eedd
commit
d2e0607622
@ -146,6 +146,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
valueChangedNode.UpdateOutputPins(EventPath);
|
valueChangedNode.UpdateOutputPins(EventPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Script.Nodes.Contains(_startNode))
|
||||||
|
Script.AddNode(_startNode);
|
||||||
Script.Save();
|
Script.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,14 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
|
|||||||
{
|
{
|
||||||
_value = value;
|
_value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="Numeric" /> from a <see cref="long" />
|
||||||
|
/// </summary>
|
||||||
|
public Numeric(long value)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of <see cref="Numeric" /> from an <see cref="object" />
|
/// Creates a new instance of <see cref="Numeric" /> from an <see cref="object" />
|
||||||
@ -60,6 +68,7 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
|
|||||||
int value => value,
|
int value => value,
|
||||||
double value => (float) value,
|
double value => (float) value,
|
||||||
byte value => value,
|
byte value => value,
|
||||||
|
long value => value,
|
||||||
Numeric value => value,
|
Numeric value => value,
|
||||||
_ => ParseFloatOrDefault(pathValue?.ToString())
|
_ => ParseFloatOrDefault(pathValue?.ToString())
|
||||||
};
|
};
|
||||||
@ -150,6 +159,11 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
|
|||||||
{
|
{
|
||||||
return (byte) Math.Clamp(p._value, 0, 255);
|
return (byte) Math.Clamp(p._value, 0, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static implicit operator long(Numeric p)
|
||||||
|
{
|
||||||
|
return (long) p._value;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool operator >(Numeric a, Numeric b)
|
public static bool operator >(Numeric a, Numeric b)
|
||||||
{
|
{
|
||||||
@ -264,6 +278,7 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
|
|||||||
type == typeof(float) ||
|
type == typeof(float) ||
|
||||||
type == typeof(double) ||
|
type == typeof(double) ||
|
||||||
type == typeof(int) ||
|
type == typeof(int) ||
|
||||||
|
type == typeof(long) ||
|
||||||
type == typeof(byte);
|
type == typeof(byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Artemis.Core.Modules;
|
using Artemis.Core.Modules;
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
using Artemis.Core.VisualScripting.Internal;
|
||||||
@ -11,13 +12,12 @@ namespace Artemis.Core.Internal;
|
|||||||
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
||||||
{
|
{
|
||||||
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
|
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
|
||||||
private readonly List<OutputPin> _pinBucket = new();
|
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
||||||
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
|
|
||||||
private IDataModelEvent? _dataModelEvent;
|
private IDataModelEvent? _dataModelEvent;
|
||||||
|
|
||||||
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
|
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
|
||||||
{
|
{
|
||||||
_propertyPins = new Dictionary<PropertyInfo, OutputPin>();
|
_propertyPins = new Dictionary<Func<DataModelEventArgs, object>, OutputPin>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreatePins(IDataModelEvent? dataModelEvent)
|
public void CreatePins(IDataModelEvent? dataModelEvent)
|
||||||
@ -33,40 +33,22 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (dataModelEvent == null)
|
if (dataModelEvent == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType
|
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
|
||||||
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
|
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
|
||||||
_propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates or adds an input pin to the node using a bucket.
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
private OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
|
|
||||||
{
|
|
||||||
// Grab the first pin from the bucket that isn't on the node yet
|
|
||||||
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
|
||||||
|
|
||||||
if (Numeric.IsTypeCompatible(valueType))
|
|
||||||
valueType = typeof(Numeric);
|
|
||||||
|
|
||||||
// If there is none, create a new one and add it to the bucket
|
|
||||||
if (pin == null)
|
|
||||||
{
|
{
|
||||||
pin = CreateOutputPin(valueType, displayName);
|
// Expect an IDataModelEvent
|
||||||
_pinBucket.Add(pin);
|
ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event");
|
||||||
}
|
// Cast it to the actual event type
|
||||||
// If there was a pin in the bucket, update it's type and display name and reuse it
|
UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!);
|
||||||
else
|
// Access the property
|
||||||
{
|
MemberExpression accessor = Expression.Property(eventCast, propertyInfo);
|
||||||
pin.ChangeType(valueType);
|
// Cast the property to an object (sadly boxing)
|
||||||
pin.Name = displayName;
|
UnaryExpression objectCast = Expression.Convert(accessor, typeof(object));
|
||||||
AddPin(pin);
|
// Compile the resulting expression
|
||||||
}
|
Func<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(objectCast, eventParameter).Compile();
|
||||||
|
|
||||||
return pin;
|
_propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
@ -74,13 +56,12 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
|
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
||||||
{
|
{
|
||||||
if (outputPin.ConnectedTo.Any())
|
if (!outputPin.ConnectedTo.Any())
|
||||||
{
|
continue;
|
||||||
object value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!;
|
object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
||||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using Ninject;
|
using Ninject;
|
||||||
using Ninject.Parameters;
|
using Ninject.Parameters;
|
||||||
|
|
||||||
@ -15,6 +16,9 @@ public abstract class Node : CorePropertyChanged, INode
|
|||||||
public event EventHandler? Resetting;
|
public event EventHandler? Resetting;
|
||||||
|
|
||||||
#region Properties & Fields
|
#region Properties & Fields
|
||||||
|
|
||||||
|
private readonly List<OutputPin> _outputPinBucket = new();
|
||||||
|
private readonly List<InputPin> _inputPinBucket = new();
|
||||||
|
|
||||||
private Guid _id;
|
private Guid _id;
|
||||||
|
|
||||||
@ -160,6 +164,66 @@ public abstract class Node : CorePropertyChanged, INode
|
|||||||
OnPropertyChanged(nameof(Pins));
|
OnPropertyChanged(nameof(Pins));
|
||||||
return pin;
|
return pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or adds an output pin to the node using a bucket.
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
protected 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));
|
||||||
|
|
||||||
|
if (Numeric.IsTypeCompatible(valueType))
|
||||||
|
valueType = typeof(Numeric);
|
||||||
|
|
||||||
|
// If there is none, create a new one and add it to the bucket
|
||||||
|
if (pin == null)
|
||||||
|
{
|
||||||
|
pin = CreateOutputPin(valueType, displayName);
|
||||||
|
_outputPinBucket.Add(pin);
|
||||||
|
}
|
||||||
|
// If there was a pin in the bucket, update it's type and display name and reuse it
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pin.ChangeType(valueType);
|
||||||
|
pin.Name = displayName;
|
||||||
|
AddPin(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or adds an input pin to the node using a bucket.
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
protected 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));
|
||||||
|
|
||||||
|
if (Numeric.IsTypeCompatible(valueType))
|
||||||
|
valueType = typeof(Numeric);
|
||||||
|
|
||||||
|
// If there is none, create a new one and add it to the bucket
|
||||||
|
if (pin == null)
|
||||||
|
{
|
||||||
|
pin = CreateInputPin(valueType, displayName);
|
||||||
|
_inputPinBucket.Add(pin);
|
||||||
|
}
|
||||||
|
// If there was a pin in the bucket, update it's type and display name and reuse it
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pin.ChangeType(valueType);
|
||||||
|
pin.Name = displayName;
|
||||||
|
AddPin(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the provided <paramref name="pin" /> from the node and it's <see cref="Pins" /> collection
|
/// Removes the provided <paramref name="pin" /> from the node and it's <see cref="Pins" /> collection
|
||||||
|
|||||||
@ -14,7 +14,7 @@ namespace Artemis.UI.Services;
|
|||||||
|
|
||||||
public class UpdateService : IUpdateService
|
public class UpdateService : IUpdateService
|
||||||
{
|
{
|
||||||
private const double UPDATE_CHECK_INTERVAL = 10000; // once per hour
|
private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour
|
||||||
|
|
||||||
private readonly PluginSetting<bool> _autoUpdate;
|
private readonly PluginSetting<bool> _autoUpdate;
|
||||||
private readonly PluginSetting<bool> _checkForUpdates;
|
private readonly PluginSetting<bool> _checkForUpdates;
|
||||||
|
|||||||
@ -33,6 +33,10 @@
|
|||||||
<Compile Update="Nodes\DataModel\Screens\DataModelNodeCustomView.axaml.cs">
|
<Compile Update="Nodes\DataModel\Screens\DataModelNodeCustomView.axaml.cs">
|
||||||
<DependentUpon>DataModelNodeCustomView.axaml</DependentUpon>
|
<DependentUpon>DataModelNodeCustomView.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Nodes\DataModel\Screens\DataModelEventNodeCustomView.axaml.cs">
|
||||||
|
<DependentUpon>DataModelEventNodeCustomView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -0,0 +1,167 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Events;
|
||||||
|
using Artemis.Storage.Entities.Profile;
|
||||||
|
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.DataModel;
|
||||||
|
|
||||||
|
[Node("Data Model-Event Value Cycle", "Cycles through provided values each time the select event fires.", "Data Model", OutputType = typeof(object))]
|
||||||
|
public class DataModelEventCycleNode : Node<DataModelPathEntity, DataModelEventCycleNodeCustomViewModel>, IDisposable
|
||||||
|
{
|
||||||
|
private int _currentIndex;
|
||||||
|
private Type _currentType;
|
||||||
|
private DataModelPath? _dataModelPath;
|
||||||
|
private object? _lastPathValue;
|
||||||
|
private DateTime _lastTrigger;
|
||||||
|
private bool _updating;
|
||||||
|
|
||||||
|
public DataModelEventCycleNode() : base("Data Model-Event Value Cycle", "Cycles through provided values each time the select event fires.")
|
||||||
|
{
|
||||||
|
_currentType = typeof(object);
|
||||||
|
|
||||||
|
CycleValues = CreateInputPinCollection(typeof(object), "", 0);
|
||||||
|
Output = CreateOutputPin(typeof(object));
|
||||||
|
|
||||||
|
CycleValues.PinAdded += CycleValuesOnPinAdded;
|
||||||
|
CycleValues.PinRemoved += CycleValuesOnPinRemoved;
|
||||||
|
CycleValues.Add(CycleValues.CreatePin());
|
||||||
|
|
||||||
|
// Monitor storage for changes
|
||||||
|
StorageModified += (_, _) => UpdateDataModelPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public INodeScript? Script { get; set; }
|
||||||
|
|
||||||
|
public InputPinCollection CycleValues { get; }
|
||||||
|
public OutputPin Output { get; }
|
||||||
|
|
||||||
|
public override void Initialize(INodeScript script)
|
||||||
|
{
|
||||||
|
Script = script;
|
||||||
|
|
||||||
|
if (Storage == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdateDataModelPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Evaluate()
|
||||||
|
{
|
||||||
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
|
bool hasTriggered = pathValue is IDataModelEvent dataModelEvent
|
||||||
|
? EvaluateEvent(dataModelEvent)
|
||||||
|
: EvaluateValue(pathValue);
|
||||||
|
|
||||||
|
if (hasTriggered)
|
||||||
|
{
|
||||||
|
_currentIndex++;
|
||||||
|
|
||||||
|
if (_currentIndex >= CycleValues.Count())
|
||||||
|
_currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
object? outputValue = CycleValues.ElementAt(_currentIndex).PinValue;
|
||||||
|
if (Output.Type.IsInstanceOfType(outputValue))
|
||||||
|
Output.Value = outputValue;
|
||||||
|
else if (Output.Type.IsValueType)
|
||||||
|
Output.Value = Output.Type.GetDefault()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EvaluateEvent(IDataModelEvent dataModelEvent)
|
||||||
|
{
|
||||||
|
if (dataModelEvent.LastTrigger <= _lastTrigger)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_lastTrigger = dataModelEvent.LastTrigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EvaluateValue(object? pathValue)
|
||||||
|
{
|
||||||
|
if (Equals(pathValue, _lastPathValue))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_lastPathValue = pathValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CycleValuesOnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
e.Value.PinConnected += OnPinConnected;
|
||||||
|
e.Value.PinDisconnected += OnPinDisconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
e.Value.PinConnected -= OnPinConnected;
|
||||||
|
e.Value.PinDisconnected -= OnPinDisconnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
ProcessPinDisconnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
ProcessPinConnected(e.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessPinConnected(IPin source)
|
||||||
|
{
|
||||||
|
if (_updating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_updating = true;
|
||||||
|
|
||||||
|
// No need to change anything if the types haven't changed
|
||||||
|
if (_currentType != source.Type)
|
||||||
|
ChangeCurrentType(source.Type);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDataModelPath()
|
||||||
|
{
|
||||||
|
DataModelPath? old = _dataModelPath;
|
||||||
|
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||||
|
old?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangeCurrentType(Type type)
|
||||||
|
{
|
||||||
|
CycleValues.ChangeType(type);
|
||||||
|
Output.ChangeType(type);
|
||||||
|
|
||||||
|
_currentType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessPinDisconnected()
|
||||||
|
{
|
||||||
|
if (_updating)
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If there's still a connected pin, stick to the current type
|
||||||
|
if (CycleValues.Any(v => v.ConnectedTo.Any()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ChangeCurrentType(typeof(object));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_dataModelPath?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +1,35 @@
|
|||||||
using Artemis.Core;
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Artemis.Core;
|
||||||
using Artemis.Core.Events;
|
using Artemis.Core.Events;
|
||||||
|
using Artemis.Core.Modules;
|
||||||
using Artemis.Storage.Entities.Profile;
|
using Artemis.Storage.Entities.Profile;
|
||||||
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||||
|
using Humanizer;
|
||||||
|
|
||||||
namespace Artemis.VisualScripting.Nodes.DataModel;
|
namespace Artemis.VisualScripting.Nodes.DataModel;
|
||||||
|
|
||||||
[Node("Data Model-Event", "Responds to a data model event trigger", "Data Model", OutputType = typeof(object))]
|
[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
|
public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
|
||||||
{
|
{
|
||||||
private int _currentIndex;
|
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
||||||
private Type _currentType;
|
|
||||||
private DataModelPath? _dataModelPath;
|
private DataModelPath? _dataModelPath;
|
||||||
private object? _lastPathValue;
|
private IDataModelEvent? _dataModelEvent;
|
||||||
private DateTime _lastTrigger;
|
|
||||||
private bool _updating;
|
public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.")
|
||||||
|
|
||||||
public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger")
|
|
||||||
{
|
{
|
||||||
_currentType = typeof(object);
|
_propertyPins = new Dictionary<Func<DataModelEventArgs, object>, OutputPin>();
|
||||||
|
|
||||||
CycleValues = CreateInputPinCollection(typeof(object), "", 0);
|
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
|
||||||
Output = CreateOutputPin(typeof(object));
|
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
||||||
|
|
||||||
CycleValues.PinAdded += CycleValuesOnPinAdded;
|
|
||||||
CycleValues.PinRemoved += CycleValuesOnPinRemoved;
|
|
||||||
CycleValues.Add(CycleValues.CreatePin());
|
|
||||||
|
|
||||||
// Monitor storage for changes
|
// Monitor storage for changes
|
||||||
StorageModified += (_, _) => UpdateDataModelPath();
|
StorageModified += (_, _) => UpdateDataModelPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public INodeScript? Script { get; set; }
|
public INodeScript? Script { get; set; }
|
||||||
|
public OutputPin<Numeric> TimeSinceLastTrigger { get; }
|
||||||
public InputPinCollection CycleValues { get; }
|
public OutputPin<Numeric> TriggerCount { get; }
|
||||||
public OutputPin Output { get; }
|
|
||||||
|
|
||||||
public override void Initialize(INodeScript script)
|
public override void Initialize(INodeScript script)
|
||||||
{
|
{
|
||||||
@ -45,84 +41,53 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
UpdateDataModelPath();
|
UpdateDataModelPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreatePins(IDataModelEvent? dataModelEvent)
|
||||||
|
{
|
||||||
|
if (_dataModelEvent == dataModelEvent)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<IPin> pins = Pins.Skip(2).ToList();
|
||||||
|
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<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(objectCast, eventParameter).Compile();
|
||||||
|
|
||||||
|
_propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
object? pathValue = _dataModelPath?.GetValue();
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
bool hasTriggered = pathValue is IDataModelEvent dataModelEvent
|
if (pathValue is not IDataModelEvent dataModelEvent)
|
||||||
? EvaluateEvent(dataModelEvent)
|
|
||||||
: EvaluateValue(pathValue);
|
|
||||||
|
|
||||||
if (hasTriggered)
|
|
||||||
{
|
|
||||||
_currentIndex++;
|
|
||||||
|
|
||||||
if (_currentIndex >= CycleValues.Count())
|
|
||||||
_currentIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
object? outputValue = CycleValues.ElementAt(_currentIndex).PinValue;
|
|
||||||
if (Output.Type.IsInstanceOfType(outputValue))
|
|
||||||
Output.Value = outputValue;
|
|
||||||
else if (Output.Type.IsValueType)
|
|
||||||
Output.Value = Output.Type.GetDefault()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EvaluateEvent(IDataModelEvent dataModelEvent)
|
|
||||||
{
|
|
||||||
if (dataModelEvent.LastTrigger <= _lastTrigger)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_lastTrigger = dataModelEvent.LastTrigger;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EvaluateValue(object? pathValue)
|
|
||||||
{
|
|
||||||
if (Equals(pathValue, _lastPathValue))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_lastPathValue = pathValue;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CycleValuesOnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
|
|
||||||
{
|
|
||||||
e.Value.PinConnected += OnPinConnected;
|
|
||||||
e.Value.PinDisconnected += OnPinDisconnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CycleValuesOnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
|
|
||||||
{
|
|
||||||
e.Value.PinConnected -= OnPinConnected;
|
|
||||||
e.Value.PinDisconnected -= OnPinDisconnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
|
|
||||||
{
|
|
||||||
ProcessPinDisconnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
|
|
||||||
{
|
|
||||||
ProcessPinConnected(e.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessPinConnected(IPin source)
|
|
||||||
{
|
|
||||||
if (_updating)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
TimeSinceLastTrigger.Value = new Numeric(dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds);
|
||||||
|
TriggerCount.Value = new Numeric(dataModelEvent.TriggerCount);
|
||||||
|
|
||||||
|
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
||||||
{
|
{
|
||||||
_updating = true;
|
if (!outputPin.ConnectedTo.Any())
|
||||||
|
continue;
|
||||||
// No need to change anything if the types haven't changed
|
object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
||||||
if (_currentType != source.Type)
|
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
||||||
ChangeCurrentType(source.Type);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updating = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,32 +96,10 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
DataModelPath? old = _dataModelPath;
|
DataModelPath? old = _dataModelPath;
|
||||||
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||||
old?.Dispose();
|
old?.Dispose();
|
||||||
}
|
|
||||||
|
|
||||||
private void ChangeCurrentType(Type type)
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
{
|
if (pathValue is IDataModelEvent dataModelEvent)
|
||||||
CycleValues.ChangeType(type);
|
CreatePins(dataModelEvent);
|
||||||
Output.ChangeType(type);
|
|
||||||
|
|
||||||
_currentType = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessPinDisconnected()
|
|
||||||
{
|
|
||||||
if (_updating)
|
|
||||||
return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If there's still a connected pin, stick to the current type
|
|
||||||
if (CycleValues.Any(v => v.ConnectedTo.Any()))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ChangeCurrentType(typeof(object));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updating = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
<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.DataModel.Screens"
|
||||||
|
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.VisualScripting.Nodes.DataModel.Screens.DataModelEventCycleNodeCustomView"
|
||||||
|
x:DataType="screens:DataModelEventCycleNodeCustomViewModel">
|
||||||
|
<dataModelPicker:DataModelPickerButton Classes="condensed"
|
||||||
|
DataModelPath="{CompiledBinding DataModelPath}"
|
||||||
|
Modules="{CompiledBinding Modules}"
|
||||||
|
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
|
||||||
|
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
|
||||||
|
IsEventPicker="True"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
MaxWidth="300"/>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||||
|
|
||||||
|
public class DataModelEventCycleNodeCustomView : ReactiveUserControl<DataModelEventCycleNodeCustomViewModel>
|
||||||
|
{
|
||||||
|
public DataModelEventCycleNodeCustomView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Modules;
|
||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.Storage.Entities.Profile;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
|
using Artemis.UI.Shared.VisualScripting;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||||
|
|
||||||
|
public class DataModelEventCycleNodeCustomViewModel : CustomNodeViewModel
|
||||||
|
{
|
||||||
|
private readonly DataModelEventCycleNode _cycleNode;
|
||||||
|
private readonly INodeEditorService _nodeEditorService;
|
||||||
|
private DataModelPath? _dataModelPath;
|
||||||
|
private ObservableCollection<Module>? _modules;
|
||||||
|
private bool _updating;
|
||||||
|
|
||||||
|
public DataModelEventCycleNodeCustomViewModel(DataModelEventCycleNode cycleNode, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService) : base(cycleNode, script)
|
||||||
|
{
|
||||||
|
_cycleNode = cycleNode;
|
||||||
|
_nodeEditorService = nodeEditorService;
|
||||||
|
|
||||||
|
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
|
||||||
|
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
||||||
|
Modules = new ObservableCollection<Module>();
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
// Set up extra modules
|
||||||
|
if (_cycleNode.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
|
||||||
|
Modules = new ObservableCollection<Module> {scriptProfile.Configuration.Module};
|
||||||
|
else if (_cycleNode.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
|
||||||
|
Modules = new ObservableCollection<Module> {profileConfiguration.Module};
|
||||||
|
|
||||||
|
// Subscribe to node changes
|
||||||
|
_cycleNode.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d);
|
||||||
|
this.WhenAnyValue(vm => vm.DataModelPath).Subscribe(ApplyDataModelPath).DisposeWith(d);
|
||||||
|
|
||||||
|
Disposable.Create(() =>
|
||||||
|
{
|
||||||
|
_dataModelPath?.Dispose();
|
||||||
|
_dataModelPath = null;
|
||||||
|
}).DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginSetting<bool> ShowFullPaths { get; }
|
||||||
|
public PluginSetting<bool> ShowDataModelValues { get; }
|
||||||
|
|
||||||
|
public ObservableCollection<Module>? Modules
|
||||||
|
{
|
||||||
|
get => _modules;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _modules, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataModelPath? DataModelPath
|
||||||
|
{
|
||||||
|
get => _dataModelPath;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDataModelPath(DataModelPathEntity? entity)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_updating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_updating = true;
|
||||||
|
|
||||||
|
DataModelPath? old = DataModelPath;
|
||||||
|
DataModelPath = entity != null ? new DataModelPath(entity) : null;
|
||||||
|
old?.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyDataModelPath(DataModelPath? path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_updating)
|
||||||
|
return;
|
||||||
|
if (path?.Path == _cycleNode.Storage?.Path)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_updating = true;
|
||||||
|
|
||||||
|
path?.Save();
|
||||||
|
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<DataModelPathEntity>(_cycleNode, path?.Entity, "event"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@
|
|||||||
Modules="{CompiledBinding Modules}"
|
Modules="{CompiledBinding Modules}"
|
||||||
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
|
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
|
||||||
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
|
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
|
||||||
|
FilterTypes="{CompiledBinding FilterTypes}"
|
||||||
IsEventPicker="True"
|
IsEventPicker="True"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
MaxWidth="300"/>
|
MaxWidth="300"/>
|
||||||
|
|||||||
@ -63,6 +63,8 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel
|
|||||||
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
|
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Type> FilterTypes => new() {typeof(IDataModelEvent)};
|
||||||
|
|
||||||
private void UpdateDataModelPath(DataModelPathEntity? entity)
|
private void UpdateDataModelPath(DataModelPathEntity? entity)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -32,6 +32,14 @@ public class DisplayValueNodeCustomViewModel : CustomNodeViewModel
|
|||||||
|
|
||||||
private void Update(object? sender, EventArgs e)
|
private void Update(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
CurrentValue = _node.Input.Value;
|
try
|
||||||
|
{
|
||||||
|
CurrentValue = _node.Input.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Don't crash the timer on exceptions and display the messages as a bit of a nice to have
|
||||||
|
CurrentValue = ex.Message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +21,9 @@ public class StringFormatNode : Node
|
|||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
Output.Value = string.Format(Format.Value ?? string.Empty, Values.Values.ToArray());
|
// Convert numerics to floats beforehand to allow string.Format to format them
|
||||||
|
object[] values = Values.Values.Select(v => v is Numeric n ? (float) n : v).ToArray();
|
||||||
|
Output.Value = string.Format(Format.Value ?? string.Empty, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user