1
0
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:
Robert 2022-08-23 20:35:25 +02:00
parent 3efb97eedd
commit d2e0607622
15 changed files with 488 additions and 160 deletions

View File

@ -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();
} }

View File

@ -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);
} }

View File

@ -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;
}
} }
} }
} }

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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 />

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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"/>

View File

@ -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

View File

@ -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;
}
} }
} }

View File

@ -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