diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs
index 0de01d907..92ee2d1f1 100644
--- a/src/Artemis.Core/Services/NodeService.cs
+++ b/src/Artemis.Core/Services/NodeService.cs
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using System.Security.Cryptography;
+using System.Text;
using Artemis.Storage.Entities.Profile.Nodes;
-using Newtonsoft.Json;
using Ninject;
-using Ninject.Parameters;
using SkiaSharp;
namespace Artemis.Core.Services
@@ -37,9 +37,19 @@ namespace Artemis.Core.Services
#region Methods
///
- public TypeColorRegistration? GetTypeColor(Type type)
+ public TypeColorRegistration GetTypeColorRegistration(Type type)
{
- return NodeTypeStore.GetColor(type);
+ TypeColorRegistration? match = NodeTypeStore.GetColor(type);
+ if (match != null)
+ return match;
+
+ // Come up with a random color based on the type name that should be the same each time
+ MD5 md5Hasher = MD5.Create();
+ byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!));
+ int hash = BitConverter.ToInt32(hashed, 0);
+
+ SKColor baseColor = SKColor.FromHsl(hash % 255, 50 + hash % 50, 50);
+ return new TypeColorRegistration(type, baseColor, Constants.CorePlugin);
}
public NodeTypeRegistration RegisterNodeType(Plugin plugin, Type nodeType)
@@ -53,7 +63,7 @@ namespace Artemis.Core.Services
string name = nodeAttribute?.Name ?? nodeType.Name;
string description = nodeAttribute?.Description ?? string.Empty;
string category = nodeAttribute?.Category ?? string.Empty;
-
+
NodeData nodeData = new(plugin, nodeType, name, description, category, nodeAttribute?.InputType, nodeAttribute?.OutputType, (s, e) => CreateNode(s, e, nodeType));
return NodeTypeStore.Add(nodeData);
}
@@ -105,7 +115,7 @@ namespace Artemis.Core.Services
///
/// Gets the best matching registration for the provided type
///
- TypeColorRegistration? GetTypeColor(Type type);
+ TypeColorRegistration GetTypeColorRegistration(Type type);
///
/// Registers a node of the provided
diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs
index 5e4aeef46..dd109637b 100644
--- a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs
+++ b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs
@@ -12,16 +12,6 @@ namespace Artemis.Core
///
public INode Node { get; }
- ///
- /// Called whenever the custom view model is activated
- ///
- void OnActivate();
-
- ///
- /// Called whenever the custom view model is closed
- ///
- void OnDeactivate();
-
///
/// Occurs whenever the node was modified by the view model
///
diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs
index b924b0ae4..263d995e8 100644
--- a/src/Artemis.Core/VisualScripting/NodeScript.cs
+++ b/src/Artemis.Core/VisualScripting/NodeScript.cs
@@ -125,9 +125,6 @@ namespace Artemis.Core
_nodes.Remove(node);
}
- if (node is IDisposable disposable)
- disposable.Dispose();
-
NodeRemoved?.Invoke(this, new SingleValueEventArgs(node));
}
diff --git a/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs
index 314cdb989..7aa20d342 100644
--- a/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs
+++ b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs
@@ -25,7 +25,7 @@ namespace Artemis.UI.Shared
if (type == typeof(object))
return (SKColors.White.ToColor(), SKColors.White.Darken(0.35f).ToColor());
- TypeColorRegistration? typeColorRegistration = NodeService?.GetTypeColor(type);
+ TypeColorRegistration? typeColorRegistration = NodeService?.GetTypeColorRegistration(type);
if (typeColorRegistration != null)
return (typeColorRegistration.Color.ToColor(), typeColorRegistration.DarkenedColor.ToColor());
diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs
index 1f24ff5ba..56ae49395 100644
--- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs
+++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs
@@ -154,12 +154,12 @@ namespace Artemis.VisualScripting.Editor.Controls
private void GetCustomViewModel()
{
- CustomViewModel?.OnDeactivate();
+ // CustomViewModel?.OnDeactivate();
if (Node?.Node is Node customViewModelNode)
{
CustomViewModel = customViewModelNode.GetCustomViewModel();
- CustomViewModel?.OnActivate();
+ // CustomViewModel?.OnActivate();
}
else
CustomViewModel = null;
@@ -182,7 +182,7 @@ namespace Artemis.VisualScripting.Editor.Controls
if (CustomViewModel != null)
{
CustomViewModel.NodeModified -= CustomViewModelOnNodeModified;
- CustomViewModel.OnDeactivate();
+ // CustomViewModel.OnDeactivate();
}
CustomViewModel = null;
diff --git a/src/Artemis.sln b/src/Artemis.sln
index 46d3ed238..9eb974685 100644
--- a/src/Artemis.sln
+++ b/src/Artemis.sln
@@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Linux", "Avaloni
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.MacOS", "Avalonia\Artemis.UI.MacOS\Artemis.UI.MacOS.csproj", "{2F5F16DC-FACF-4559-9882-37C2949814C7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.VisualScripting", "Avalonia\Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +105,14 @@ Global
{2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|Any CPU.Build.0 = Release|Any CPU
{2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|x64.ActiveCfg = Release|Any CPU
{2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|x64.Build.0 = Release|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|x64.Build.0 = Debug|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|Any CPU.Build.0 = Release|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|Any CPU
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -113,6 +123,7 @@ Global
{DE45A288-9320-461F-BE2A-26DFE3817216} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042}
{9012C8E2-3BEC-42F5-8270-7352A5922B04} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042}
{2F5F16DC-FACF-4559-9882-37C2949814C7} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042}
+ {412B921A-26F5-4AE6-8B32-0C19BE54F421} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C203080A-4473-4CC2-844B-F552EA43D66A}
diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json
index 2ea08bb81..748e8244f 100644
--- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json
+++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json
@@ -426,6 +426,11 @@
"resolved": "5.0.0",
"contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q=="
},
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "5.0.9",
+ "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -544,6 +549,14 @@
"Ninject": "3.3.3"
}
},
+ "NoStringEvaluating": {
+ "type": "Transitive",
+ "resolved": "2.2.2",
+ "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==",
+ "dependencies": {
+ "Microsoft.Extensions.ObjectPool": "5.0.9"
+ }
+ },
"ReactiveUI.Validation": {
"type": "Transitive",
"resolved": "2.2.1",
@@ -1758,6 +1771,7 @@
"dependencies": {
"Artemis.Core": "1.0.0",
"Artemis.UI.Shared": "1.0.0",
+ "Artemis.VisualScripting": "1.0.0",
"Avalonia": "0.10.13",
"Avalonia.Controls.PanAndZoom": "10.12.0",
"Avalonia.Desktop": "0.10.13",
@@ -1795,6 +1809,19 @@
"ReactiveUI.Validation": "2.2.1",
"SkiaSharp": "2.88.0-preview.178"
}
+ },
+ "artemis.visualscripting": {
+ "type": "Project",
+ "dependencies": {
+ "Artemis.Core": "1.0.0",
+ "Artemis.UI.Shared": "1.0.0",
+ "Avalonia": "0.10.13",
+ "Avalonia.ReactiveUI": "0.10.13",
+ "Ninject": "3.3.4",
+ "NoStringEvaluating": "2.2.2",
+ "ReactiveUI": "17.1.50",
+ "SkiaSharp": "2.88.0-preview.178"
+ }
}
}
}
diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json
index 2ea08bb81..748e8244f 100644
--- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json
+++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json
@@ -426,6 +426,11 @@
"resolved": "5.0.0",
"contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q=="
},
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "5.0.9",
+ "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -544,6 +549,14 @@
"Ninject": "3.3.3"
}
},
+ "NoStringEvaluating": {
+ "type": "Transitive",
+ "resolved": "2.2.2",
+ "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==",
+ "dependencies": {
+ "Microsoft.Extensions.ObjectPool": "5.0.9"
+ }
+ },
"ReactiveUI.Validation": {
"type": "Transitive",
"resolved": "2.2.1",
@@ -1758,6 +1771,7 @@
"dependencies": {
"Artemis.Core": "1.0.0",
"Artemis.UI.Shared": "1.0.0",
+ "Artemis.VisualScripting": "1.0.0",
"Avalonia": "0.10.13",
"Avalonia.Controls.PanAndZoom": "10.12.0",
"Avalonia.Desktop": "0.10.13",
@@ -1795,6 +1809,19 @@
"ReactiveUI.Validation": "2.2.1",
"SkiaSharp": "2.88.0-preview.178"
}
+ },
+ "artemis.visualscripting": {
+ "type": "Project",
+ "dependencies": {
+ "Artemis.Core": "1.0.0",
+ "Artemis.UI.Shared": "1.0.0",
+ "Avalonia": "0.10.13",
+ "Avalonia.ReactiveUI": "0.10.13",
+ "Ninject": "3.3.4",
+ "NoStringEvaluating": "2.2.2",
+ "ReactiveUI": "17.1.50",
+ "SkiaSharp": "2.88.0-preview.178"
+ }
}
}
}
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs
index 8a3060d13..e0136d83f 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/DataModelUIService.cs
@@ -38,7 +38,7 @@ namespace Artemis.UI.Shared.Services
public DataModelPropertiesViewModel GetMainDataModelVisualization()
{
DataModelPropertiesViewModel viewModel = new(null, null, null);
- foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion).OrderBy(d => d.DataModelDescription.Name))
+ foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion || d.Module.IsActivated).OrderBy(d => d.DataModelDescription.Name))
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion)));
// Update to populate children
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs
new file mode 100644
index 000000000..2ac132a0f
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs
@@ -0,0 +1,53 @@
+using System;
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
+
+///
+/// Represents a node editor command that can be used to add a node.
+///
+public class AddNode : INodeEditorCommand, IDisposable
+{
+ private readonly INodeScript _nodeScript;
+ private readonly INode _node;
+ private bool _isRemoved;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The node script the node belongs to.
+ /// The node to delete.
+ public AddNode(INodeScript nodeScript, INode node)
+ {
+ _nodeScript = nodeScript;
+ _node = node;
+ }
+
+ ///
+ public string DisplayName => $"Add '{_node.Name}' node";
+
+ ///
+ public void Execute()
+ {
+ _nodeScript.AddNode(_node);
+ _isRemoved = false;
+ }
+
+ ///
+ public void Undo()
+ {
+ _nodeScript.RemoveNode(_node);
+ _isRemoved = true;
+ }
+
+ #region IDisposable
+
+ ///
+ public void Dispose()
+ {
+ if (_isRemoved)
+ _nodeScript.Dispose();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/CompositeCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/CompositeCommand.cs
new file mode 100644
index 000000000..6d0c3eaa2
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/CompositeCommand.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
+
+///
+/// Represents a profile editor command that can be used to combine multiple commands into one.
+///
+public class CompositeCommand : INodeEditorCommand, IDisposable
+{
+ private bool _ignoreNextExecute;
+ private readonly List _commands;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The commands to execute.
+ /// The display name of the composite command.
+ public CompositeCommand(IEnumerable commands, string displayName)
+ {
+ if (commands == null)
+ throw new ArgumentNullException(nameof(commands));
+ _commands = commands.ToList();
+ DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
+ }
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The commands to execute.
+ /// The display name of the composite command.
+ /// Whether or not to ignore the first execute because commands are already executed
+ internal CompositeCommand(IEnumerable commands, string displayName, bool ignoreFirstExecute)
+ {
+ if (commands == null)
+ throw new ArgumentNullException(nameof(commands));
+
+ _ignoreNextExecute = ignoreFirstExecute;
+ _commands = commands.ToList();
+ DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (INodeEditorCommand NodeEditorCommand in _commands)
+ if (NodeEditorCommand is IDisposable disposable)
+ disposable.Dispose();
+ }
+
+ #region Implementation of INodeEditorCommand
+
+ ///
+ public string DisplayName { get; }
+
+ ///
+ public void Execute()
+ {
+ if (_ignoreNextExecute)
+ {
+ _ignoreNextExecute = false;
+ return;
+ }
+
+ foreach (INodeEditorCommand NodeEditorCommand in _commands)
+ NodeEditorCommand.Execute();
+ }
+
+ ///
+ public void Undo()
+ {
+ // Undo in reverse by iterating from the back
+ for (int index = _commands.Count - 1; index >= 0; index--)
+ _commands[index].Undo();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs
new file mode 100644
index 000000000..6c1577427
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
+
+///
+/// Represents a node editor command that can be used to delete a node.
+///
+public class DeleteNode : INodeEditorCommand, IDisposable
+{
+ private readonly INode _node;
+ private readonly INodeScript _nodeScript;
+ private readonly Dictionary> _pinConnections = new();
+ private bool _isRemoved;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The node script the node belongs to.
+ /// The node to delete.
+ public DeleteNode(INodeScript nodeScript, INode node)
+ {
+ _nodeScript = nodeScript;
+ _node = node;
+ }
+
+ private void StoreConnections()
+ {
+ _pinConnections.Clear();
+ foreach (IPin nodePin in _node.Pins)
+ {
+ _pinConnections.Add(nodePin, nodePin.ConnectedTo);
+ nodePin.DisconnectAll();
+ }
+
+ foreach (IPinCollection nodePinCollection in _node.PinCollections)
+ {
+ foreach (IPin nodePin in nodePinCollection)
+ {
+ _pinConnections.Add(nodePin, nodePin.ConnectedTo);
+ nodePin.DisconnectAll();
+ }
+ }
+ }
+
+ private void RestoreConnections()
+ {
+ foreach (IPin nodePin in _node.Pins)
+ {
+ if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections))
+ foreach (IPin connection in connections)
+ nodePin.ConnectTo(connection);
+ }
+
+ foreach (IPinCollection nodePinCollection in _node.PinCollections)
+ {
+ foreach (IPin nodePin in nodePinCollection)
+ {
+ if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections))
+ foreach (IPin connection in connections)
+ nodePin.ConnectTo(connection);
+ }
+ }
+
+ _pinConnections.Clear();
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_isRemoved)
+ _nodeScript.Dispose();
+ }
+
+ ///
+ public string DisplayName => $"Delete '{_node.Name}' node";
+
+ ///
+ public void Execute()
+ {
+ StoreConnections();
+ _nodeScript.RemoveNode(_node);
+
+ _isRemoved = true;
+ }
+
+ ///
+ public void Undo()
+ {
+ _nodeScript.AddNode(_node);
+ RestoreConnections();
+
+ _isRemoved = false;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs
new file mode 100644
index 000000000..0df32684d
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs
@@ -0,0 +1,48 @@
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
+
+///
+/// Represents a node editor command that can be used to move a node.
+///
+public class MoveNode : INodeEditorCommand
+{
+ private readonly INode _node;
+ private readonly double _originalX;
+ private readonly double _originalY;
+ private readonly double _x;
+ private readonly double _y;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The node to update.
+ /// The new X-position.
+ /// The new Y-position.
+ public MoveNode(INode node, double x, double y)
+ {
+ _node = node;
+ _x = x;
+ _y = y;
+
+ _originalX = node.X;
+ _originalY = node.Y;
+ }
+
+ ///
+ public string DisplayName => "Move node";
+
+ ///
+ public void Execute()
+ {
+ _node.X = _x;
+ _node.Y = _y;
+ }
+
+ ///
+ public void Undo()
+ {
+ _node.X = _originalX;
+ _node.Y = _originalY;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorCommand.cs
new file mode 100644
index 000000000..ae85d13ef
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorCommand.cs
@@ -0,0 +1,23 @@
+namespace Artemis.UI.Shared.Services.NodeEditor
+{
+ ///
+ /// Represents a command that can be executed and if needed, undone
+ ///
+ public interface INodeEditorCommand
+ {
+ ///
+ /// Gets the name of the command
+ ///
+ string DisplayName { get; }
+
+ ///
+ /// Executes the command
+ ///
+ void Execute();
+
+ ///
+ /// Undoes the command
+ ///
+ void Undo();
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorService.cs
new file mode 100644
index 000000000..c3fdf4864
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/INodeEditorService.cs
@@ -0,0 +1,31 @@
+using System;
+using Artemis.Core;
+using Artemis.UI.Shared.Services.Interfaces;
+using Artemis.UI.Shared.Services.ProfileEditor;
+
+namespace Artemis.UI.Shared.Services.NodeEditor;
+
+public interface INodeEditorService : IArtemisSharedUIService
+{
+ ///
+ /// Gets the editor history for the provided node script.
+ ///
+ /// The node script to get the editor history for.
+ /// The node editor history of the given node script.
+ NodeEditorHistory GetHistory(INodeScript nodeScript);
+
+ ///
+ /// Executes the provided command and adds it to the history.
+ ///
+ /// The node script to execute the command upon.
+ /// The command to execute.
+ void ExecuteCommand(INodeScript nodeScript, INodeEditorCommand command);
+
+ ///
+ /// Creates a new command scope which can be used to group undo/redo actions of multiple commands.
+ ///
+ /// The node script to create the scope for.
+ /// The name of the command scope.
+ /// The command scope that will group any commands until disposed.
+ NodeEditorCommandScope CreateCommandScope(INodeScript nodeScript, string name);
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorCommandScope.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorCommandScope.cs
new file mode 100644
index 000000000..fbd40520c
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorCommandScope.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.NodeEditor;
+
+///
+/// Represents a scope in which editor commands are executed until disposed.
+///
+public class NodeEditorCommandScope : IDisposable
+{
+ private readonly List _commands;
+
+ private readonly NodeEditorService _nodeEditorService;
+ private readonly INodeScript _nodeScript;
+
+ internal NodeEditorCommandScope(NodeEditorService nodeEditorService, INodeScript nodeScript, string name)
+ {
+ Name = name;
+ _nodeEditorService = nodeEditorService;
+ _nodeScript = nodeScript;
+ _commands = new List();
+ }
+
+ ///
+ /// Gets the name of the scope.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets a read only collection of commands in the scope.
+ ///
+ public ReadOnlyCollection NodeEditorCommands => new(_commands);
+
+ internal void AddCommand(INodeEditorCommand command)
+ {
+ command.Execute();
+ _commands.Add(command);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _nodeEditorService.StopCommandScope(_nodeScript);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorHistory.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorHistory.cs
new file mode 100644
index 000000000..006e4adeb
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorHistory.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Artemis.Core;
+using ReactiveUI;
+
+namespace Artemis.UI.Shared.Services.NodeEditor;
+
+public class NodeEditorHistory
+{
+ private readonly Subject _canRedo = new();
+ private readonly Subject _canUndo = new();
+ private readonly Stack _redoCommands = new();
+ private readonly Stack _undoCommands = new();
+
+ public NodeEditorHistory(INodeScript nodeScript)
+ {
+ NodeScript = nodeScript;
+
+ Execute = ReactiveCommand.Create(ExecuteEditorCommand);
+ Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
+ Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
+ }
+
+ public INodeScript NodeScript { get; }
+ public IObservable CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
+ public IObservable CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
+
+ public ReactiveCommand Execute { get; }
+ public ReactiveCommand Undo { get; }
+ public ReactiveCommand Redo { get; }
+
+ public void Clear()
+ {
+ ClearRedo();
+ ClearUndo();
+ UpdateSubjects();
+ }
+
+ public void ExecuteEditorCommand(INodeEditorCommand command)
+ {
+ command.Execute();
+
+ _undoCommands.Push(command);
+ ClearRedo();
+ UpdateSubjects();
+ }
+
+ private void ClearRedo()
+ {
+ foreach (INodeEditorCommand nodeEditorCommand in _redoCommands)
+ if (nodeEditorCommand is IDisposable disposable)
+ disposable.Dispose();
+
+ _redoCommands.Clear();
+ }
+
+ private void ClearUndo()
+ {
+ foreach (INodeEditorCommand nodeEditorCommand in _undoCommands)
+ if (nodeEditorCommand is IDisposable disposable)
+ disposable.Dispose();
+
+ _undoCommands.Clear();
+ }
+
+ private INodeEditorCommand? ExecuteUndo()
+ {
+ if (!_undoCommands.TryPop(out INodeEditorCommand? command))
+ return null;
+
+ command.Undo();
+ _redoCommands.Push(command);
+ UpdateSubjects();
+
+ return command;
+ }
+
+ private INodeEditorCommand? ExecuteRedo()
+ {
+ if (!_redoCommands.TryPop(out INodeEditorCommand? command))
+ return null;
+
+ command.Execute();
+ _undoCommands.Push(command);
+ UpdateSubjects();
+
+ return command;
+ }
+
+ private void UpdateSubjects()
+ {
+ _canUndo.OnNext(_undoCommands.Any());
+ _canRedo.OnNext(_redoCommands.Any());
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorService.cs
new file mode 100644
index 000000000..5f717b0e3
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/NodeEditorService.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using Artemis.Core;
+using Artemis.UI.Shared.Services.Interfaces;
+using Artemis.UI.Shared.Services.NodeEditor.Commands;
+
+namespace Artemis.UI.Shared.Services.NodeEditor;
+
+///
+public class NodeEditorService : INodeEditorService
+{
+ private readonly IWindowService _windowService;
+
+ public NodeEditorService(IWindowService windowService)
+ {
+ _windowService = windowService;
+ }
+
+ private readonly Dictionary _nodeEditorHistories = new();
+ private readonly Dictionary _nodeEditorCommandScopes = new();
+
+ ///
+ public NodeEditorHistory GetHistory(INodeScript nodeScript)
+ {
+ if (_nodeEditorHistories.TryGetValue(nodeScript, out NodeEditorHistory? history))
+ return history;
+
+ NodeEditorHistory newHistory = new(nodeScript);
+ _nodeEditorHistories.Add(nodeScript, newHistory);
+ return newHistory;
+ }
+
+ ///
+ public void ExecuteCommand(INodeScript nodeScript, INodeEditorCommand command)
+ {
+ try
+ {
+ NodeEditorHistory history = GetHistory(nodeScript);
+
+ // If a scope is active add the command to it, the scope will execute it immediately
+ _nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope);
+ if (scope != null)
+ {
+ scope.AddCommand(command);
+ return;
+ }
+
+ history.Execute.Execute(command).Subscribe();
+ }
+ catch (Exception e)
+ {
+ _windowService.ShowExceptionDialog("Editor command failed", e);
+ throw;
+ }
+ }
+
+ ///
+ public NodeEditorCommandScope CreateCommandScope(INodeScript nodeScript, string name)
+ {
+ if (_nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope))
+ throw new ArtemisSharedUIException($"A command scope is already active, name: {scope.Name}.");
+
+ NodeEditorCommandScope newScope = new(this, nodeScript, name);
+ _nodeEditorCommandScopes.Add(nodeScript, newScope);
+ return newScope;
+ }
+
+ internal void StopCommandScope(INodeScript nodeScript)
+ {
+ // This might happen if the scope is disposed twice, it's no biggie
+ if (!_nodeEditorCommandScopes.TryGetValue(nodeScript, out NodeEditorCommandScope? scope))
+ return;
+
+ _nodeEditorCommandScopes.Remove(nodeScript);
+
+ // Executing the composite command won't do anything the first time (see last ctor variable)
+ // commands were already executed each time they were added to the scope
+ ExecuteCommand(nodeScript, new CompositeCommand(scope.NodeEditorCommands, scope.Name, true));
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs b/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs
new file mode 100644
index 000000000..dcff99245
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs
@@ -0,0 +1,49 @@
+using System;
+using System.ComponentModel;
+using System.Reactive.Disposables;
+using Artemis.Core;
+using ReactiveUI;
+
+namespace Artemis.UI.Shared.VisualScripting
+{
+ public abstract class CustomNodeViewModel : ActivatableViewModelBase, ICustomNodeViewModel
+ {
+ protected CustomNodeViewModel(INode node)
+ {
+ Node = node;
+
+ this.WhenActivated(d =>
+ {
+ Node.PropertyChanged += NodeOnPropertyChanged;
+ Disposable.Create(() => Node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d);
+ });
+ }
+
+ public INode Node { get; }
+
+ #region Events
+
+ ///
+ public event EventHandler NodeModified;
+
+ ///
+ /// Invokes the event
+ ///
+ protected virtual void OnNodeModified()
+ {
+ NodeModified?.Invoke(this, EventArgs.Empty);
+ }
+
+ #endregion
+
+ #region Event handlers
+
+ private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "Storage")
+ OnNodeModified();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json
index 8ce041f51..c8195bec5 100644
--- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json
+++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json
@@ -90,11 +90,6 @@
"Splat": "14.1.45"
}
},
- "ArtemisRGB.Plugins.BuildTask": {
- "type": "Transitive",
- "resolved": "1.1.0",
- "contentHash": "B8A5NkUEzTUgc5M/QACfyjIF5M23EUzSx8A8l/owJtB0bgmij6y/MQW1i/PcS3EDFQRothBOUuC3BCPY5UoRRQ=="
- },
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.0.2020091801",
@@ -447,6 +442,11 @@
"resolved": "5.0.0",
"contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q=="
},
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "5.0.9",
+ "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -565,10 +565,13 @@
"Ninject": "3.3.3"
}
},
- "OpenRGB.NET": {
+ "NoStringEvaluating": {
"type": "Transitive",
- "resolved": "1.7.0",
- "contentHash": "12pMEUaeoG8mN707QRO9hdT529+UnqUpwMW1H/gDTMsJrerhJve6Yt5Dnheu1isQB4PWP1wu3IDVbHCchznkiw=="
+ "resolved": "2.2.2",
+ "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==",
+ "dependencies": {
+ "Microsoft.Extensions.ObjectPool": "5.0.9"
+ }
},
"ReactiveUI.Validation": {
"type": "Transitive",
@@ -1772,21 +1775,6 @@
"System.ValueTuple": "4.5.0"
}
},
- "artemis.plugins.devices.openrgb": {
- "type": "Project",
- "dependencies": {
- "ArtemisRGB.Plugins.BuildTask": "1.1.0",
- "Avalonia": "0.10.11",
- "Avalonia.Controls.DataGrid": "0.10.11",
- "Avalonia.ReactiveUI": "0.10.11",
- "Material.Icons.Avalonia": "1.0.2",
- "OpenRGB.NET": "1.7.0",
- "RGB.NET.Core": "1.0.0-prerelease7",
- "ReactiveUI": "16.3.10",
- "Serilog": "2.10.0",
- "SkiaSharp": "2.88.0-preview.178"
- }
- },
"artemis.storage": {
"type": "Project",
"dependencies": {
@@ -1799,6 +1787,7 @@
"dependencies": {
"Artemis.Core": "1.0.0",
"Artemis.UI.Shared": "1.0.0",
+ "Artemis.VisualScripting": "1.0.0",
"Avalonia": "0.10.13",
"Avalonia.Controls.PanAndZoom": "10.12.0",
"Avalonia.Desktop": "0.10.13",
@@ -1836,6 +1825,19 @@
"ReactiveUI.Validation": "2.2.1",
"SkiaSharp": "2.88.0-preview.178"
}
+ },
+ "artemis.visualscripting": {
+ "type": "Project",
+ "dependencies": {
+ "Artemis.Core": "1.0.0",
+ "Artemis.UI.Shared": "1.0.0",
+ "Avalonia": "0.10.13",
+ "Avalonia.ReactiveUI": "0.10.13",
+ "Ninject": "3.3.4",
+ "NoStringEvaluating": "2.2.2",
+ "ReactiveUI": "17.1.50",
+ "SkiaSharp": "2.88.0-preview.178"
+ }
}
}
}
diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj
index d0eb58247..44930e0ee 100644
--- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj
+++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj
@@ -39,6 +39,7 @@
+
diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs
index a72152b46..cca29609a 100644
--- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs
+++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs
@@ -7,6 +7,7 @@ using Artemis.UI.Ninject;
using Artemis.UI.Screens.Root;
using Artemis.UI.Shared.Ninject;
using Artemis.UI.Shared.Services.Interfaces;
+using Artemis.VisualScripting.Ninject;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -36,6 +37,7 @@ namespace Artemis.UI
_kernel.Load();
_kernel.Load();
_kernel.Load();
+ _kernel.Load();
_kernel.Load(modules);
_kernel.UseNinjectDependencyResolver();
diff --git a/src/Avalonia/Artemis.UI/Converters/ColorLuminosityConverter.cs b/src/Avalonia/Artemis.UI/Converters/ColorLuminosityConverter.cs
new file mode 100644
index 000000000..46fa7140d
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Converters/ColorLuminosityConverter.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Skia;
+using SkiaSharp;
+
+namespace Artemis.UI.Converters;
+
+public class ColorLuminosityConverter : IValueConverter
+{
+ ///
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is Color color && double.TryParse(parameter?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double multiplier))
+ return multiplier > 0 ? Brighten(color, (float) multiplier) : Darken(color, (float) multiplier * -1);
+
+ return value;
+ }
+
+ ///
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value;
+ }
+
+ private static Color Brighten(Color color, float multiplier)
+ {
+ color.ToSKColor().ToHsl(out float h, out float s, out float l);
+ l += l * (multiplier / 100f);
+ SKColor skColor = SKColor.FromHsl(h, s, l);
+
+ return new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
+ }
+
+ private static Color Darken(Color color, float multiplier)
+ {
+ color.ToSKColor().ToHsl(out float h, out float s, out float l);
+ l -= l * (multiplier / 100f);
+ SKColor skColor = SKColor.FromHsl(h, s, l);
+
+ return new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs b/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs
index e69ed58f5..f3f12ad41 100644
--- a/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs
+++ b/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs
@@ -11,7 +11,7 @@ public class ColorOpacityConverter : IValueConverter
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color color && double.TryParse(parameter?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double multiplier))
- return new Color((byte) (color.A * multiplier), color.R, color.G, color.B);
+ return new Color((byte)(color.A * multiplier), color.R, color.G, color.B);
return value;
}
diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
index d575bfbc3..652feee62 100644
--- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
+++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs
@@ -90,14 +90,18 @@ namespace Artemis.UI.Ninject.Factories
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel);
}
- public interface INodeVmFactory
+ public interface INodeVmFactory : IVmFactory
{
NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript);
NodePickerViewModel NodePickerViewModel(NodeScript nodeScript);
- NodeViewModel NodeViewModel(INode node);
- InputPinCollectionViewModel InputPinCollectionViewModel(InputPinCollection inputPinCollection);
- InputPinViewModel InputPinViewModel(InputPin inputPin);
- OutputPinCollectionViewModel OutputPinCollectionViewModel(OutputPinCollection outputPinCollection);
- OutputPinViewModel OutputPinViewModel(OutputPin outputPin);
+ NodeViewModel NodeViewModel(NodeScript nodeScript, INode node);
+ }
+
+ public interface INodePinVmFactory
+ {
+ PinCollectionViewModel InputPinCollectionViewModel(PinCollection inputPinCollection);
+ PinViewModel InputPinViewModel(IPin inputPin);
+ PinCollectionViewModel OutputPinCollectionViewModel(PinCollection outputPinCollection);
+ PinViewModel OutputPinViewModel(IPin outputPin);
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/NodePinViewModelInstanceProvider.cs b/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/NodePinViewModelInstanceProvider.cs
new file mode 100644
index 000000000..c3a46614d
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/NodePinViewModelInstanceProvider.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Reflection;
+using Artemis.Core;
+using Artemis.UI.Screens.VisualScripting.Pins;
+using Ninject.Extensions.Factory;
+
+namespace Artemis.UI.Ninject.InstanceProviders;
+
+public class NodePinViewModelInstanceProvider : StandardInstanceProvider
+{
+ protected override Type GetType(MethodInfo methodInfo, object[] arguments)
+ {
+ if (methodInfo.ReturnType != typeof(PinCollectionViewModel) && methodInfo.ReturnType != typeof(PinViewModel))
+ return base.GetType(methodInfo, arguments);
+
+ if (arguments[0] is IPin pin)
+ return CreatePinViewModelType(pin);
+ if (arguments[0] is IPinCollection pinCollection)
+ return CreatePinCollectionViewModelType(pinCollection);
+
+ return base.GetType(methodInfo, arguments);
+ }
+
+ private Type CreatePinViewModelType(IPin pin)
+ {
+ if (pin.Direction == PinDirection.Input)
+ return typeof(InputPinViewModel<>).MakeGenericType(pin.Type);
+ return typeof(OutputPinViewModel<>).MakeGenericType(pin.Type);
+ }
+
+ private Type CreatePinCollectionViewModelType(IPinCollection pinCollection)
+ {
+ if (pinCollection.Direction == PinDirection.Input)
+ return typeof(InputPinCollectionViewModel<>).MakeGenericType(pinCollection.Type);
+ return typeof(OutputPinCollectionViewModel<>).MakeGenericType(pinCollection.Type);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
index e186b02a9..9abf0ef83 100644
--- a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
+++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
@@ -59,6 +59,7 @@ namespace Artemis.UI.Ninject
});
Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
+ Kernel.Bind().ToFactory(() => new NodePinViewModelInstanceProvider());
// Bind all UI services as singletons
Kernel.Bind(x =>
diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs
index 59acf2659..a3e1dc0b4 100644
--- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -181,6 +181,7 @@ namespace Artemis.UI.Screens.Root
_registrationService.RegisterBuiltInDataModelDisplays();
_registrationService.RegisterBuiltInDataModelInputs();
_registrationService.RegisterBuiltInPropertyEditors();
+ _registrationService.RegisterBuiltInNodeTypes();
if (_lifeTime.MainWindow == null)
{
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml
index 329c64a18..bb7c03328 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml
@@ -3,43 +3,54 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ mc:Ignorable="d" d:DesignWidth="650" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.NodePickerView"
- x:DataType="visualScripting:NodePickerViewModel">
+ x:DataType="visualScripting:NodePickerViewModel"
+ Width="600"
+ Height="400">
-
-
-
-
- Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None of the nodes match your search
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs
index 921da0c83..e6ddc86fc 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs
@@ -1,7 +1,13 @@
+using System;
+using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Mixins;
+using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
+using Avalonia.VisualTree;
+using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting
{
@@ -10,6 +16,13 @@ namespace Artemis.UI.Screens.VisualScripting
public NodePickerView()
{
InitializeComponent();
+ this.WhenActivated(
+ d => ViewModel
+ .WhenAnyValue(vm => vm.IsVisible)
+ .Where(visible => !visible)
+ .Subscribe(_ => this.FindLogicalAncestorOfType()?.ContextFlyout?.Hide())
+ .DisposeWith(d)
+ );
}
private void InitializeComponent()
@@ -17,4 +30,4 @@ namespace Artemis.UI.Screens.VisualScripting
AvaloniaXamlLoader.Load(this);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs
index 231463b3b..9e66457c8 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs
@@ -1,20 +1,51 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Collections.ObjectModel;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services.NodeEditor;
+using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
+using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
public class NodePickerViewModel : ActivatableViewModelBase
{
+ private readonly INodeEditorService _nodeEditorService;
+ private readonly NodeScript _nodeScript;
private readonly INodeService _nodeService;
private bool _isVisible;
private Point _position;
+ private string? _searchText;
+ private NodeData? _selectedNode;
+
+ public NodePickerViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService)
+ {
+ _nodeScript = nodeScript;
+ _nodeService = nodeService;
+ _nodeEditorService = nodeEditorService;
+
+ Nodes = new ObservableCollection(_nodeService.AvailableNodes);
+
+ this.WhenActivated(d =>
+ {
+ IsVisible = true;
+ Disposable.Create(() => IsVisible = false).DisposeWith(d);
+ });
+
+ this.WhenAnyValue(vm => vm.SelectedNode).WhereNotNull().Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler).Subscribe(data =>
+ {
+ CreateNode(data);
+ Hide();
+ SelectedNode = null;
+ });
+ }
+
+ public ObservableCollection Nodes { get; }
public bool IsVisible
{
@@ -28,9 +59,16 @@ public class NodePickerViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _position, value);
}
- public NodePickerViewModel(INodeService nodeService)
+ public string? SearchText
{
- _nodeService = nodeService;
+ get => _searchText;
+ set => RaiseAndSetIfChanged(ref _searchText, value);
+ }
+
+ public NodeData? SelectedNode
+ {
+ get => _selectedNode;
+ set => RaiseAndSetIfChanged(ref _selectedNode, value);
}
public void Show(Point position)
@@ -43,4 +81,13 @@ public class NodePickerViewModel : ActivatableViewModelBase
{
IsVisible = false;
}
+
+ private void CreateNode(NodeData data)
+ {
+ INode node = data.CreateNode(_nodeScript, null);
+ node.X = Position.X;
+ node.Y = Position.Y;
+
+ _nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node));
+ }
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml
index 2f0c9114b..fd189eb1d 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml
@@ -2,18 +2,70 @@
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:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptView"
x:DataType="visualScripting:NodeScriptViewModel">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs
index 8f5e65282..c3094011b 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs
@@ -1,16 +1,50 @@
+using System;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
using Avalonia.ReactiveUI;
+using Avalonia.VisualTree;
namespace Artemis.UI.Screens.VisualScripting
{
public partial class NodeScriptView : ReactiveUserControl
{
+ private readonly ZoomBorder _zoomBorder;
+ private readonly Grid _grid;
+
public NodeScriptView()
{
InitializeComponent();
+
+ _zoomBorder = this.Find("ZoomBorder");
+ _grid = this.Find("ContainerGrid");
+ _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
+ UpdateZoomBorderBackground();
+
+ _grid?.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
+ }
+
+ private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ // If the flyout handled the click, update the position of the node picker
+ if (e.Handled && ViewModel != null)
+ ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid);
+ }
+
+ private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property.Name == nameof(_zoomBorder.Background))
+ UpdateZoomBorderBackground();
+ }
+
+ private void UpdateZoomBorderBackground()
+ {
+ if (_zoomBorder.Background is VisualBrush visualBrush)
+ visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
}
private void InitializeComponent()
@@ -18,12 +52,9 @@ namespace Artemis.UI.Screens.VisualScripting
AvaloniaXamlLoader.Load(this);
}
- private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e)
{
- if (e.InitialPressMouseButton == MouseButton.Right)
- ViewModel?.ShowNodePicker(e.GetCurrentPoint(this).Position);
- if (e.InitialPressMouseButton == MouseButton.Left)
- ViewModel?.HideNodePicker();
+ UpdateZoomBorderBackground();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs
index e584407bc..88caf49a6 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs
@@ -7,6 +7,7 @@ using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services.NodeEditor;
using Avalonia;
using Avalonia.Controls.Mixins;
using ReactiveUI;
@@ -16,15 +17,18 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase
{
private readonly INodeService _nodeService;
+ private readonly INodeEditorService _nodeEditorService;
private readonly INodeVmFactory _nodeVmFactory;
- public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService)
+ public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService)
{
_nodeVmFactory = nodeVmFactory;
_nodeService = nodeService;
+ _nodeEditorService = nodeEditorService;
NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
+ History = _nodeEditorService.GetHistory(NodeScript);
this.WhenActivated(d =>
{
@@ -38,17 +42,18 @@ public class NodeScriptViewModel : ActivatableViewModelBase
NodeViewModels = new ObservableCollection();
foreach (INode nodeScriptNode in NodeScript.Nodes)
- NodeViewModels.Add(_nodeVmFactory.NodeViewModel(nodeScriptNode));
+ NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, nodeScriptNode));
}
public NodeScript NodeScript { get; }
public ObservableCollection NodeViewModels { get; }
+ public ObservableCollection CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; }
-
+ public NodeEditorHistory History { get; }
private void HandleNodeAdded(SingleValueEventArgs eventArgs)
{
- NodeViewModels.Add(_nodeVmFactory.NodeViewModel(eventArgs.Value));
+ NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value));
}
private void HandleNodeRemoved(SingleValueEventArgs eventArgs)
@@ -57,14 +62,4 @@ public class NodeScriptViewModel : ActivatableViewModelBase
if (toRemove != null)
NodeViewModels.Remove(toRemove);
}
-
- public void ShowNodePicker(Point position)
- {
- NodePickerViewModel.Show(position);
- }
-
- public void HideNodePicker()
- {
- NodePickerViewModel.Hide();
- }
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml
index 37cfb4264..a3ce0fbda 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml
@@ -2,7 +2,57 @@
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"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Screens.VisualScripting.NodeView">
- Welcome to Avalonia!
-
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
+ mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="150"
+ x:Class="Artemis.UI.Screens.VisualScripting.NodeView"
+ x:DataType="visualScripting:NodeViewModel">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs
index 7d8e70086..9244d4791 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs
@@ -1,20 +1,36 @@
+using System;
using Avalonia;
-using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Screens.VisualScripting
-{
- public partial class NodeView : ReactiveUserControl
- {
- public NodeView()
- {
- InitializeComponent();
- }
+namespace Artemis.UI.Screens.VisualScripting;
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
+public class NodeView : ReactiveUserControl
+{
+ public NodeView()
+ {
+ InitializeComponent();
}
-}
+
+ #region Overrides of Layoutable
+
+ ///
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ // Take the base implementation's size
+ (double width, double height) = base.MeasureOverride(availableSize);
+
+ // Ceil the resulting size
+ width = Math.Ceiling(width / 10.0) * 10.0;
+ height = Math.Ceiling(height / 10.0) * 10.0;
+
+ return new Size(width, height);
+ }
+
+ #endregion
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs
index e38eed9ed..07be3a158 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs
@@ -1,23 +1,79 @@
-using Artemis.Core;
+using System;
+using System.Collections.ObjectModel;
+using System.Reactive;
+using System.Reactive.Linq;
+using Artemis.Core;
+using Artemis.UI.Ninject.Factories;
+using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
-using Avalonia;
+using Artemis.UI.Shared.Services.NodeEditor;
+using Artemis.UI.Shared.Services.NodeEditor.Commands;
+using Avalonia.Controls.Mixins;
+using DynamicData;
+using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
public class NodeViewModel : ActivatableViewModelBase
{
- private Point _position;
+ private readonly NodeScript _nodeScript;
+ private readonly INodeEditorService _nodeEditorService;
- public NodeViewModel(INode node)
+ private ICustomNodeViewModel? _customNodeViewModel;
+ private ReactiveCommand? _deleteNode;
+ private ObservableAsPropertyHelper? _isStaticNode;
+
+ public NodeViewModel(NodeScript nodeScript, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{
+ _nodeScript = nodeScript;
+ _nodeEditorService = nodeEditorService;
Node = node;
+
+ SourceList nodePins = new();
+ this.WhenActivated(d =>
+ {
+ _isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode)
+ .Select(tuple => tuple.Item1 || tuple.Item2)
+ .ToProperty(this, model => model.IsStaticNode)
+ .DisposeWith(d);
+
+ Node.WhenAnyValue(n => n.Pins).Subscribe(pins => nodePins.Edit(source =>
+ {
+ source.Clear();
+ source.AddRange(pins);
+ })).DisposeWith(d);
+ });
+
+ DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v));
+
+ nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel).Bind(out ReadOnlyObservableCollection inputPins).Subscribe();
+ nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection outputPins).Subscribe();
+ InputPinViewModels = inputPins;
+ OutputPinViewModels = outputPins;
}
+ public bool IsStaticNode => _isStaticNode?.Value ?? true;
+
public INode Node { get; }
+ public ReadOnlyObservableCollection InputPinViewModels { get; }
+ public ReadOnlyObservableCollection InputPinCollectionViewModels { get; }
+ public ReadOnlyObservableCollection OutputPinViewModels { get; }
+ public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; }
- public Point Position
+ public ICustomNodeViewModel? CustomNodeViewModel
{
- get => _position;
- set => RaiseAndSetIfChanged(ref _position, value);
+ get => _customNodeViewModel;
+ set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
+ }
+
+ public ReactiveCommand? DeleteNode
+ {
+ get => _deleteNode;
+ set => RaiseAndSetIfChanged(ref _deleteNode, value);
+ }
+
+ private void ExecuteDeleteNode()
+ {
+ _nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node));
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml
index ae1b6724d..523457358 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml
@@ -2,7 +2,30 @@
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"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView">
- Welcome to Avalonia!
-
+ xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
+ mc:Ignorable="d"
+ d:DesignWidth="200"
+ x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView"
+ x:DataType="pins:PinViewModel">
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs
index ed148b458..098c76d3c 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs
@@ -1,4 +1,5 @@
using Artemis.Core;
+using Artemis.Core.Services;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@@ -6,7 +7,7 @@ public class InputPinViewModel : PinViewModel
{
public InputPin InputPin { get; }
- public InputPinViewModel(InputPin inputPin) : base(inputPin)
+ public InputPinViewModel(InputPin inputPin, INodeService nodeService) : base(inputPin, nodeService)
{
InputPin = inputPin;
}
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml
index 893ff48e2..cb8e3d832 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml
@@ -2,7 +2,30 @@
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"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView">
- Welcome to Avalonia!
-
+ xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
+ mc:Ignorable="d"
+ d:DesignWidth="200"
+ x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView"
+ x:DataType="pins:PinViewModel">
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs
index 0719fdcab..8f0e8e82b 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs
@@ -1,4 +1,5 @@
using Artemis.Core;
+using Artemis.Core.Services;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@@ -6,7 +7,7 @@ public class OutputPinViewModel : PinViewModel
{
public OutputPin OutputPin { get; }
- public OutputPinViewModel(OutputPin outputPin) : base(outputPin)
+ public OutputPinViewModel(OutputPin outputPin, INodeService nodeService) : base(outputPin, nodeService)
{
OutputPin = outputPin;
}
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs
index ec321632a..0b1ce6202 100644
--- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs
@@ -1,14 +1,22 @@
using Artemis.Core;
+using Artemis.Core.Services;
using Artemis.UI.Shared;
+using Avalonia.Media;
namespace Artemis.UI.Screens.VisualScripting.Pins;
public abstract class PinViewModel : ActivatableViewModelBase
{
- protected PinViewModel(IPin pin)
+ protected PinViewModel(IPin pin, INodeService nodeService)
{
Pin = pin;
+
+ TypeColorRegistration registration = nodeService.GetTypeColorRegistration(Pin.Type);
+ PinColor = new Color(registration.Color.Alpha, registration.Color.Red, registration.Color.Green, registration.Color.Blue);
+ DarkenedPinColor = new Color(registration.DarkenedColor.Alpha, registration.DarkenedColor.Red, registration.DarkenedColor.Green, registration.DarkenedColor.Blue);
}
public IPin Pin { get; }
+ public Color PinColor { get; }
+ public Color DarkenedPinColor { get; }
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml
new file mode 100644
index 000000000..afd62ecf2
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml
index 1580a3f95..6f972839c 100644
--- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml
@@ -12,49 +12,54 @@
x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
x:DataType="workshop:WorkshopViewModel">
-
- Workshop!! :3
-
-
- Notification tests
-
-
-
-
+
+
+
+ Nodes tests
+
+
+
+
+
+ Notification tests
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs
index cb7f09681..694ad3741 100644
--- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs
@@ -2,6 +2,8 @@
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
+using Artemis.UI.Ninject.Factories;
+using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia.Input;
@@ -25,15 +27,19 @@ namespace Artemis.UI.Screens.Workshop
new ColorGradientStop(new SKColor(0xFF00FCCC), 1f),
};
- public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop")
+ public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService, INodeVmFactory nodeVmFactory) : base(hostScreen, "workshop")
{
_notificationService = notificationService;
_cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor);
DisplayName = "Workshop";
ShowNotification = ReactiveCommand.Create(ExecuteShowNotification);
+
+ VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(new NodeScript("Test script", "A test script"));
}
+ public NodeScriptViewModel VisualEditorViewModel { get; }
+
public ReactiveCommand ShowNotification { get; set; }
public StandardCursorType SelectedCursor
diff --git a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs
index 4c45d8b28..838947dbc 100644
--- a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs
+++ b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs
@@ -7,5 +7,6 @@
void RegisterBuiltInPropertyEditors();
void RegisterControllers();
void ApplyPreferredGraphicsContext();
+ void RegisterBuiltInNodeTypes();
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs
index f6f8e6c53..24574e35b 100644
--- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs
+++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs
@@ -1,4 +1,7 @@
-using System.Collections.Generic;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DefaultTypes.PropertyInput;
@@ -6,9 +9,11 @@ using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.PropertyInput;
+using Artemis.VisualScripting.Nodes;
using Avalonia;
using DynamicData;
using Ninject;
+using SkiaSharp;
namespace Artemis.UI.Services;
@@ -17,13 +22,15 @@ public class RegistrationService : IRegistrationService
private readonly IKernel _kernel;
private readonly IInputService _inputService;
private readonly IPropertyInputService _propertyInputService;
+ private readonly INodeService _nodeService;
private bool _registeredBuiltInPropertyEditors;
- public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable toolViewModels)
+ public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, INodeService nodeService, IEnumerable toolViewModels)
{
_kernel = kernel;
_inputService = inputService;
_propertyInputService = propertyInputService;
+ _nodeService = nodeService;
profileEditorService.Tools.AddRange(toolViewModels);
CreateCursorResources();
@@ -75,4 +82,18 @@ public class RegistrationService : IRegistrationService
public void ApplyPreferredGraphicsContext()
{
}
+
+ public void RegisterBuiltInNodeTypes()
+ {
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(bool), new SKColor(0xFFCD3232));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(string), new SKColor(0xFFFFD700));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(int), new SKColor(0xFF32CD32));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(float), new SKColor(0xFFFF7C00));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61));
+ _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF));
+
+ foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface))
+ _nodeService.RegisterNodeType(Constants.CorePlugin, nodeType);
+ }
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json
index 9a5fbda19..1e975be58 100644
--- a/src/Avalonia/Artemis.UI/packages.lock.json
+++ b/src/Avalonia/Artemis.UI/packages.lock.json
@@ -481,6 +481,11 @@
"resolved": "5.0.0",
"contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q=="
},
+ "Microsoft.Extensions.ObjectPool": {
+ "type": "Transitive",
+ "resolved": "5.0.9",
+ "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -599,6 +604,14 @@
"Ninject": "3.3.3"
}
},
+ "NoStringEvaluating": {
+ "type": "Transitive",
+ "resolved": "2.2.2",
+ "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==",
+ "dependencies": {
+ "Microsoft.Extensions.ObjectPool": "5.0.9"
+ }
+ },
"RGB.NET.Presets": {
"type": "Transitive",
"resolved": "1.0.0-prerelease7",
@@ -1783,6 +1796,19 @@
"ReactiveUI.Validation": "2.2.1",
"SkiaSharp": "2.88.0-preview.178"
}
+ },
+ "artemis.visualscripting": {
+ "type": "Project",
+ "dependencies": {
+ "Artemis.Core": "1.0.0",
+ "Artemis.UI.Shared": "1.0.0",
+ "Avalonia": "0.10.13",
+ "Avalonia.ReactiveUI": "0.10.13",
+ "Ninject": "3.3.4",
+ "NoStringEvaluating": "2.2.2",
+ "ReactiveUI": "17.1.50",
+ "SkiaSharp": "2.88.0-preview.178"
+ }
}
}
}
diff --git a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj
new file mode 100644
index 000000000..be4170e96
--- /dev/null
+++ b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj
@@ -0,0 +1,74 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs b/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs
new file mode 100644
index 000000000..3bf75bd56
--- /dev/null
+++ b/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.ObjectPool;
+using Ninject.Modules;
+using NoStringEvaluating;
+using NoStringEvaluating.Contract;
+using NoStringEvaluating.Models.Values;
+using NoStringEvaluating.Services.Cache;
+using NoStringEvaluating.Services.Checking;
+using NoStringEvaluating.Services.Parsing;
+using NoStringEvaluating.Services.Parsing.NodeReaders;
+
+namespace Artemis.VisualScripting.Ninject
+{
+ public class NoStringNinjectModule : NinjectModule
+ {
+ public override void Load()
+ {
+ // Pooling
+ Bind>>()
+ .ToConstant(ObjectPool.Create>())
+ .InSingletonScope();
+
+ Bind>>()
+ .ToConstant(ObjectPool.Create>())
+ .InSingletonScope();
+
+ Bind>()
+ .ToConstant(ObjectPool.Create())
+ .InSingletonScope();
+
+ // Parser
+ Bind().To().InSingletonScope();
+ Bind().To().InSingletonScope();
+ Bind().To().InSingletonScope();
+
+ // Checker
+ Bind().To().InSingletonScope();
+
+ // Evaluator
+ Bind().To().InSingletonScope();
+
+ // If needed
+ InjectUserDefinedFunctions();
+ }
+
+ private void InjectUserDefinedFunctions()
+ {
+ IFunctionReader functionReader = (IFunctionReader) Kernel!.GetService(typeof(IFunctionReader));
+ NoStringFunctionsInitializer.InitializeFunctions(functionReader, typeof(NoStringNinjectModule));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs
new file mode 100644
index 000000000..a900a4fb1
--- /dev/null
+++ b/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs
@@ -0,0 +1,299 @@
+using System.Collections;
+using Artemis.Core;
+using Artemis.VisualScripting.Nodes.CustomViewModels;
+
+namespace Artemis.VisualScripting.Nodes;
+
+[Node("Greater than", "Checks if the first input is greater than the second.", "Operators", InputType = typeof(object), OutputType = typeof(bool))]
+public class GreaterThanNode : Node
+{
+ #region Constructors
+
+ public GreaterThanNode()
+ : base("Greater than", "Checks if the first input is greater than the second.")
+ {
+ Input1 = CreateInputPin