From afc4bee7ac34ab5f0282979c91bebdbb64b4e3b0 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 10 Aug 2021 16:32:21 +0200 Subject: [PATCH] Nodes - Implemented saving/loading Nodes - Added data model node --- .../Extensions/IEnumerableExtensions.cs | 12 + .../Models/Profile/DataModel/DataModelPath.cs | 144 ++++++----- .../Models/Profile/RenderProfileElement.cs | 82 +++--- src/Artemis.Core/Services/NodeService.cs | 18 +- src/Artemis.Core/Stores/NodeTypeStore.cs | 24 +- .../VisualScripting/CustomNodeViewModel.cs | 15 ++ src/Artemis.Core/VisualScripting/InputPin.cs | 2 + .../VisualScripting/Interfaces/INode.cs | 4 +- src/Artemis.Core/VisualScripting/Node.cs | 14 +- src/Artemis.Core/VisualScripting/NodeData.cs | 7 +- .../VisualScripting/NodeScript.cs | 235 +++++++++++++++++- src/Artemis.Core/VisualScripting/OutputPin.cs | 2 + .../Profile/Abstract/RenderElementEntity.cs | 3 + .../Profile/Nodes/NodeConnectionEntity.cs | 14 ++ .../Entities/Profile/Nodes/NodeEntity.cs | 26 ++ .../Profile/Nodes/NodePinCollectionEntity.cs | 10 + .../Profile/Nodes/NodeScriptEntity.cs | 19 ++ src/Artemis.UI/packages.lock.json | 1 + .../Artemis.VisualScripting.csproj | 3 +- .../Editor/Controls/VisualScriptPresenter.cs | 2 +- .../Styles/VisualScriptNodePresenter.xaml | 13 +- .../DataModelNodeCustomViewModel.cs | 42 ++++ .../StaticDoubleValueNodeCustomViewModel.cs | 19 +- .../StaticIntegerValueNodeCustomViewModel.cs | 19 +- .../StaticStringValueNodeCustomViewModel.cs | 19 +- .../CustomViews/DataModelNodeCustomView.xaml | 11 + .../Nodes/DataModelNode.cs | 48 ++++ .../Nodes/StaticValueNodes.cs | 12 +- .../packages.lock.json | 67 +++++ 29 files changed, 730 insertions(+), 157 deletions(-) create mode 100644 src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Nodes/NodeConnectionEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Nodes/NodePinCollectionEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Nodes/NodeScriptEntity.cs create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml create mode 100644 src/Artemis.VisualScripting/Nodes/DataModelNode.cs diff --git a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs index 3fe2b8caa..0c60de0f9 100644 --- a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs +++ b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs @@ -92,5 +92,17 @@ namespace Artemis.Core } } } + + public static int IndexOf(this IReadOnlyCollection self, T elementToFind) + { + int i = 0; + foreach (T element in self) + { + if (Equals(element, elementToFind)) + return i; + i++; + } + return -1; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index baa7eac6c..8dc7c9723 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -72,7 +72,12 @@ namespace Artemis.Core SubscribeToDataModelStore(); } - internal DataModelPath(DataModel? target, DataModelPathEntity entity) + /// + /// Creates a new instance of the class based on a + /// + /// + /// + public DataModelPath(DataModel? target, DataModelPathEntity entity) { Target = target; Path = entity.Path; @@ -110,7 +115,10 @@ namespace Artemis.Core /// public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); - internal DataModelPathEntity Entity { get; } + /// + /// Gets the entity used for persistent storage + /// + public DataModelPathEntity Entity { get; } internal Func? Accessor { get; private set; } @@ -173,6 +181,52 @@ namespace Artemis.Core return string.IsNullOrWhiteSpace(Path) ? "this" : Path; } + /// + /// Occurs whenever the path becomes invalid + /// + public event EventHandler? PathInvalidated; + + /// + /// Occurs whenever the path becomes valid + /// + public event EventHandler? PathValidated; + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + + Invalidate(); + } + } + + /// + /// Invokes the event + /// + protected virtual void OnPathValidated() + { + PathValidated?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the event + /// + protected virtual void OnPathInvalidated() + { + PathInvalidated?.Invoke(this, EventArgs.Empty); + } + internal void Invalidate() { Target?.RemoveDataModelPath(this); @@ -262,27 +316,24 @@ namespace Artemis.Core DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; } - - #region IDisposable - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) { - if (disposing) - { - _disposed = true; + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) + return; - DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; - DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + Invalidate(); + Target = e.Registration.DataModel; + Initialize(); + } - Invalidate(); - } + private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) + { + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) + return; + + Invalidate(); + Target = null; } /// @@ -292,8 +343,6 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - #endregion - #region Storage /// @@ -324,58 +373,5 @@ namespace Artemis.Core } #endregion - - #region Event handlers - - private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) - { - if (e.Registration.DataModel.Module.Id != Entity.DataModelId) - return; - - Invalidate(); - Target = e.Registration.DataModel; - Initialize(); - } - - private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) - { - if (e.Registration.DataModel.Module.Id != Entity.DataModelId) - return; - - Invalidate(); - Target = null; - } - - #endregion - - #region Events - - /// - /// Occurs whenever the path becomes invalid - /// - public event EventHandler? PathInvalidated; - - /// - /// Occurs whenever the path becomes valid - /// - public event EventHandler? PathValidated; - - /// - /// Invokes the event - /// - protected virtual void OnPathValidated() - { - PathValidated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - protected virtual void OnPathInvalidated() - { - PathInvalidated?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index af4832a68..b72c5f9d0 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -7,6 +7,7 @@ using Artemis.Core.LayerEffects.Placeholder; using Artemis.Core.Properties; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core @@ -66,6 +67,8 @@ namespace Artemis.Core internal void LoadRenderElement() { + DisplayCondition = RenderElementEntity.NodeScript != null ? new NodeScript(RenderElementEntity.NodeScript) : null; + // DisplayCondition = RenderElementEntity.DisplayCondition != null // ? new DataModelConditionGroup(null, RenderElementEntity.DisplayCondition) // : new DataModelConditionGroup(null); @@ -97,6 +100,8 @@ namespace Artemis.Core } // Conditions + DisplayCondition?.Save(); + RenderElementEntity.NodeScript = DisplayCondition?.Entity; // RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; // DisplayCondition?.Save(); @@ -126,7 +131,7 @@ namespace Artemis.Core // The play mode dictates whether to stick to the main segment unless the display conditions contains events bool stickToMainSegment = (Timeline.PlayMode == TimelinePlayMode.Repeat || Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) && DisplayConditionMet; // if (DisplayCondition != null && DisplayCondition.ContainsEvents && Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle) - // stickToMainSegment = false; + // stickToMainSegment = false; Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment); } @@ -386,50 +391,51 @@ namespace Artemis.Core if (Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle) _toggledOnByEvent = false; - + DisplayCondition.Run(); - bool conditionMet = DisplayCondition.Result; + + // TODO: Handle this nicely, right now when there's only an exit node we assume true + bool conditionMet = DisplayCondition.Nodes.Count() == 1 || DisplayCondition.Result; if (Parent is RenderProfileElement parent && !parent.DisplayConditionMet) conditionMet = false; // if (!DisplayCondition.ContainsEvents) - // { - // // Regular conditions reset the timeline whenever their condition is met and was not met before that - // if (conditionMet && !DisplayConditionMet && Timeline.IsFinished) - // Timeline.JumpToStart(); - // // If regular conditions are no longer met, jump to the end segment if stop mode requires it - // if (!conditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd) - // Timeline.JumpToEndSegment(); - // } - // else if (conditionMet) - if (conditionMet) { - if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) - { - _toggledOnByEvent = !_toggledOnByEvent; - if (_toggledOnByEvent) - Timeline.JumpToStart(); - } - else - { - // Event conditions reset if the timeline finished - if (Timeline.IsFinished) - { - Timeline.JumpToStart(); - } - // and otherwise apply their overlap mode - else - { - if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart) - Timeline.JumpToStart(); - else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy) - Timeline.AddExtraTimeline(); - // The third option is ignore which is handled below: - - // done - } - } + // Regular conditions reset the timeline whenever their condition is met and was not met before that + if (conditionMet && !DisplayConditionMet && Timeline.IsFinished) + Timeline.JumpToStart(); + // If regular conditions are no longer met, jump to the end segment if stop mode requires it + if (!conditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd) + Timeline.JumpToEndSegment(); } + // else if (conditionMet) + // { + // if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) + // { + // _toggledOnByEvent = !_toggledOnByEvent; + // if (_toggledOnByEvent) + // Timeline.JumpToStart(); + // } + // else + // { + // // Event conditions reset if the timeline finished + // if (Timeline.IsFinished) + // { + // Timeline.JumpToStart(); + // } + // // and otherwise apply their overlap mode + // else + // { + // if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart) + // Timeline.JumpToStart(); + // else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy) + // Timeline.AddExtraTimeline(); + // // The third option is ignore which is handled below: + // + // // done + // } + // } + // } DisplayConditionMet = Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle ? _toggledOnByEvent diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 47837646b..74d856f51 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Reflection; +using Artemis.Storage.Entities.Profile.Nodes; using Ninject; +using Ninject.Parameters; namespace Artemis.Core.Services { @@ -44,15 +46,25 @@ namespace Artemis.Core.Services string description = nodeAttribute?.Description ?? string.Empty; string category = nodeAttribute?.Category ?? string.Empty; - NodeData nodeData = new(plugin, nodeType, name, description, category, () => CreateNode(nodeType)); + NodeData nodeData = new(plugin, nodeType, name, description, category, (e) => CreateNode(e, nodeType)); return NodeTypeStore.Add(nodeData); } - private INode CreateNode(Type nodeType) + private INode CreateNode(NodeEntity? entity, Type nodeType) { INode node = _kernel.Get(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode"); + + if (entity != null) + { + node.X = entity.X; + node.Y = entity.Y; + node.Storage = entity.Storage; + } + if (node is CustomViewModelNode customViewModelNode) - customViewModelNode.BaseCustomViewModel = _kernel.Get(customViewModelNode.CustomViewModelType); + customViewModelNode.BaseCustomViewModel = _kernel.Get(customViewModelNode.CustomViewModelType, new ConstructorArgument("node", node)); + + node.Initialize(); return node; } diff --git a/src/Artemis.Core/Stores/NodeTypeStore.cs b/src/Artemis.Core/Stores/NodeTypeStore.cs index 2a4b84dd9..705a4d223 100644 --- a/src/Artemis.Core/Stores/NodeTypeStore.cs +++ b/src/Artemis.Core/Stores/NodeTypeStore.cs @@ -23,7 +23,7 @@ namespace Artemis.Core Registrations.Add(typeRegistration); } - OnDataBindingModifierAdded(new NodeTypeStoreEvent(typeRegistration)); + OnNodeTypeAdded(new NodeTypeStoreEvent(typeRegistration)); return typeRegistration; } @@ -38,7 +38,7 @@ namespace Artemis.Core typeRegistration.IsInStore = false; } - OnDataBindingModifierRemoved(new NodeTypeStoreEvent(typeRegistration)); + OnNodeTypeRemoved(new NodeTypeStoreEvent(typeRegistration)); } public static IEnumerable GetAll() @@ -59,19 +59,27 @@ namespace Artemis.Core #region Events - public static event EventHandler? DataBindingModifierAdded; - public static event EventHandler? DataBindingModifierRemoved; + public static event EventHandler? NodeTypeAdded; + public static event EventHandler? NodeTypeRemoved; - private static void OnDataBindingModifierAdded(NodeTypeStoreEvent e) + private static void OnNodeTypeAdded(NodeTypeStoreEvent e) { - DataBindingModifierAdded?.Invoke(null, e); + NodeTypeAdded?.Invoke(null, e); } - private static void OnDataBindingModifierRemoved(NodeTypeStoreEvent e) + private static void OnNodeTypeRemoved(NodeTypeStoreEvent e) { - DataBindingModifierRemoved?.Invoke(null, e); + NodeTypeRemoved?.Invoke(null, e); } #endregion + + public static Plugin? GetPlugin(INode node) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(r => r.Plugin.GetType().Assembly == node.GetType().Assembly)?.Plugin; + } + } } } diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs new file mode 100644 index 000000000..c53cf951e --- /dev/null +++ b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Artemis.Core +{ + public class CustomNodeViewModel : CorePropertyChanged + { + [JsonIgnore] + public INode Node { get; } + + public CustomNodeViewModel(INode node) + { + Node = node; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/InputPin.cs b/src/Artemis.Core/VisualScripting/InputPin.cs index 96309db07..dc88e37b0 100644 --- a/src/Artemis.Core/VisualScripting/InputPin.cs +++ b/src/Artemis.Core/VisualScripting/InputPin.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace Artemis.Core { @@ -32,6 +33,7 @@ namespace Artemis.Core #region Constructors + [JsonConstructor] internal InputPin(INode node, string name) : base(node, name) { } diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index ecd06cc3f..5a548a4b9 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -12,13 +12,15 @@ namespace Artemis.Core public double X { get; set; } public double Y { get; set; } + public object Storage { get; set; } public IReadOnlyCollection Pins { get; } public IReadOnlyCollection PinCollections { get; } event EventHandler Resetting; + void Initialize(); void Evaluate(); void Reset(); } -} +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 0862bdcd3..90a5f404e 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -42,6 +42,14 @@ namespace Artemis.Core set => SetAndNotify(ref _y, value); } + private object? _storage; + + public object? Storage + { + get => _storage; + set => SetAndNotify(ref _storage, value); + } + public virtual bool IsExitNode => false; private readonly List _pins = new(); @@ -128,6 +136,8 @@ namespace Artemis.Core return pin; } + public virtual void Initialize() { } + public abstract void Evaluate(); public virtual void Reset() @@ -138,7 +148,7 @@ namespace Artemis.Core #endregion } - public abstract class Node : CustomViewModelNode + public abstract class Node : CustomViewModelNode where T : CustomNodeViewModel { protected Node() { @@ -149,7 +159,7 @@ namespace Artemis.Core } public override Type CustomViewModelType => typeof(T); - public T? CustomViewModel => (T?) BaseCustomViewModel; + public T CustomViewModel => (T) BaseCustomViewModel!; } public abstract class CustomViewModelNode : Node diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index d08c155dd..7d21a0438 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.Core { @@ -13,13 +14,13 @@ namespace Artemis.Core public string Description { get; } public string Category { get; } - private Func _create; + private Func _create; #endregion #region Constructors - internal NodeData(Plugin plugin, Type type, string name, string description, string category, Func create) + internal NodeData(Plugin plugin, Type type, string name, string description, string category, Func? create) { this.Plugin = plugin; this.Type = type; @@ -33,7 +34,7 @@ namespace Artemis.Core #region Methods - public INode CreateNode() => _create(); + public INode CreateNode(NodeEntity? entity) => _create(entity); #endregion } diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index bccb384f0..8d30b5fbd 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Artemis.Core.Internal; using Artemis.Core.Properties; +using Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.Core { - public abstract class NodeScript : CorePropertyChanged, INodeScript + public abstract class NodeScript : CorePropertyChanged, INodeScript, IStorageModel { #region Properties & Fields + internal NodeScriptEntity Entity { get; } + public string Name { get; } public string Description { get; } @@ -18,6 +22,7 @@ namespace Artemis.Core protected INode ExitNode { get; set; } public abstract Type ResultType { get; } + public abstract void CreateExitNode(string name, string description = ""); #endregion @@ -27,6 +32,22 @@ namespace Artemis.Core { this.Name = name; this.Description = description; + this.Entity = new NodeScriptEntity(); + + NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeChanged; + NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; + } + + internal NodeScript(NodeScriptEntity entity) + { + this.Name = entity.Name; + this.Description = entity.Description; + this.Entity = entity; + + Load(); + + NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeChanged; + NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; } #endregion @@ -50,9 +71,203 @@ namespace Artemis.Core { _nodes.Remove(node); } - + public void Dispose() - { } + { + NodeTypeStore.NodeTypeAdded -= NodeTypeStoreOnNodeTypeChanged; + NodeTypeStore.NodeTypeRemoved -= NodeTypeStoreOnNodeTypeChanged; + } + + #endregion + + #region Implementation of IStorageModel + + /// + public void Load() + { + bool gotExitNode = false; + + // Create nodes + Dictionary nodes = new(); + foreach (NodeEntity entityNode in Entity.Nodes) + { + INode? node = LoadNode(entityNode); + if (node == null) + continue; + + if (node.IsExitNode) + gotExitNode = true; + + nodes.Add(entityNode.Id, node); + } + + if (!gotExitNode) + CreateExitNode("Exit node"); + + LoadConnections(nodes); + + _nodes.Clear(); + _nodes.AddRange(nodes.Values); + } + + private INode? LoadNode(NodeEntity nodeEntity) + { + INode node; + if (nodeEntity.IsExitNode) + { + CreateExitNode(nodeEntity.Name, nodeEntity.Description); + node = ExitNode; + } + else + { + NodeTypeRegistration? nodeTypeRegistration = NodeTypeStore.Get(nodeEntity.PluginId, nodeEntity.Type); + if (nodeTypeRegistration == null) + return null; + + // Create the node + node = nodeTypeRegistration.NodeData.CreateNode(nodeEntity); + } + + // Restore pin collections + foreach (NodePinCollectionEntity entityNodePinCollection in nodeEntity.PinCollections) + { + IPinCollection? collection = node.PinCollections.FirstOrDefault(c => c.Name == entityNodePinCollection.Name && + c.Type.Name == entityNodePinCollection.Type && + (int) c.Direction == entityNodePinCollection.Direction); + if (collection == null) + continue; + + while (collection.Count() < entityNodePinCollection.Amount) + collection.AddPin(); + } + + return node; + } + + private void LoadConnections(Dictionary nodes) + { + foreach (NodeConnectionEntity nodeConnectionEntity in Entity.Connections) + { + // Find the source and target node + if (!nodes.TryGetValue(nodeConnectionEntity.SourceNode, out INode? source) || !nodes.TryGetValue(nodeConnectionEntity.TargetNode, out INode? target)) + continue; + + IPin? sourcePin = nodeConnectionEntity.SourcePinCollectionId == -1 + ? source.Pins.ElementAtOrDefault(nodeConnectionEntity.SourcePinId) + : source.PinCollections.ElementAtOrDefault(nodeConnectionEntity.SourcePinCollectionId)?.ElementAtOrDefault(nodeConnectionEntity.SourcePinId); + IPin? targetPin = nodeConnectionEntity.TargetPinCollectionId == -1 + ? target.Pins.ElementAtOrDefault(nodeConnectionEntity.TargetPinId) + : target.PinCollections.ElementAtOrDefault(nodeConnectionEntity.TargetPinCollectionId)?.ElementAtOrDefault(nodeConnectionEntity.TargetPinId); + + // Ensure both nodes have the required pins + if (sourcePin == null || targetPin == null) + continue; + + targetPin.ConnectTo(sourcePin); + sourcePin.ConnectTo(targetPin); + } + } + + /// + public void Save() + { + Entity.Name = Name; + Entity.Description = Description; + + Entity.Nodes.Clear(); + int id = 0; + + Dictionary nodes = new(); + foreach (INode node in Nodes) + { + NodeEntity nodeEntity = new() + { + Id = id, + PluginId = NodeTypeStore.GetPlugin(node)?.Guid ?? Constants.CorePlugin.Guid, + Type = node.GetType().Name, + X = node.X, + Y = node.Y, + Storage = node.Storage, + Name = node.Name, + Description = node.Description, + IsExitNode = node.IsExitNode + }; + + foreach (IPinCollection nodePinCollection in node.PinCollections) + { + nodeEntity.PinCollections.Add(new NodePinCollectionEntity + { + Name = nodePinCollection.Name, + Type = nodePinCollection.Type.Name, + Direction = (int) nodePinCollection.Direction, + Amount = nodePinCollection.Count() + }); + } + + Entity.Nodes.Add(nodeEntity); + nodes.Add(node, id); + id++; + } + + // Store connections + Entity.Connections.Clear(); + foreach (INode node in Nodes) + { + SavePins(nodes, node, -1, node.Pins); + + int pinCollectionId = 0; + foreach (IPinCollection pinCollection in node.PinCollections) + { + SavePins(nodes, node, pinCollectionId, pinCollection); + pinCollectionId++; + } + } + } + + private void SavePins(Dictionary nodes, INode node, int collectionId, IEnumerable pins) + { + int sourcePinId = 0; + foreach (IPin sourcePin in pins.Where(p => p.Direction == PinDirection.Input)) + { + foreach (IPin targetPin in sourcePin.ConnectedTo) + { + int targetPinCollectionId = -1; + int targetPinId; + + IPinCollection? targetCollection = targetPin.Node.PinCollections.FirstOrDefault(c => c.Contains(targetPin)); + if (targetCollection != null) + { + targetPinCollectionId = targetPin.Node.PinCollections.IndexOf(targetCollection); + targetPinId = targetCollection.ToList().IndexOf(targetPin); + } + else + targetPinId = targetPin.Node.Pins.IndexOf(targetPin); + + Entity.Connections.Add(new NodeConnectionEntity() + { + SourceType = sourcePin.Type.Name, + SourceNode = nodes[node], + SourcePinCollectionId = collectionId, + SourcePinId = sourcePinId, + TargetType = targetPin.Type.Name, + TargetNode = nodes[targetPin.Node], + TargetPinCollectionId = targetPinCollectionId, + TargetPinId = targetPinId, + }); + } + + sourcePinId++; + } + } + + #endregion + + #region Event handlers + + private void NodeTypeStoreOnNodeTypeChanged(object? sender, NodeTypeStoreEvent e) + { + Load(); + } #endregion } @@ -61,14 +276,24 @@ namespace Artemis.Core { #region Properties & Fields - public T Result => ((ExitNode)ExitNode).Value; + public T Result => ((ExitNode) ExitNode).Value; public override Type ResultType => typeof(T); + public override void CreateExitNode(string name, string description = "") + { + ExitNode = new ExitNode(name, description); + } + #endregion #region Constructors + internal NodeScript(NodeScriptEntity entity) + : base(entity) + { + } + public NodeScript(string name, string description) : base(name, description) { @@ -86,4 +311,4 @@ namespace Artemis.Core #endregion } -} +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/OutputPin.cs b/src/Artemis.Core/VisualScripting/OutputPin.cs index e52a21d0e..0aec65d05 100644 --- a/src/Artemis.Core/VisualScripting/OutputPin.cs +++ b/src/Artemis.Core/VisualScripting/OutputPin.cs @@ -1,4 +1,5 @@ using System; +using Newtonsoft.Json; namespace Artemis.Core { @@ -31,6 +32,7 @@ namespace Artemis.Core #region Constructors + [JsonConstructor] internal OutputPin(INode node, string name) : base(node, name) { } diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index 8e9cd1db2..8dc4c6044 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Conditions; +using Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.Storage.Entities.Profile.Abstract { @@ -15,5 +16,7 @@ namespace Artemis.Storage.Entities.Profile.Abstract public DataModelConditionGroupEntity DisplayCondition { get; set; } public TimelineEntity Timeline { get; set; } + + public NodeScriptEntity NodeScript { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Nodes/NodeConnectionEntity.cs b/src/Artemis.Storage/Entities/Profile/Nodes/NodeConnectionEntity.cs new file mode 100644 index 000000000..1536f31ab --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Nodes/NodeConnectionEntity.cs @@ -0,0 +1,14 @@ +namespace Artemis.Storage.Entities.Profile.Nodes +{ + public class NodeConnectionEntity + { + public string SourceType { get; set; } + public int SourceNode { get; set; } + public int TargetNode { get; set; } + public int SourcePinCollectionId { get; set; } + public int SourcePinId { get; set; } + public string TargetType { get; set; } + public int TargetPinCollectionId { get; set; } + public int TargetPinId { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs b/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs new file mode 100644 index 000000000..6f33425f0 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile.Nodes +{ + public class NodeEntity + { + public NodeEntity() + { + PinCollections = new List(); + } + + public int Id { get; set; } + public string Type { get; set; } + public Guid PluginId { get; set; } + + public string Name { get; set; } + public string Description { get; set; } + public bool IsExitNode { get; set; } + public double X { get; set; } + public double Y { get; set; } + public object Storage { get; set; } + + public List PinCollections { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Nodes/NodePinCollectionEntity.cs b/src/Artemis.Storage/Entities/Profile/Nodes/NodePinCollectionEntity.cs new file mode 100644 index 000000000..61e65eac4 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Nodes/NodePinCollectionEntity.cs @@ -0,0 +1,10 @@ +namespace Artemis.Storage.Entities.Profile.Nodes +{ + public class NodePinCollectionEntity + { + public string Name { get; set; } + public string Type { get; set; } + public int Direction { set; get; } + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Nodes/NodeScriptEntity.cs b/src/Artemis.Storage/Entities/Profile/Nodes/NodeScriptEntity.cs new file mode 100644 index 000000000..7f6659d46 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Nodes/NodeScriptEntity.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile.Nodes +{ + public class NodeScriptEntity + { + public NodeScriptEntity() + { + Nodes = new List(); + Connections = new List(); + } + + public string Name { get; set; } + public string Description { get; set; } + + public List Nodes { get; set; } + public List Connections { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index cab03eb16..4a5da0c7b 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -1482,6 +1482,7 @@ "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", + "Artemis.UI.Shared": "2.0.0", "JetBrains.Annotations": "2021.1.0", "Stylet": "1.3.6" } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index 44e4e5ba7..bf5de01e7 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -15,7 +15,7 @@ x64 - bin\Artemis.VisualScripting.xml + 5 @@ -42,6 +42,7 @@ + diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index f9e37b791..bd47ec9ee 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -383,7 +383,7 @@ namespace Artemis.VisualScripting.Editor.Controls if (_creationBoxParent.ContextMenu != null) _creationBoxParent.ContextMenu.IsOpen = false; - INode node = nodeData.CreateNode(); + INode node = nodeData.CreateNode(null); Script.AddNode(node); InitializeNode(node, _lastRightClickLocation); diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml index ccb72a60c..c124dec3b 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml @@ -207,9 +207,18 @@ - - + + + + + + + + + + + diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs new file mode 100644 index 000000000..1578dcd9d --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -0,0 +1,42 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Input; +using Artemis.UI.Shared.Services; +using Stylet; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public class DataModelNodeCustomViewModel : CustomNodeViewModel + { + private readonly DataModelNode _node; + + public DataModelNodeCustomViewModel(DataModelNode node, IDataModelUIService dataModelUIService) : base(node) + { + _node = node; + + Execute.OnUIThreadSync(() => + { + SelectionViewModel = dataModelUIService.GetDynamicSelectionViewModel(module: null); + SelectionViewModel.PropertySelected += SelectionViewModelOnPropertySelected; + }); + } + + public DataModelDynamicViewModel SelectionViewModel { get; set; } + + private void SelectionViewModelOnPropertySelected(object? sender, DataModelInputDynamicEventArgs e) + { + _node.DataModelPath = SelectionViewModel.DataModelPath; + if (_node.DataModelPath != null) + { + _node.DataModelPath.Save(); + _node.Storage = _node.DataModelPath.Entity; + } + else + { + _node.Storage = null; + } + + _node.UpdateOutputPin(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs index 60b44dcca..1a0db0a2e 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs @@ -1,15 +1,24 @@ -using Stylet; +using Artemis.Core; namespace Artemis.VisualScripting.Nodes.CustomViewModels { - public class StaticDoubleValueNodeCustomViewModel : PropertyChangedBase + public class StaticDoubleValueNodeCustomViewModel : CustomNodeViewModel { - private double _input; + private readonly StaticDoubleValueNode _node; + + public StaticDoubleValueNodeCustomViewModel(StaticDoubleValueNode node) : base(node) + { + _node = node; + } public double Input { - get => _input; - set => SetAndNotify(ref _input, value); + get => (double) _node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs index 2860569b1..d07f21437 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs @@ -1,15 +1,24 @@ -using Stylet; +using Artemis.Core; namespace Artemis.VisualScripting.Nodes.CustomViewModels { - public class StaticIntegerValueNodeCustomViewModel : PropertyChangedBase + public class StaticIntegerValueNodeCustomViewModel : CustomNodeViewModel { - private int _input; + private readonly StaticIntegerValueNode _node; + + public StaticIntegerValueNodeCustomViewModel(StaticIntegerValueNode node) : base(node) + { + _node = node; + } public int Input { - get => _input; - set => SetAndNotify(ref _input, value); + get => (int)(long) _node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs index b4d76920b..1b6f3eae4 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs @@ -1,15 +1,24 @@ -using Stylet; +using Artemis.Core; namespace Artemis.VisualScripting.Nodes.CustomViewModels { - public class StaticStringValueNodeCustomViewModel : PropertyChangedBase + public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel { - private string _input; + private readonly StaticStringValueNode _node; + + public StaticStringValueNodeCustomViewModel(StaticStringValueNode node) : base(node) + { + _node = node; + } public string Input { - get => _input; - set => SetAndNotify(ref _input, value); + get => (string) _node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml new file mode 100644 index 000000000..755de1d42 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml @@ -0,0 +1,11 @@ + + + diff --git a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs new file mode 100644 index 000000000..95ede78ff --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Artemis.Core; +using Artemis.Storage.Entities.Profile; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes +{ + [Node("Data Model-Value", "Outputs a selectable data model value.")] + public class DataModelNode : Node + { + public DataModelNode() : base("Data Model", "Outputs a selectable data model value") + { + } + + public OutputPin Output { get; private set; } + public DataModelPath DataModelPath { get; set; } + + public override void Initialize() + { + if (Storage is not DataModelPathEntity pathEntity) + return; + + DataModelPath = new DataModelPath(null, pathEntity); + CustomViewModel.SelectionViewModel.ChangeDataModelPath(DataModelPath); + UpdateOutputPin(); + } + + public override void Evaluate() + { + if (DataModelPath.IsValid && Output != null) + Output.Value = DataModelPath.GetValue()!; + } + + public void UpdateOutputPin() + { + if (Pins.Contains(Output)) + { + RemovePin(Output); + Output = null; + } + + Type type = DataModelPath?.GetPropertyType(); + if (type != null) + Output = CreateOutputPin(type); + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs index df95609f6..b0d124511 100644 --- a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs +++ b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs @@ -26,9 +26,11 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { - Output.Value = CustomViewModel.Input; + Output.Value = (int) (Storage as long? ?? 0); } + public override void Initialize() => Storage ??= 0; + #endregion } @@ -55,9 +57,11 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { - Output.Value = CustomViewModel.Input; + Output.Value = Storage as double? ?? 0.0; } + public override void Initialize() => Storage ??= 0.0; + #endregion } @@ -84,9 +88,9 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { - Output.Value = CustomViewModel.Input; + Output.Value = Storage as string; } - + #endregion } diff --git a/src/Artemis.VisualScripting/packages.lock.json b/src/Artemis.VisualScripting/packages.lock.json index f7b9686f5..9772bbc7d 100644 --- a/src/Artemis.VisualScripting/packages.lock.json +++ b/src/Artemis.VisualScripting/packages.lock.json @@ -57,6 +57,28 @@ "resolved": "5.0.10", "contentHash": "x70WuqMDuP75dajqSLvO+AnI/BbwS6da+ukTO7rueV7VoXoQ5CRA9FV4r7cOS4OUr2NS1Up7LDIutjCxQycRvg==" }, + "MaterialDesignColors": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "Azl8nN23SD6QPE0PdsfpKiIqWTvH7rzXwgXPiFSEt91NFOrwB5cx3iq/sbINWMZunhXJ32+jVUHiV03B8eJbZw==" + }, + "MaterialDesignExtensions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "dlxWtdrMH8aHNib3dWJhNQ/nNiA2b/CNvr90w/5KB6erTisuTpyYVx2l2+UGCZvwhSX5mHTHQYHfjgAKbDrgjg==", + "dependencies": { + "MaterialDesignColors": "1.2.7", + "MaterialDesignThemes": "3.2.0" + } + }, + "MaterialDesignThemes": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "WqrO9AbtdE4pLPtDk/C5BZRnkgWFwVGyUHWj7tRJrgnKl089DEobVXBCLeqp2mkgBeFHj4Xe3AfWyhmlnO6AZA==", + "dependencies": { + "MaterialDesignColors": "2.0.1" + } + }, "McMaster.NETCore.Plugins": { "type": "Transitive", "resolved": "1.4.0", @@ -104,6 +126,11 @@ "Microsoft.NETCore.Platforms": "3.0.0" } }, + "Microsoft.Xaml.Behaviors.Wpf": { + "type": "Transitive", + "resolved": "1.1.31", + "contentHash": "LZpuf82ACZWldmfMuv3CTUMDh3o0xo0uHUaybR5HgqVLDBJJ9RZLykplQ/bTJd0/VDt3EhD4iDgUgbdIUAM+Kg==" + }, "NETStandard.Library": { "type": "Transitive", "resolved": "1.6.1", @@ -336,6 +363,11 @@ "System.Threading.Timer": "4.0.1" } }, + "SharpVectors.Reloaded": { + "type": "Transitive", + "resolved": "1.7.5", + "contentHash": "v9U5sSMGFE2zCMbh42BYHkaRYkmwwhsKMGcNRdHAKqD1ryOf4mhqnJR0o07hwg5KIEmCI9bDdrgYSmiZWlL+eA==" + }, "SkiaSharp": { "type": "Transitive", "resolved": "2.80.2", @@ -344,6 +376,23 @@ "System.Memory": "4.5.3" } }, + "SkiaSharp.Views.Desktop.Common": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "0vBvweMysgl1wgjuTQUhdJMD5z5nBjtYqmnHPeX+qHfkc336Wj2L3jEqwmGb0YP+RV47gFGz0EzMAW6szZch9w==", + "dependencies": { + "SkiaSharp": "2.80.2" + } + }, + "SkiaSharp.Views.WPF": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "Fzo2+MNwHDh9Cob8sk7OO26kp3bhofjXMwlEK8IncF1ehu9hi3sH9iQDJrue9a88VEJJ+yyLISPUFcmXlGHSyQ==", + "dependencies": { + "SkiaSharp": "2.80.2", + "SkiaSharp.Views.Desktop.Common": "2.80.2" + } + }, "System.AppContext": { "type": "Transitive", "resolved": "4.3.0", @@ -1266,6 +1315,24 @@ "LiteDB": "5.0.10", "Serilog": "2.10.0" } + }, + "artemis.ui.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Humanizer.Core": "2.11.10", + "MaterialDesignExtensions": "3.3.0", + "MaterialDesignThemes": "4.1.0", + "Microsoft.Xaml.Behaviors.Wpf": "1.1.31", + "Ninject": "3.3.4", + "Ninject.Extensions.Conventions": "3.3.0", + "SharpVectors.Reloaded": "1.7.5", + "SkiaSharp": "2.80.2", + "SkiaSharp.Views.WPF": "2.80.2", + "Stylet": "1.3.6", + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.5.0" + } } } }