mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Nodes - Added object output pins and a new list predicate node
This commit is contained in:
parent
9106359515
commit
b19854ee47
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Artemis.Core.Internal;
|
using Artemis.Core.Internal;
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
|
||||||
using Artemis.Storage.Entities.Profile.Abstract;
|
using Artemis.Storage.Entities.Profile.Abstract;
|
||||||
using Artemis.Storage.Entities.Profile.Conditions;
|
using Artemis.Storage.Entities.Profile.Conditions;
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Artemis.Core.Internal;
|
namespace Artemis.Core;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a kind of node that cannot be deleted inside a <see cref="INode" />.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDefaultNode : INode
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DefaultNode : Node, IDefaultNode
|
public abstract class DefaultNode : Node
|
||||||
{
|
{
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode
|
|||||||
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
|
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
|
||||||
Description = description;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -1,23 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reflection;
|
|
||||||
using Artemis.Core.Modules;
|
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
|
||||||
using Humanizer;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Internal;
|
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 Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
private readonly ObjectOutputPins _objectOutputPins;
|
||||||
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<Func<DataModelEventArgs, object>, OutputPin>();
|
_objectOutputPins = new ObjectOutputPins(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreatePins(IDataModelEvent? dataModelEvent)
|
public void CreatePins(IDataModelEvent? dataModelEvent)
|
||||||
@ -25,30 +18,8 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (_dataModelEvent == dataModelEvent)
|
if (_dataModelEvent == dataModelEvent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
while (Pins.Any())
|
|
||||||
RemovePin((Pin) Pins.First());
|
|
||||||
_propertyPins.Clear();
|
|
||||||
|
|
||||||
_dataModelEvent = dataModelEvent;
|
_dataModelEvent = dataModelEvent;
|
||||||
if (dataModelEvent == null)
|
_objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType);
|
||||||
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()
|
||||||
@ -56,12 +27,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
_objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped);
|
||||||
{
|
|
||||||
if (!outputPin.ConnectedTo.Any())
|
|
||||||
continue;
|
|
||||||
object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
|
||||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
|
||||||
|
|
||||||
namespace Artemis.Core.Internal;
|
namespace Artemis.Core.Internal;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Artemis.Core.VisualScripting.Internal;
|
namespace Artemis.Core.Internal;
|
||||||
|
|
||||||
internal interface IEventConditionNode : INode
|
internal interface IEventConditionNode : INode
|
||||||
{
|
{
|
||||||
|
|||||||
@ -132,7 +132,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <returns>The newly created pin</returns>
|
||||||
protected InputPin<T> CreateInputPin<T>(string name = "")
|
public InputPin<T> CreateInputPin<T>(string name = "")
|
||||||
{
|
{
|
||||||
InputPin<T> pin = new(this, name);
|
InputPin<T> pin = new(this, name);
|
||||||
_pins.Add(pin);
|
_pins.Add(pin);
|
||||||
@ -146,7 +146,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="type">The type of value the pin will hold</param>
|
/// <param name="type">The type of value the pin will hold</param>
|
||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <returns>The newly created pin</returns>
|
||||||
protected InputPin CreateInputPin(Type type, string name = "")
|
public InputPin CreateInputPin(Type type, string name = "")
|
||||||
{
|
{
|
||||||
InputPin pin = new(this, type, name);
|
InputPin pin = new(this, type, name);
|
||||||
_pins.Add(pin);
|
_pins.Add(pin);
|
||||||
@ -160,7 +160,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <returns>The newly created pin</returns>
|
||||||
protected OutputPin<T> CreateOutputPin<T>(string name = "")
|
public OutputPin<T> CreateOutputPin<T>(string name = "")
|
||||||
{
|
{
|
||||||
OutputPin<T> pin = new(this, name);
|
OutputPin<T> pin = new(this, name);
|
||||||
_pins.Add(pin);
|
_pins.Add(pin);
|
||||||
@ -174,7 +174,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="type">The type of value the pin will hold</param>
|
/// <param name="type">The type of value the pin will hold</param>
|
||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <returns>The newly created pin</returns>
|
||||||
protected OutputPin CreateOutputPin(Type type, string name = "")
|
public OutputPin CreateOutputPin(Type type, string name = "")
|
||||||
{
|
{
|
||||||
OutputPin pin = new(this, type, name);
|
OutputPin pin = new(this, type, name);
|
||||||
_pins.Add(pin);
|
_pins.Add(pin);
|
||||||
@ -187,7 +187,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
/// 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.
|
/// editor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
|
public OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
|
||||||
{
|
{
|
||||||
// Grab the first pin from the bucket that isn't on the node yet
|
// Grab the first pin from the bucket that isn't on the node yet
|
||||||
OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||||
@ -217,7 +217,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
/// 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.
|
/// editor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected InputPin CreateOrAddInputPin(Type valueType, string displayName)
|
public InputPin CreateOrAddInputPin(Type valueType, string displayName)
|
||||||
{
|
{
|
||||||
// Grab the first pin from the bucket that isn't on the node yet
|
// Grab the first pin from the bucket that isn't on the node yet
|
||||||
InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||||
@ -247,7 +247,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin to remove</param>
|
/// <param name="pin">The pin to remove</param>
|
||||||
/// <returns><see langword="true" /> if the pin was removed; otherwise <see langword="false" />.</returns>
|
/// <returns><see langword="true" /> if the pin was removed; otherwise <see langword="false" />.</returns>
|
||||||
protected bool RemovePin(Pin pin)
|
public bool RemovePin(Pin pin)
|
||||||
{
|
{
|
||||||
bool isRemoved = _pins.Remove(pin);
|
bool isRemoved = _pins.Remove(pin);
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
@ -263,7 +263,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// Adds an existing <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
/// Adds an existing <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin to add</param>
|
/// <param name="pin">The pin to add</param>
|
||||||
protected void AddPin(Pin pin)
|
public void AddPin(Pin pin)
|
||||||
{
|
{
|
||||||
if (pin.Node != this)
|
if (pin.Node != this)
|
||||||
throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to.");
|
throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to.");
|
||||||
@ -281,7 +281,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin collection</param>
|
/// <param name="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting input pin collection</returns>
|
/// <returns>The resulting input pin collection</returns>
|
||||||
protected InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
public InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
||||||
{
|
{
|
||||||
InputPinCollection<T> pin = new(this, name, initialCount);
|
InputPinCollection<T> pin = new(this, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_pinCollections.Add(pin);
|
||||||
@ -296,7 +296,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin collection</param>
|
/// <param name="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting input pin collection</returns>
|
/// <returns>The resulting input pin collection</returns>
|
||||||
protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1)
|
public InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1)
|
||||||
{
|
{
|
||||||
InputPinCollection pin = new(this, type, name, initialCount);
|
InputPinCollection pin = new(this, type, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_pinCollections.Add(pin);
|
||||||
@ -311,7 +311,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin collection</param>
|
/// <param name="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting output pin collection</returns>
|
/// <returns>The resulting output pin collection</returns>
|
||||||
protected OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
public OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
||||||
{
|
{
|
||||||
OutputPinCollection<T> pin = new(this, name, initialCount);
|
OutputPinCollection<T> pin = new(this, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_pinCollections.Add(pin);
|
||||||
@ -325,7 +325,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pinCollection">The pin collection to remove</param>
|
/// <param name="pinCollection">The pin collection to remove</param>
|
||||||
/// <returns><see langword="true" /> if the pin collection was removed; otherwise <see langword="false" />.</returns>
|
/// <returns><see langword="true" /> if the pin collection was removed; otherwise <see langword="false" />.</returns>
|
||||||
protected bool RemovePinCollection(PinCollection pinCollection)
|
public bool RemovePinCollection(PinCollection pinCollection)
|
||||||
{
|
{
|
||||||
bool isRemoved = _pinCollections.Remove(pinCollection);
|
bool isRemoved = _pinCollections.Remove(pinCollection);
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
|
|||||||
@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
|||||||
|
|
||||||
#region Properties & Fields
|
#region Properties & Fields
|
||||||
|
|
||||||
internal NodeScriptEntity Entity { get; private set; }
|
/// <summary>
|
||||||
|
/// Gets the entity used to store this script.
|
||||||
|
/// </summary>
|
||||||
|
public NodeScriptEntity Entity { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
@ -410,7 +413,8 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
|||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
/// <inheritdoc />
|
||||||
|
public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
||||||
: base(name, description, entity, context)
|
: base(name, description, entity, context)
|
||||||
{
|
{
|
||||||
ExitNode = new ExitNode<T>(name, description);
|
ExitNode = new ExitNode<T>(name, description);
|
||||||
|
|||||||
142
src/Artemis.Core/VisualScripting/ObjectOutputPins.cs
Normal file
142
src/Artemis.Core/VisualScripting/ObjectOutputPins.cs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Artemis.Core.Modules;
|
||||||
|
using Humanizer;
|
||||||
|
|
||||||
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a collection of output pins for a node capable of outputting the properties of an object or value type.
|
||||||
|
/// </summary>
|
||||||
|
public class ObjectOutputPins
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Func<object, object>, OutputPin> _propertyPins;
|
||||||
|
private OutputPin? _valueTypePin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instance of the <see cref="ObjectOutputPins" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">The node the object output was created for.</param>
|
||||||
|
public ObjectOutputPins(Node node)
|
||||||
|
{
|
||||||
|
Node = node;
|
||||||
|
_propertyPins = new Dictionary<Func<object, object>, OutputPin>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the node the object output was created for.
|
||||||
|
/// </summary>
|
||||||
|
public Node Node { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current type the node's pins are set up for.
|
||||||
|
/// </summary>
|
||||||
|
public Type? CurrentType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read only collection of the pins outputting the object of this object node.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyCollection<OutputPin> Pins => _valueTypePin != null ? new ReadOnlyCollection<OutputPin>(new List<OutputPin> {_valueTypePin}) : _propertyPins.Values.ToList().AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the current type and create pins on the node to reflect this.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to change the collection to.</param>
|
||||||
|
public void ChangeType(Type? type)
|
||||||
|
{
|
||||||
|
if (type == CurrentType)
|
||||||
|
return;
|
||||||
|
CurrentType = type;
|
||||||
|
|
||||||
|
// Remove current pins
|
||||||
|
foreach ((Func<object, object>? _, OutputPin? pin) in _propertyPins)
|
||||||
|
Node.RemovePin(pin);
|
||||||
|
_propertyPins.Clear();
|
||||||
|
if (_valueTypePin != null)
|
||||||
|
{
|
||||||
|
Node.RemovePin(_valueTypePin);
|
||||||
|
_valueTypePin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Create new pins
|
||||||
|
List<TypeColorRegistration> nodeTypeColors = NodeTypeStore.GetColors();
|
||||||
|
if (type.IsClass && type != typeof(string))
|
||||||
|
foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
Type propertyType = propertyInfo.PropertyType;
|
||||||
|
bool toNumeric = Numeric.IsTypeCompatible(propertyType);
|
||||||
|
|
||||||
|
// Skip ignored properties
|
||||||
|
if (propertyInfo.CustomAttributes.Any(a => a.AttributeType == typeof(DataModelIgnoreAttribute)))
|
||||||
|
continue;
|
||||||
|
// Skip incompatible properties
|
||||||
|
if (!toNumeric && !nodeTypeColors.Any(c => c.Type.IsAssignableFrom(propertyType)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Expect an object
|
||||||
|
ParameterExpression itemParameter = Expression.Parameter(typeof(object), "item");
|
||||||
|
// Cast it to the actual item type
|
||||||
|
UnaryExpression itemCast = Expression.Convert(itemParameter, propertyInfo.DeclaringType!);
|
||||||
|
// Access the property
|
||||||
|
MemberExpression accessor = Expression.Property(itemCast, propertyInfo);
|
||||||
|
|
||||||
|
// Turn into a numeric if needed or access directly
|
||||||
|
UnaryExpression objectExpression;
|
||||||
|
if (toNumeric)
|
||||||
|
{
|
||||||
|
propertyType = typeof(Numeric);
|
||||||
|
ConstructorInfo constructor = typeof(Numeric).GetConstructors().First(c => c.GetParameters().First().ParameterType == propertyInfo.PropertyType);
|
||||||
|
// Cast the property to an object (sadly boxing)
|
||||||
|
objectExpression = Expression.Convert(Expression.New(constructor, accessor), typeof(object));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Cast the property to an object (sadly boxing)
|
||||||
|
objectExpression = Expression.Convert(accessor, typeof(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the resulting expression
|
||||||
|
Func<object, object> expression = Expression.Lambda<Func<object, object>>(objectExpression, itemParameter).Compile();
|
||||||
|
_propertyPins.Add(expression, Node.CreateOrAddOutputPin(propertyType, propertyInfo.Name.Humanize()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
// Value types are applied directly to a single pin, however if the type is compatible with Numeric, we use a Numeric pin instead
|
||||||
|
// the value will then be turned into a numeric in SetCurrentValue
|
||||||
|
_valueTypePin = Node.CreateOrAddOutputPin(Numeric.IsTypeCompatible(type) ? typeof(Numeric) : type, "Item");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the current value to be output onto connected pins.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to output onto the connected pins.</param>
|
||||||
|
/// <exception cref="ArtemisCoreException"></exception>
|
||||||
|
public void SetCurrentValue(object? value)
|
||||||
|
{
|
||||||
|
if (CurrentType == null)
|
||||||
|
throw new ArtemisCoreException("Cannot apply a value to an object output pins not yet configured for a type.");
|
||||||
|
if (value != null && CurrentType != value.GetType())
|
||||||
|
throw new ArtemisCoreException($"Cannot apply a value of type {value.GetType().FullName} to an object output pins configured for type {CurrentType.FullName}");
|
||||||
|
|
||||||
|
// Apply the object to the pin, it must be connected if SetCurrentValue got called
|
||||||
|
if (_valueTypePin != null)
|
||||||
|
{
|
||||||
|
value ??= _valueTypePin.Type.GetDefault();
|
||||||
|
_valueTypePin.Value = _valueTypePin.Type == typeof(Numeric) ? new Numeric(value) : value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the properties of the object to each connected pin
|
||||||
|
foreach ((Func<object, object>? propertyAccessor, OutputPin? outputPin) in _propertyPins)
|
||||||
|
{
|
||||||
|
if (outputPin.ConnectedTo.Any())
|
||||||
|
outputPin.Value = value != null ? propertyAccessor(value) : outputPin.Type.GetDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.NodeEditor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base of the node script editor window view model.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class NodeScriptWindowViewModelBase : DialogViewModelBase<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="NodeScriptWindowViewModelBase" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeScript">The node script being edited.</param>
|
||||||
|
protected NodeScriptWindowViewModelBase(NodeScript nodeScript)
|
||||||
|
{
|
||||||
|
NodeScript = nodeScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the node script being edited.
|
||||||
|
/// </summary>
|
||||||
|
public NodeScript NodeScript { get; init; }
|
||||||
|
}
|
||||||
@ -103,4 +103,11 @@
|
|||||||
<Setter Property="Padding" Value="6 3 11 3" />
|
<Setter Property="Padding" Value="6 3 11 3" />
|
||||||
<Setter Property="Height" Value="24" />
|
<Setter Property="Height" Value="24" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.condensed">
|
||||||
|
<Setter Property="Padding" Value="1" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="MinHeight" Value="24" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
||||||
@ -2,8 +2,10 @@
|
|||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Ninject.InstanceProviders;
|
using Artemis.UI.Ninject.InstanceProviders;
|
||||||
using Artemis.UI.Screens;
|
using Artemis.UI.Screens;
|
||||||
|
using Artemis.UI.Screens.VisualScripting;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Shared.PlatformSupport;
|
using Avalonia.Shared.PlatformSupport;
|
||||||
@ -57,6 +59,7 @@ public class UIModule : NinjectModule
|
|||||||
.BindToFactory();
|
.BindToFactory();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Kernel.Bind<NodeScriptWindowViewModelBase>().To<NodeScriptWindowViewModel>();
|
||||||
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
||||||
|
|
||||||
// Bind all UI services as singletons
|
// Bind all UI services as singletons
|
||||||
|
|||||||
@ -76,18 +76,23 @@ public class PropertiesViewModel : ActivatableViewModelBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
this.WhenActivated(d =>
|
||||||
.Select(p => p is Layer l
|
{
|
||||||
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||||
: Observable.Never<EventPattern<object>>())
|
.Select(p => p is Layer l
|
||||||
.Switch()
|
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
||||||
.Subscribe(_ => UpdatePropertyGroups());
|
: Observable.Never<EventPattern<object>>())
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
.Switch()
|
||||||
.Select(p => p != null
|
.Subscribe(_ => UpdatePropertyGroups())
|
||||||
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
.DisposeWith(d);
|
||||||
: Observable.Never<EventPattern<object>>())
|
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||||
.Switch()
|
.Select(p => p != null
|
||||||
.Subscribe(_ => UpdatePropertyGroups());
|
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
||||||
|
: Observable.Never<EventPattern<object>>())
|
||||||
|
.Switch()
|
||||||
|
.Subscribe(_ => UpdatePropertyGroups())
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups());
|
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups());
|
||||||
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
|
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Shared;
|
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.NodeEditor;
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
@ -20,7 +19,7 @@ using ReactiveUI;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.VisualScripting;
|
namespace Artemis.UI.Screens.VisualScripting;
|
||||||
|
|
||||||
public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
||||||
{
|
{
|
||||||
private readonly INodeEditorService _nodeEditorService;
|
private readonly INodeEditorService _nodeEditorService;
|
||||||
private readonly INodeService _nodeService;
|
private readonly INodeService _nodeService;
|
||||||
@ -34,7 +33,7 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
|||||||
INodeVmFactory vmFactory,
|
INodeVmFactory vmFactory,
|
||||||
ISettingsService settingsService,
|
ISettingsService settingsService,
|
||||||
IProfileService profileService,
|
IProfileService profileService,
|
||||||
IWindowService windowService)
|
IWindowService windowService) : base(nodeScript)
|
||||||
{
|
{
|
||||||
NodeScript = nodeScript;
|
NodeScript = nodeScript;
|
||||||
NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false);
|
NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false);
|
||||||
@ -77,7 +76,6 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public NodeScript NodeScript { get; }
|
|
||||||
public NodeScriptViewModel NodeScriptViewModel { get; set; }
|
public NodeScriptViewModel NodeScriptViewModel { get; set; }
|
||||||
|
|
||||||
public NodeEditorHistory History { get; }
|
public NodeEditorHistory History { get; }
|
||||||
|
|||||||
@ -1,29 +1,25 @@
|
|||||||
using System.Linq.Expressions;
|
using Artemis.Core;
|
||||||
using System.Reflection;
|
|
||||||
using Artemis.Core;
|
|
||||||
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", "Outputs the latest values of a data model event.", "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 readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
private readonly ObjectOutputPins _objectOutputPins;
|
||||||
private DataModelPath? _dataModelPath;
|
|
||||||
private IDataModelEvent? _dataModelEvent;
|
private IDataModelEvent? _dataModelEvent;
|
||||||
private OutputPin? _oldValuePin;
|
private DataModelPath? _dataModelPath;
|
||||||
private OutputPin? _newValuePin;
|
|
||||||
private DateTime _lastTrigger;
|
private DateTime _lastTrigger;
|
||||||
private object? _lastValue;
|
private object? _lastValue;
|
||||||
|
private OutputPin? _newValuePin;
|
||||||
|
private OutputPin? _oldValuePin;
|
||||||
private int _valueChangeCount;
|
private int _valueChangeCount;
|
||||||
|
|
||||||
public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.")
|
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");
|
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
|
||||||
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
||||||
|
|
||||||
@ -48,20 +44,14 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
object? pathValue = _dataModelPath?.GetValue();
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
|
|
||||||
// If the path is a data model event, evaluate the event
|
// If the path is a data model event, evaluate the event
|
||||||
if (pathValue is IDataModelEvent dataModelEvent)
|
if (pathValue is IDataModelEvent dataModelEvent)
|
||||||
{
|
{
|
||||||
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
||||||
TriggerCount.Value = dataModelEvent.TriggerCount;
|
TriggerCount.Value = dataModelEvent.TriggerCount;
|
||||||
|
|
||||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
_objectOutputPins.SetCurrentValue(dataModelEvent.LastEventArgumentsUntyped);
|
||||||
{
|
|
||||||
if (!outputPin.ConnectedTo.Any())
|
|
||||||
continue;
|
|
||||||
object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
|
||||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If the path is a regular value, evaluate the current value
|
// If the path is a regular value, evaluate the current value
|
||||||
else if (_oldValuePin != null && _newValuePin != null)
|
else if (_oldValuePin != null && _newValuePin != null)
|
||||||
@ -71,13 +61,13 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_valueChangeCount++;
|
_valueChangeCount++;
|
||||||
_lastTrigger = DateTime.Now;
|
_lastTrigger = DateTime.Now;
|
||||||
|
|
||||||
_oldValuePin.Value = _lastValue;
|
_oldValuePin.Value = _lastValue;
|
||||||
_newValuePin.Value = pathValue;
|
_newValuePin.Value = pathValue;
|
||||||
|
|
||||||
_lastValue = pathValue;
|
_lastValue = pathValue;
|
||||||
|
|
||||||
TimeSinceLastTrigger.Value = 0;
|
TimeSinceLastTrigger.Value = 0;
|
||||||
@ -91,19 +81,20 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||||
if (_dataModelPath != null)
|
if (_dataModelPath != null)
|
||||||
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
|
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
|
||||||
|
|
||||||
if (old != null)
|
if (old != null)
|
||||||
{
|
{
|
||||||
old.PathValidated -= DataModelPathOnPathValidated;
|
old.PathValidated -= DataModelPathOnPathValidated;
|
||||||
old.Dispose();
|
old.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateOutputPins();
|
UpdateOutputPins();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateOutputPins()
|
private void UpdateOutputPins()
|
||||||
{
|
{
|
||||||
object? pathValue = _dataModelPath?.GetValue();
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
|
|
||||||
if (pathValue is IDataModelEvent dataModelEvent)
|
if (pathValue is IDataModelEvent dataModelEvent)
|
||||||
CreateEventPins(dataModelEvent);
|
CreateEventPins(dataModelEvent);
|
||||||
else
|
else
|
||||||
@ -114,25 +105,10 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
{
|
{
|
||||||
if (_dataModelEvent == dataModelEvent)
|
if (_dataModelEvent == dataModelEvent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ClearPins();
|
ClearPins();
|
||||||
_dataModelEvent = dataModelEvent;
|
_dataModelEvent = dataModelEvent;
|
||||||
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
_objectOutputPins.ChangeType(dataModelEvent.ArgumentsType);
|
||||||
.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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateValuePins()
|
private void CreateValuePins()
|
||||||
@ -142,24 +118,31 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
Type? propertyType = _dataModelPath?.GetPropertyType();
|
Type? propertyType = _dataModelPath?.GetPropertyType();
|
||||||
if (propertyType == null)
|
if (propertyType == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
||||||
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
||||||
_lastValue = null;
|
_lastValue = null;
|
||||||
_valueChangeCount = 0;
|
_valueChangeCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearPins()
|
private void ClearPins()
|
||||||
{
|
{
|
||||||
List<IPin> pins = Pins.Skip(2).ToList();
|
// Clear the output pins by changing the type to null
|
||||||
foreach (IPin pin in pins)
|
_objectOutputPins.ChangeType(null);
|
||||||
RemovePin((Pin) pin);
|
|
||||||
|
if (_oldValuePin != null)
|
||||||
_propertyPins.Clear();
|
{
|
||||||
_oldValuePin = null;
|
RemovePin(_oldValuePin);
|
||||||
_newValuePin = null;
|
_oldValuePin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_newValuePin != null)
|
||||||
|
{
|
||||||
|
RemovePin(_newValuePin);
|
||||||
|
_newValuePin = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
|
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
|
// 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;
|
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 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>();
|
InputList = CreateInputPin<IList>();
|
||||||
InputValue = CreateInputPin<object>();
|
InputValue = CreateInputPin<object>();
|
||||||
|
|
||||||
Ouput = CreateOutputPin<bool>();
|
Output = CreateOutputPin<bool>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputPin<IList> InputList { get; }
|
public InputPin<IList> InputList { get; }
|
||||||
public InputPin<object> InputValue { get; }
|
public InputPin<object> InputValue { get; }
|
||||||
public OutputPin<bool> Ouput { get; }
|
public OutputPin<bool> Output { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
if (InputList.Value == null)
|
if (InputList.Value == null)
|
||||||
{
|
{
|
||||||
Ouput.Value = Storage == ListOperator.None;
|
Output.Value = Storage == ListOperator.None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
object? input = InputValue.Value;
|
object? input = InputValue.Value;
|
||||||
if (Storage == ListOperator.Any)
|
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)
|
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.All)
|
else if (Storage == ListOperator.None)
|
||||||
Ouput.Value = InputList.Value.Cast<object>().All(v => !v.Equals(input));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user