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);
}
if (!Script.Nodes.Contains(_startNode))
Script.AddNode(_startNode);
Script.Save();
}

View File

@ -48,6 +48,14 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
{
_value = value;
}
/// <summary>
/// Creates a new instance of <see cref="Numeric" /> from a <see cref="long" />
/// </summary>
public Numeric(long value)
{
_value = value;
}
/// <summary>
/// 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,
double value => (float) value,
byte value => value,
long value => value,
Numeric value => value,
_ => ParseFloatOrDefault(pathValue?.ToString())
};
@ -150,6 +159,11 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
{
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)
{
@ -264,6 +278,7 @@ public readonly struct Numeric : IComparable<Numeric>, IConvertible
type == typeof(float) ||
type == typeof(double) ||
type == typeof(int) ||
type == typeof(long) ||
type == typeof(byte);
}

View File

@ -1,6 +1,7 @@
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;
@ -11,13 +12,12 @@ namespace Artemis.Core.Internal;
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
{
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
private readonly List<OutputPin> _pinBucket = new();
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
private IDataModelEvent? _dataModelEvent;
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)
@ -33,40 +33,22 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
if (dataModelEvent == null)
return;
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.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);
_pinBucket.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);
}
// 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();
return pin;
_propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
}
}
public override void Evaluate()
@ -74,13 +56,12 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return;
foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
{
if (outputPin.ConnectedTo.Any())
{
object value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!;
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
}
if (!outputPin.ConnectedTo.Any())
continue;
object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Ninject;
using Ninject.Parameters;
@ -15,6 +16,9 @@ public abstract class Node : CorePropertyChanged, INode
public event EventHandler? Resetting;
#region Properties & Fields
private readonly List<OutputPin> _outputPinBucket = new();
private readonly List<InputPin> _inputPinBucket = new();
private Guid _id;
@ -160,6 +164,66 @@ public abstract class Node : CorePropertyChanged, INode
OnPropertyChanged(nameof(Pins));
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>
/// 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
{
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> _checkForUpdates;

View File

@ -33,6 +33,10 @@
<Compile Update="Nodes\DataModel\Screens\DataModelNodeCustomView.axaml.cs">
<DependentUpon>DataModelNodeCustomView.axaml</DependentUpon>
</Compile>
<Compile Update="Nodes\DataModel\Screens\DataModelEventNodeCustomView.axaml.cs">
<DependentUpon>DataModelEventNodeCustomView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</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.Modules;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.Screens;
using Humanizer;
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
{
private int _currentIndex;
private Type _currentType;
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
private DataModelPath? _dataModelPath;
private object? _lastPathValue;
private DateTime _lastTrigger;
private bool _updating;
public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger")
private IDataModelEvent? _dataModelEvent;
public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.")
{
_currentType = typeof(object);
CycleValues = CreateInputPinCollection(typeof(object), "", 0);
Output = CreateOutputPin(typeof(object));
CycleValues.PinAdded += CycleValuesOnPinAdded;
CycleValues.PinRemoved += CycleValuesOnPinRemoved;
CycleValues.Add(CycleValues.CreatePin());
_propertyPins = new Dictionary<Func<DataModelEventArgs, object>, OutputPin>();
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
// Monitor storage for changes
StorageModified += (_, _) => UpdateDataModelPath();
}
public INodeScript? Script { get; set; }
public InputPinCollection CycleValues { get; }
public OutputPin Output { get; }
public OutputPin<Numeric> TimeSinceLastTrigger { get; }
public OutputPin<Numeric> TriggerCount { get; }
public override void Initialize(INodeScript script)
{
@ -45,84 +41,53 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
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()
{
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)
if (pathValue is not IDataModelEvent dataModelEvent)
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;
// No need to change anything if the types haven't changed
if (_currentType != source.Type)
ChangeCurrentType(source.Type);
}
finally
{
_updating = false;
if (!outputPin.ConnectedTo.Any())
continue;
object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
}
}
@ -131,32 +96,10 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
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;
}
object? pathValue = _dataModelPath?.GetValue();
if (pathValue is IDataModelEvent dataModelEvent)
CreatePins(dataModelEvent);
}
/// <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}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
FilterTypes="{CompiledBinding FilterTypes}"
IsEventPicker="True"
VerticalAlignment="Top"
MaxWidth="300"/>

View File

@ -63,6 +63,8 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
}
public List<Type> FilterTypes => new() {typeof(IDataModelEvent)};
private void UpdateDataModelPath(DataModelPathEntity? entity)
{
try

View File

@ -32,6 +32,14 @@ public class DisplayValueNodeCustomViewModel : CustomNodeViewModel
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()
{
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