1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Node editor - Implemented node visuals, pin visuals

Node editor - Implemented undo/redo and some commands
This commit is contained in:
Robert 2022-03-13 22:07:16 +01:00
parent c99224ab2d
commit 034879a2c9
91 changed files with 5241 additions and 193 deletions

View File

@ -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
/// <inheritdoc />
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)
@ -105,7 +115,7 @@ namespace Artemis.Core.Services
/// <summary>
/// Gets the best matching registration for the provided type
/// </summary>
TypeColorRegistration? GetTypeColor(Type type);
TypeColorRegistration GetTypeColorRegistration(Type type);
/// <summary>
/// Registers a node of the provided <paramref name="nodeType" />

View File

@ -12,16 +12,6 @@ namespace Artemis.Core
/// </summary>
public INode Node { get; }
/// <summary>
/// Called whenever the custom view model is activated
/// </summary>
void OnActivate();
/// <summary>
/// Called whenever the custom view model is closed
/// </summary>
void OnDeactivate();
/// <summary>
/// Occurs whenever the node was modified by the view model
/// </summary>

View File

@ -125,9 +125,6 @@ namespace Artemis.Core
_nodes.Remove(node);
}
if (node is IDisposable disposable)
disposable.Dispose();
NodeRemoved?.Invoke(this, new SingleValueEventArgs<INode>(node));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,53 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to add a node.
/// </summary>
public class AddNode : INodeEditorCommand, IDisposable
{
private readonly INodeScript _nodeScript;
private readonly INode _node;
private bool _isRemoved;
/// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class.
/// </summary>
/// <param name="nodeScript">The node script the node belongs to.</param>
/// <param name="node">The node to delete.</param>
public AddNode(INodeScript nodeScript, INode node)
{
_nodeScript = nodeScript;
_node = node;
}
/// <inheritdoc />
public string DisplayName => $"Add '{_node.Name}' node";
/// <inheritdoc />
public void Execute()
{
_nodeScript.AddNode(_node);
_isRemoved = false;
}
/// <inheritdoc />
public void Undo()
{
_nodeScript.RemoveNode(_node);
_isRemoved = true;
}
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_isRemoved)
_nodeScript.Dispose();
}
#endregion
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to combine multiple commands into one.
/// </summary>
public class CompositeCommand : INodeEditorCommand, IDisposable
{
private bool _ignoreNextExecute;
private readonly List<INodeEditorCommand> _commands;
/// <summary>
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
/// </summary>
/// <param name="commands">The commands to execute.</param>
/// <param name="displayName">The display name of the composite command.</param>
public CompositeCommand(IEnumerable<INodeEditorCommand> commands, string displayName)
{
if (commands == null)
throw new ArgumentNullException(nameof(commands));
_commands = commands.ToList();
DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName));
}
/// <summary>
/// Creates a new instance of the <see cref="CompositeCommand" /> class.
/// </summary>
/// <param name="commands">The commands to execute.</param>
/// <param name="displayName">The display name of the composite command.</param>
/// <param name="ignoreFirstExecute">Whether or not to ignore the first execute because commands are already executed</param>
internal CompositeCommand(IEnumerable<INodeEditorCommand> 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));
}
/// <inheritdoc />
public void Dispose()
{
foreach (INodeEditorCommand NodeEditorCommand in _commands)
if (NodeEditorCommand is IDisposable disposable)
disposable.Dispose();
}
#region Implementation of INodeEditorCommand
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public void Execute()
{
if (_ignoreNextExecute)
{
_ignoreNextExecute = false;
return;
}
foreach (INodeEditorCommand NodeEditorCommand in _commands)
NodeEditorCommand.Execute();
}
/// <inheritdoc />
public void Undo()
{
// Undo in reverse by iterating from the back
for (int index = _commands.Count - 1; index >= 0; index--)
_commands[index].Undo();
}
#endregion
}

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to delete a node.
/// </summary>
public class DeleteNode : INodeEditorCommand, IDisposable
{
private readonly INode _node;
private readonly INodeScript _nodeScript;
private readonly Dictionary<IPin, IReadOnlyList<IPin>> _pinConnections = new();
private bool _isRemoved;
/// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class.
/// </summary>
/// <param name="nodeScript">The node script the node belongs to.</param>
/// <param name="node">The node to delete.</param>
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<IPin>? 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<IPin>? connections))
foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
}
}
_pinConnections.Clear();
}
/// <inheritdoc />
public void Dispose()
{
if (_isRemoved)
_nodeScript.Dispose();
}
/// <inheritdoc />
public string DisplayName => $"Delete '{_node.Name}' node";
/// <inheritdoc />
public void Execute()
{
StoreConnections();
_nodeScript.RemoveNode(_node);
_isRemoved = true;
}
/// <inheritdoc />
public void Undo()
{
_nodeScript.AddNode(_node);
RestoreConnections();
_isRemoved = false;
}
}

View File

@ -0,0 +1,48 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to move a node.
/// </summary>
public class MoveNode : INodeEditorCommand
{
private readonly INode _node;
private readonly double _originalX;
private readonly double _originalY;
private readonly double _x;
private readonly double _y;
/// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class.
/// </summary>
/// <param name="node">The node to update.</param>
/// <param name="x">The new X-position.</param>
/// <param name="y">The new Y-position.</param>
public MoveNode(INode node, double x, double y)
{
_node = node;
_x = x;
_y = y;
_originalX = node.X;
_originalY = node.Y;
}
/// <inheritdoc />
public string DisplayName => "Move node";
/// <inheritdoc />
public void Execute()
{
_node.X = _x;
_node.Y = _y;
}
/// <inheritdoc />
public void Undo()
{
_node.X = _originalX;
_node.Y = _originalY;
}
}

View File

@ -0,0 +1,23 @@
namespace Artemis.UI.Shared.Services.NodeEditor
{
/// <summary>
/// Represents a command that can be executed and if needed, undone
/// </summary>
public interface INodeEditorCommand
{
/// <summary>
/// Gets the name of the command
/// </summary>
string DisplayName { get; }
/// <summary>
/// Executes the command
/// </summary>
void Execute();
/// <summary>
/// Undoes the command
/// </summary>
void Undo();
}
}

View File

@ -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
{
/// <summary>
/// Gets the editor history for the provided node script.
/// </summary>
/// <param name="nodeScript">The node script to get the editor history for.</param>
/// <returns>The node editor history of the given node script.</returns>
NodeEditorHistory GetHistory(INodeScript nodeScript);
/// <summary>
/// Executes the provided command and adds it to the history.
/// </summary>
/// <param name="nodeScript">The node script to execute the command upon.</param>
/// <param name="command">The command to execute.</param>
void ExecuteCommand(INodeScript nodeScript, INodeEditorCommand command);
/// <summary>
/// Creates a new command scope which can be used to group undo/redo actions of multiple commands.
/// </summary>
/// <param name="nodeScript">The node script to create the scope for.</param>
/// <param name="name">The name of the command scope.</param>
/// <returns>The command scope that will group any commands until disposed.</returns>
NodeEditorCommandScope CreateCommandScope(INodeScript nodeScript, string name);
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor;
/// <summary>
/// Represents a scope in which editor commands are executed until disposed.
/// </summary>
public class NodeEditorCommandScope : IDisposable
{
private readonly List<INodeEditorCommand> _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<INodeEditorCommand>();
}
/// <summary>
/// Gets the name of the scope.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a read only collection of commands in the scope.
/// </summary>
public ReadOnlyCollection<INodeEditorCommand> NodeEditorCommands => new(_commands);
internal void AddCommand(INodeEditorCommand command)
{
command.Execute();
_commands.Add(command);
}
/// <inheritdoc />
public void Dispose()
{
_nodeEditorService.StopCommandScope(_nodeScript);
}
}

View File

@ -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<bool> _canRedo = new();
private readonly Subject<bool> _canUndo = new();
private readonly Stack<INodeEditorCommand> _redoCommands = new();
private readonly Stack<INodeEditorCommand> _undoCommands = new();
public NodeEditorHistory(INodeScript nodeScript)
{
NodeScript = nodeScript;
Execute = ReactiveCommand.Create<INodeEditorCommand>(ExecuteEditorCommand);
Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
}
public INodeScript NodeScript { get; }
public IObservable<bool> CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
public IObservable<bool> CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
public ReactiveCommand<INodeEditorCommand, Unit> Execute { get; }
public ReactiveCommand<Unit, INodeEditorCommand?> Undo { get; }
public ReactiveCommand<Unit, INodeEditorCommand?> 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());
}
}

View File

@ -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;
/// <inheritdoc cref="INodeEditorService"/>
public class NodeEditorService : INodeEditorService
{
private readonly IWindowService _windowService;
public NodeEditorService(IWindowService windowService)
{
_windowService = windowService;
}
private readonly Dictionary<INodeScript, NodeEditorHistory> _nodeEditorHistories = new();
private readonly Dictionary<INodeScript, NodeEditorCommandScope> _nodeEditorCommandScopes = new();
/// <inheritdoc />
public NodeEditorHistory GetHistory(INodeScript nodeScript)
{
if (_nodeEditorHistories.TryGetValue(nodeScript, out NodeEditorHistory? history))
return history;
NodeEditorHistory newHistory = new(nodeScript);
_nodeEditorHistories.Add(nodeScript, newHistory);
return newHistory;
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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));
}
}

View File

@ -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
/// <inheritdoc />
public event EventHandler NodeModified;
/// <summary>
/// Invokes the <see cref="NodeModified"/> event
/// </summary>
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
}
}

View File

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

View File

@ -39,6 +39,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Images\Logo\bow-black.ico" />

View File

@ -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<CoreModule>();
_kernel.Load<UIModule>();
_kernel.Load<SharedUIModule>();
_kernel.Load<NoStringNinjectModule>();
_kernel.Load(modules);
_kernel.UseNinjectDependencyResolver();

View File

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

View File

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

View File

@ -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<T> InputPinCollectionViewModel<T>(InputPinCollection<T> inputPinCollection);
InputPinViewModel<T> InputPinViewModel<T>(InputPin<T> inputPin);
OutputPinCollectionViewModel<T> OutputPinCollectionViewModel<T>(OutputPinCollection<T> outputPinCollection);
OutputPinViewModel<T> OutputPinViewModel<T>(OutputPin<T> 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);
}
}

View File

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

View File

@ -59,6 +59,7 @@ namespace Artemis.UI.Ninject
});
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
Kernel.Bind<INodePinVmFactory>().ToFactory(() => new NodePinViewModelInstanceProvider());
// Bind all UI services as singletons
Kernel.Bind(x =>

View File

@ -181,6 +181,7 @@ namespace Artemis.UI.Screens.Root
_registrationService.RegisterBuiltInDataModelDisplays();
_registrationService.RegisterBuiltInDataModelInputs();
_registrationService.RegisterBuiltInPropertyEditors();
_registrationService.RegisterBuiltInNodeTypes();
if (_lifeTime.MainWindow == null)
{

View File

@ -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">
<UserControl.Styles>
<Style Selector="Border.picker-container">
</Style>
<Style Selector="Border.picker-container-hidden">
<Style.Animations>
<Animation Duration="0:0:1">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.0" />
<Setter Property="IsVisible" Value="False" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.picker-container-visible">
<Style.Animations>
<Animation Duration="0:0:1">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.0" />
<Setter Property="IsVisible" Value="True" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
<Style Selector="TextBox#SearchBox">
<Setter Property="VerticalAlignment" Value="Top"></Setter>
<Setter Property="InnerRightContent">
<Template>
<StackPanel Orientation="Horizontal">
<controls:Button Content="&#xE8BB;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Classes="AppBarButton"
Command="{Binding $parent[TextBox].Clear}"
IsVisible="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<controls:Button Content="&#xE721;"
FontFamily="{StaticResource SymbolThemeFontFamily}"
Classes="AppBarButton"
IsHitTestVisible="False" />
</StackPanel>
</Template>
</Setter>
</Style>
</UserControl.Styles>
<Border Classes="grid picker-container"
Classes.picker-container-hidden="{CompiledBinding !IsVisible}"
Classes.picker-container-visible="{CompiledBinding !IsVisible}">
<TextBlock>Test</TextBlock>
<Border Classes="picker-container">
<Grid RowDefinitions="Auto,*">
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15"></TextBox>
<ListBox Grid.Row="1"
Items="{CompiledBinding Nodes}"
SelectedItem="{CompiledBinding SelectedNode}"
IsVisible="{CompiledBinding Nodes.Count}">
<ListBox.ItemTemplate>
<DataTemplate DataType="core:NodeData">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" FontWeight="Bold"></TextBlock>
<TextBlock Text="{CompiledBinding Description}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" VerticalAlignment="Center" Spacing="20" IsVisible="{CompiledBinding !Nodes.Count}">
<avalonia:MaterialIcon Kind="CloseCircle" Width="64" Height="64"></avalonia:MaterialIcon>
<TextBlock Classes="h4" TextAlignment="Center">None of the nodes match your search</TextBlock>
</StackPanel>
</Grid>
</Border>
</UserControl>

View File

@ -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<Grid>()?.ContextFlyout?.Hide())
.DisposeWith(d)
);
}
private void InitializeComponent()

View File

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

View File

@ -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">
<Canvas PointerReleased="InputElement_OnPointerReleased">
<UserControl.Styles>
<Style Selector="FlyoutPresenter.node-picker-flyout">
<Setter Property="MaxWidth" Value="1000"></Setter>
</Style>
</UserControl.Styles>
<UserControl.KeyBindings>
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z"></KeyBinding>
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y"></KeyBinding>
</UserControl.KeyBindings>
<paz:ZoomBorder Name="ZoomBorder"
Stretch="None"
ClipToBounds="True"
Focusable="True"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Background="{DynamicResource LargeCheckerboardBrush}"
ZoomChanged="ZoomBorder_OnZoomChanged">
<Grid Name="ContainerGrid" Background="Transparent">
<Grid.ContextFlyout>
<Flyout FlyoutPresenterClasses="node-picker-flyout">
<ContentControl Content="{CompiledBinding NodePickerViewModel}" />
</Flyout>
</Grid.ContextFlyout>
<Grid.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" Easing="CubicEaseOut" />
</Transitions>
</Grid.Transitions>
<!-- Cables -->
<ItemsControl Items="{CompiledBinding CableViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Node.X, TargetNullValue=0}" />
<Setter Property="Canvas.Top" Value="{Binding Node.Y, TargetNullValue=0}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
<!-- Nodes -->
<ItemsControl Items="{CompiledBinding NodeViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Node.X, TargetNullValue=0}" />
<Setter Property="Canvas.Top" Value="{Binding Node.Y, TargetNullValue=0}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
</Grid>
</paz:ZoomBorder>
<!-- Flyout -->
<ContentControl Content="{CompiledBinding NodePickerViewModel}"
Canvas.Left="{CompiledBinding NodePickerViewModel.Position.X}"
Canvas.Top="{CompiledBinding NodePickerViewModel.Position.Y}"/>
</Canvas>
</UserControl>

View File

@ -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<NodeScriptViewModel>
{
private readonly ZoomBorder _zoomBorder;
private readonly Grid _grid;
public NodeScriptView()
{
InitializeComponent();
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_grid = this.Find<Grid>("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();
}
}
}

View File

@ -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<NodeViewModel>();
foreach (INode nodeScriptNode in NodeScript.Nodes)
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(nodeScriptNode));
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, nodeScriptNode));
}
public NodeScript NodeScript { get; }
public ObservableCollection<NodeViewModel> NodeViewModels { get; }
public ObservableCollection<CableViewModel> CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; }
private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)
{
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(eventArgs.Value));
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value));
}
private void HandleNodeRemoved(SingleValueEventArgs<INode> 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();
}
}

View File

@ -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">
<UserControl.Styles>
<Style Selector="Border.node-container">
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="ContentControl#CustomViewModelContainer">
<Setter Property="Margin" Value="20 0"></Setter>
</Style>
</UserControl.Styles>
<Border Classes="node-container">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Background="{DynamicResource TaskDialogHeaderBackground}" CornerRadius="6 6 0 0">
<Grid Classes="node-header"
VerticalAlignment="Top"
ColumnDefinitions="*,Auto">
<TextBlock VerticalAlignment="Center"
TextAlignment="Center"
Margin="5"
Text="{CompiledBinding Node.Name}"
ToolTip.Tip="{CompiledBinding Node.Description}">
</TextBlock>
<Button VerticalAlignment="Center"
Classes="icon-button icon-button-small"
Grid.Column="1"
Margin="5"
Command="{CompiledBinding DeleteNode}">
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
</Button>
</Grid>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="5">
<StackPanel Grid.Column="0">
<ItemsControl Items="{CompiledBinding InputPinViewModels}" />
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
</StackPanel>
<ContentControl Name="CustomViewModelContainer" Grid.Column="1" Content="{CompiledBinding CustomNodeViewModel}" IsVisible="{CompiledBinding CustomNodeViewModel}" />
<StackPanel Grid.Column="2">
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" />
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
</StackPanel>
</Grid>
</Grid>
</Border>
</UserControl>

View File

@ -1,20 +1,36 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting
namespace Artemis.UI.Screens.VisualScripting;
public class NodeView : ReactiveUserControl<NodeViewModel>
{
public partial class NodeView : ReactiveUserControl<NodeViewModel>
{
public NodeView()
{
InitializeComponent();
}
#region Overrides of Layoutable
/// <inheritdoc />
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);
}
}
}

View File

@ -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<Unit, Unit>? _deleteNode;
private ObservableAsPropertyHelper<bool>? _isStaticNode;
public NodeViewModel(NodeScript nodeScript, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{
_nodeScript = nodeScript;
_nodeEditorService = nodeEditorService;
Node = node;
SourceList<IPin> 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<PinViewModel> inputPins).Subscribe();
nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins).Subscribe();
InputPinViewModels = inputPins;
OutputPinViewModels = outputPins;
}
public bool IsStaticNode => _isStaticNode?.Value ?? true;
public INode Node { get; }
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> OutputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
public Point Position
public ICustomNodeViewModel? CustomNodeViewModel
{
get => _position;
set => RaiseAndSetIfChanged(ref _position, value);
get => _customNodeViewModel;
set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
}
public ReactiveCommand<Unit, Unit>? DeleteNode
{
get => _deleteNode;
set => RaiseAndSetIfChanged(ref _deleteNode, value);
}
private void ExecuteDeleteNode()
{
_nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node));
}
}

View File

@ -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">
<UserControl.Styles>
<StyleInclude Source="/Screens/VisualScripting/VisualScripting.axaml" />
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{CompiledBinding DarkenedPinColor}"></SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{CompiledBinding PinColor}"></SolidColorBrush>
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<StackPanel Name="PinContainer" Orientation="Horizontal" Spacing="6">
<Border Name="PinPoint">
<Border Name="VisualPinPoint" />
</Border>
<TextBlock Name="PinName" VerticalAlignment="Center" Text="{CompiledBinding Pin.Name}" />
</StackPanel>
</UserControl>

View File

@ -1,4 +1,5 @@
using Artemis.Core;
using Artemis.Core.Services;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -6,7 +7,7 @@ public class InputPinViewModel<T> : PinViewModel
{
public InputPin<T> InputPin { get; }
public InputPinViewModel(InputPin<T> inputPin) : base(inputPin)
public InputPinViewModel(InputPin<T> inputPin, INodeService nodeService) : base(inputPin, nodeService)
{
InputPin = inputPin;
}

View File

@ -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">
<UserControl.Styles>
<StyleInclude Source="/Screens/VisualScripting/VisualScripting.axaml" />
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="{CompiledBinding DarkenedPinColor}" />
</Setter.Value>
</Setter>
<Setter Property="BorderBrush">
<Setter.Value>
<SolidColorBrush Color="{CompiledBinding PinColor}" />
</Setter.Value>
</Setter>
</Style>
</UserControl.Styles>
<StackPanel Name="PinContainer" Orientation="Horizontal" Spacing="6">
<TextBlock Name="PinName" VerticalAlignment="Center" Text="{CompiledBinding Pin.Name}" />
<Border Name="PinPoint">
<Border Name="VisualPinPoint" />
</Border>
</StackPanel>
</UserControl>

View File

@ -1,4 +1,5 @@
using Artemis.Core;
using Artemis.Core.Services;
namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -6,7 +7,7 @@ public class OutputPinViewModel<T> : PinViewModel
{
public OutputPin<T> OutputPin { get; }
public OutputPinViewModel(OutputPin<T> outputPin) : base(outputPin)
public OutputPinViewModel(OutputPin<T> outputPin, INodeService nodeService) : base(outputPin, nodeService)
{
OutputPin = outputPin;
}

View File

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

View File

@ -0,0 +1,31 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="StackPanel#PinContainer">
<Setter Property="Height" Value="24" />
</Style>
<Style Selector="StackPanel#PinContainer Border#PinPoint">
<Setter Property="Width" Value="13" />
<Setter Property="Height" Value="13" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
<Setter Property="Width" Value="11" />
<Setter Property="Height" Value="11" />
<Setter Property="Margin" Value="2" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Background" Value="DarkRed" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="StackPanel#PinContainer TextBlock#PinName">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontSize" Value="11" />
</Style>
</Styles>

View File

@ -12,9 +12,14 @@
x:Class="Artemis.UI.Screens.Workshop.WorkshopView"
x:DataType="workshop:WorkshopViewModel">
<Border Classes="router-container">
<StackPanel Margin="12">
<TextBlock>Workshop!! :3</TextBlock>
<Border Classes="card" Margin="0 12">
<StackPanel Margin="12" Spacing="5">
<Border Classes="card">
<StackPanel Spacing="5">
<TextBlock Classes="h4">Nodes tests</TextBlock>
<ContentControl Content="{CompiledBinding VisualEditorViewModel}" HorizontalAlignment="Stretch" Height="800"></ContentControl>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="5">
<TextBlock Classes="h4">Notification tests</TextBlock>
<Button Command="{CompiledBinding ShowNotification}" CommandParameter="{x:Static builders:NotificationSeverity.Informational}">

View File

@ -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<NotificationSeverity>(ExecuteShowNotification);
VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(new NodeScript<bool>("Test script", "A test script"));
}
public NodeScriptViewModel VisualEditorViewModel { get; }
public ReactiveCommand<NotificationSeverity, Unit> ShowNotification { get; set; }
public StandardCursorType SelectedCursor

View File

@ -7,5 +7,6 @@
void RegisterBuiltInPropertyEditors();
void RegisterControllers();
void ApplyPreferredGraphicsContext();
void RegisterBuiltInNodeTypes();
}
}

View File

@ -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<IToolViewModel> toolViewModels)
public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, INodeService nodeService, IEnumerable<IToolViewModel> 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);
}
}

View File

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

View File

@ -0,0 +1,74 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Nodes\Color\CustomViews\StaticSKColorValueNodeCustomView.xaml" />
<None Remove="Nodes\CustomViews\EnumEqualsNodeCustomView.xaml" />
<None Remove="Nodes\CustomViews\LayerPropertyNodeCustomView.xaml" />
<None Remove="Nodes\CustomViews\StaticNumericValueNodeCustomView.xaml" />
<None Remove="Nodes\CustomViews\StaticStringValueNodeCustomView.xaml" />
<None Remove="Nodes\DataModel\CustomViews\DataModelEventNodeCustomView.xaml" />
<None Remove="Nodes\DataModel\CustomViews\DataModelNodeCustomView.xaml" />
<None Remove="Nodes\Easing\CustomViews\EasingTypeNodeCustomView.xaml" />
<None Remove="Nodes\Maths\CustomViews\MathExpressionNodeCustomView.xaml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="NoStringEvaluating" Version="2.2.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="SkiaSharp" Version="2.88.0-preview.178" />
</ItemGroup>
<ItemGroup>
<Page Include="Nodes\Color\CustomViews\StaticSKColorValueNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\CustomViews\EnumEqualsNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\CustomViews\LayerPropertyNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\CustomViews\StaticNumericValueNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\CustomViews\StaticStringValueNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\DataModel\CustomViews\DataModelEventNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\DataModel\CustomViews\DataModelNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\Easing\CustomViews\EasingTypeNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Nodes\Maths\CustomViews\MathExpressionNodeCustomView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -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<ObjectPool<Stack<InternalEvaluatorValue>>>()
.ToConstant(ObjectPool.Create<Stack<InternalEvaluatorValue>>())
.InSingletonScope();
Bind<ObjectPool<List<InternalEvaluatorValue>>>()
.ToConstant(ObjectPool.Create<List<InternalEvaluatorValue>>())
.InSingletonScope();
Bind<ObjectPool<ExtraTypeIdContainer>>()
.ToConstant(ObjectPool.Create<ExtraTypeIdContainer>())
.InSingletonScope();
// Parser
Bind<IFormulaCache>().To<FormulaCache>().InSingletonScope();
Bind<IFunctionReader>().To<FunctionReader>().InSingletonScope();
Bind<IFormulaParser>().To<FormulaParser>().InSingletonScope();
// Checker
Bind<IFormulaChecker>().To<FormulaChecker>().InSingletonScope();
// Evaluator
Bind<INoStringEvaluator>().To<NoStringEvaluator>().InSingletonScope();
// If needed
InjectUserDefinedFunctions();
}
private void InjectUserDefinedFunctions()
{
IFunctionReader functionReader = (IFunctionReader) Kernel!.GetService(typeof(IFunctionReader));
NoStringFunctionsInitializer.InitializeFunctions(functionReader, typeof(NoStringNinjectModule));
}
}
}

View File

@ -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<object>();
Input2 = CreateInputPin<object>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
if (Input1.Value is Numeric numeric1 && Input2.Value is Numeric numeric2)
{
Result.Value = numeric1 > numeric2;
return;
}
if (Input2.Value != null && Input1.Value != null && Input1.Value.IsNumber() && Input2.Value.IsNumber())
{
Result.Value = Convert.ToSingle(Input1.Value) > Convert.ToSingle(Input2.Value);
return;
}
try
{
Result.Value = Comparer.DefaultInvariant.Compare(Input1.Value, Input2.Value) == 1;
}
catch
{
Result.Value = false;
}
}
#endregion
#region Properties & Fields
public InputPin<object> Input1 { get; }
public InputPin<object> Input2 { get; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Less than", "Checks if the first input is less than the second.", "Operators", InputType = typeof(object), OutputType = typeof(bool))]
public class LessThanNode : Node
{
#region Constructors
public LessThanNode()
: base("Less than", "Checks if the first input is less than the second.")
{
Input1 = CreateInputPin<object>();
Input2 = CreateInputPin<object>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
if (Input1.Value is Numeric numeric1 && Input2.Value is Numeric numeric2)
{
Result.Value = numeric1 < numeric2;
return;
}
if (Input2.Value != null && Input1.Value != null && Input1.Value.IsNumber() && Input2.Value.IsNumber())
{
Result.Value = Convert.ToSingle(Input1.Value) < Convert.ToSingle(Input2.Value);
return;
}
try
{
Result.Value = Comparer.DefaultInvariant.Compare(Input1.Value, Input2.Value) == -1;
}
catch
{
Result.Value = false;
}
}
#endregion
#region Properties & Fields
public InputPin<object> Input1 { get; }
public InputPin<object> Input2 { get; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Equals", "Checks if the two inputs are equals.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))]
public class EqualsNode : Node
{
#region Constructors
public EqualsNode()
: base("Equals", "Checks if the two inputs are equals.")
{
Input1 = CreateInputPin<object>();
Input2 = CreateInputPin<object>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
try
{
Result.Value = Equals(Input1.Value, Input2.Value);
}
catch
{
Result.Value = false;
}
}
#endregion
#region Properties & Fields
public InputPin<object> Input1 { get; }
public InputPin<object> Input2 { get; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Negate", "Negates the boolean.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))]
public class NegateNode : Node
{
#region Constructors
public NegateNode()
: base("Negate", "Negates the boolean.")
{
Input = CreateInputPin<bool>();
Output = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = !Input.Value;
}
#endregion
#region Properties & Fields
public InputPin<bool> Input { get; }
public OutputPin<bool> Output { get; }
#endregion
}
[Node("And", "Checks if all inputs are true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))]
public class AndNode : Node
{
#region Constructors
public AndNode()
: base("And", "Checks if all inputs are true.")
{
Input = CreateInputPinCollection<bool>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
Result.Value = Input.Values.All(v => v);
}
#endregion
#region Properties & Fields
public InputPinCollection<bool> Input { get; set; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Or", "Checks if any inputs are true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))]
public class OrNode : Node
{
#region Constructors
public OrNode()
: base("Or", "Checks if any inputs are true.")
{
Input = CreateInputPinCollection<bool>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
Result.Value = Input.Values.Any(v => v);
}
#endregion
#region Properties & Fields
public InputPinCollection<bool> Input { get; set; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Exclusive Or", "Checks if one of the inputs is true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))]
public class XorNode : Node
{
#region Constructors
public XorNode()
: base("Exclusive Or", "Checks if one of the inputs is true.")
{
Input = CreateInputPinCollection<bool>();
Result = CreateOutputPin<bool>();
}
#endregion
#region Methods
public override void Evaluate()
{
Result.Value = Input.Values.Count(v => v) == 1;
}
#endregion
#region Properties & Fields
public InputPinCollection<bool> Input { get; set; }
public OutputPin<bool> Result { get; }
#endregion
}
[Node("Enum Equals", "Determines the equality between an input and a selected enum value", "Operators", InputType = typeof(Enum), OutputType = typeof(bool))]
public class EnumEqualsNode : Node<Enum, EnumEqualsNodeCustomViewModel>
{
public EnumEqualsNode() : base("Enum Equals", "Determines the equality between an input and a selected enum value")
{
InputPin = CreateInputPin<Enum>();
OutputPin = CreateOutputPin<bool>();
}
public InputPin<Enum> InputPin { get; }
public OutputPin<bool> OutputPin { get; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
OutputPin.Value = InputPin.Value != null && InputPin.Value.Equals(Storage);
}
#endregion
}

View File

@ -0,0 +1,26 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Brighten Color", "Brightens a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class BrightenSKColorNode : Node
{
public BrightenSKColorNode() : base("Brighten Color", "Brightens a color by a specified amount in percent")
{
Input = CreateInputPin<SKColor>("Color");
Percentage = CreateInputPin<Numeric>("%");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public InputPin<Numeric> Percentage { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
l += l * (Percentage.Value / 100f);
Output.Value = SKColor.FromHsl(h, s, l);
}
}

View File

@ -0,0 +1,10 @@
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Color.CustomViewModels;
public class StaticSKColorValueNodeCustomViewModel : CustomNodeViewModel
{
public StaticSKColorValueNodeCustomViewModel(StaticSKColorValueNode node) : base(node)
{
}
}

View File

@ -0,0 +1,15 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.Color.CustomViews.StaticSKColorValueNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources>
<shared:ColorPicker VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Color="{Binding Node.Storage, Converter={StaticResource SKColorToColorConverter}}" />
</UserControl>

View File

@ -0,0 +1,26 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Darken Color", "Darkens a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class DarkenSKColorNode : Node
{
public DarkenSKColorNode() : base("Darken Color", "Darkens a color by a specified amount in percent")
{
Input = CreateInputPin<SKColor>("Color");
Percentage = CreateInputPin<Numeric>("%");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public InputPin<Numeric> Percentage { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
l -= l * (Percentage.Value / 100f);
Output.Value = SKColor.FromHsl(h, s, l);
}
}

View File

@ -0,0 +1,26 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Desaturate Color", "Desaturates a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class DesaturateSKColorNode : Node
{
public DesaturateSKColorNode() : base("Desaturate Color", "Desaturates a color by a specified amount in percent")
{
Input = CreateInputPin<SKColor>("Color");
Percentage = CreateInputPin<Numeric>("%");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public InputPin<Numeric> Percentage { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
s -= s * (Percentage.Value / 100f);
Output.Value = SKColor.FromHsl(h, Math.Clamp(s, 0f, 100f), l);
}
}

View File

@ -0,0 +1,31 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("HSL Color", "Creates a color from hue, saturation and lightness values", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
public class HslSKColorNode : Node
{
public HslSKColorNode() : base("HSL Color", "Creates a color from hue, saturation and lightness values")
{
H = CreateInputPin<Numeric>("H");
S = CreateInputPin<Numeric>("S");
L = CreateInputPin<Numeric>("L");
Output = CreateOutputPin<SKColor>();
}
public InputPin<Numeric> H { get; set; }
public InputPin<Numeric> S { get; set; }
public InputPin<Numeric> L { get; set; }
public OutputPin<SKColor> Output { get; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
Output.Value = SKColor.FromHsl(H.Value, S.Value, L.Value);
}
#endregion
}

View File

@ -0,0 +1,27 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Invert Color", "Inverts a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class InvertSKColorNode : Node
{
public InvertSKColorNode() : base("Invert Color", "Inverts a color")
{
Input = CreateInputPin<SKColor>();
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Output.Value = new SKColor(
(byte) (255 - Input.Value.Red),
(byte) (255 - Input.Value.Green),
(byte) (255 - Input.Value.Blue),
Input.Value.Alpha
);
}
}

View File

@ -0,0 +1,26 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Rotate Color Hue", "Rotates the hue of a color by a specified amount in degrees", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class RotateHueSKColorNode : Node
{
public RotateHueSKColorNode() : base("Rotate Color Hue", "Rotates the hue of a color by a specified amount in degrees")
{
Input = CreateInputPin<SKColor>("Color");
Amount = CreateInputPin<Numeric>("Amount");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public InputPin<Numeric> Amount { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
h += Amount.Value;
Output.Value = SKColor.FromHsl(h % 360, s, l);
}
}

View File

@ -0,0 +1,26 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Saturate Color", "Saturates a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class SaturateSKColorNode : Node
{
public SaturateSKColorNode() : base("Saturate Color", "Saturates a color by a specified amount in percent")
{
Input = CreateInputPin<SKColor>("Color");
Percentage = CreateInputPin<Numeric>("%");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; }
public InputPin<Numeric> Percentage { get; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
s += s * (Percentage.Value / 100f);
Output.Value = SKColor.FromHsl(h, Math.Clamp(s, 0f, 100f), l);
}
}

View File

@ -0,0 +1,34 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Color.CustomViewModels;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color-Value", "Outputs a configurable color value.", "Static", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class StaticSKColorValueNode : Node<SKColor, StaticSKColorValueNodeCustomViewModel>
{
#region Constructors
public StaticSKColorValueNode()
: base("Color", "Outputs a configurable color value.")
{
Output = CreateOutputPin<SKColor>();
}
#endregion
#region Properties & Fields
public OutputPin<SKColor> Output { get; }
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = Storage;
}
#endregion
}

View File

@ -0,0 +1,45 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Sum (Color)", "Sums the connected color values.", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class SumSKColorsNode : Node
{
#region Constructors
public SumSKColorsNode()
: base("Sum", "Sums the connected color values.")
{
Values = CreateInputPinCollection<SKColor>("Values", 2);
Sum = CreateOutputPin<SKColor>("Sum");
}
#endregion
#region Methods
public override void Evaluate()
{
SKColor result = SKColor.Empty;
bool first = true;
foreach (SKColor current in Values.Values)
{
result = first ? current : result.Sum(current);
first = false;
}
Sum.Value = result;
}
#endregion
#region Properties & Fields
public InputPinCollection<SKColor> Values { get; }
public OutputPin<SKColor> Sum { get; }
#endregion
}

View File

@ -0,0 +1,80 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes;
[Node("To String", "Converts the input to a string.", "Conversion", InputType = typeof(object), OutputType = typeof(string))]
public class ConvertToStringNode : Node
{
#region Constructors
public ConvertToStringNode()
: base("To String", "Converts the input to a string.")
{
Input = CreateInputPin<object>();
String = CreateOutputPin<string>();
}
#endregion
#region Methods
public override void Evaluate()
{
String.Value = Input.Value?.ToString();
}
#endregion
#region Properties & Fields
public InputPin<object> Input { get; }
public OutputPin<string> String { get; }
#endregion
}
[Node("To Numeric", "Converts the input to a numeric.", "Conversion", InputType = typeof(object), OutputType = typeof(Numeric))]
public class ConvertToNumericNode : Node
{
#region Constructors
public ConvertToNumericNode()
: base("To Numeric", "Converts the input to a numeric.")
{
Input = CreateInputPin<object>();
Output = CreateOutputPin<Numeric>();
}
#endregion
#region Properties & Fields
public InputPin<object> Input { get; }
public OutputPin<Numeric> Output { get; }
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = Input.Value switch
{
int input => new Numeric(input),
double input => new Numeric(input),
float input => new Numeric(input),
byte input => new Numeric(input),
_ => TryParse(Input.Value)
};
}
private Numeric TryParse(object input)
{
Numeric.TryParse(input?.ToString(), out Numeric value);
return value;
}
#endregion
}

View File

@ -0,0 +1,49 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.UI.Shared.VisualScripting;
using DynamicData;
namespace Artemis.VisualScripting.Nodes.CustomViewModels;
public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
{
private readonly EnumEqualsNode _node;
public EnumEqualsNodeCustomViewModel(EnumEqualsNode node) : base(node)
{
_node = node;
}
public ObservableCollection<(Enum, string)> EnumValues { get; } = new();
// public override void OnActivate()
// {
// _node.InputPin.PinConnected += InputPinOnPinConnected;
// _node.InputPin.PinDisconnected += InputPinOnPinDisconnected;
//
// if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum)
// EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType()));
// base.OnActivate();
// }
//
// public override void OnDeactivate()
// {
// _node.InputPin.PinConnected -= InputPinOnPinConnected;
// _node.InputPin.PinDisconnected -= InputPinOnPinDisconnected;
//
// base.OnDeactivate();
// }
private void InputPinOnPinDisconnected(object sender, SingleValueEventArgs<IPin> e)
{
EnumValues.Clear();
}
private void InputPinOnPinConnected(object sender, SingleValueEventArgs<IPin> e)
{
EnumValues.Clear();
if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum)
EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType()));
}
}

View File

@ -0,0 +1,73 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.CustomViewModels;
public class LayerPropertyNodeCustomViewModel : CustomNodeViewModel
{
private readonly LayerPropertyNode _node;
private ILayerProperty _selectedLayerProperty;
private RenderProfileElement _selectedProfileElement;
public LayerPropertyNodeCustomViewModel(LayerPropertyNode node) : base(node)
{
_node = node;
}
public ObservableCollection<RenderProfileElement> ProfileElements { get; } = new();
// public RenderProfileElement SelectedProfileElement
// {
// get => _selectedProfileElement;
// set
// {
// if (!SetAndNotify(ref _selectedProfileElement, value)) return;
// _node.ChangeProfileElement(_selectedProfileElement);
// GetLayerProperties();
// }
// }
public ObservableCollection<ILayerProperty> LayerProperties { get; } = new();
// public ILayerProperty SelectedLayerProperty
// {
// get => _selectedLayerProperty;
// set
// {
// if (!SetAndNotify(ref _selectedLayerProperty, value)) return;
// _node.ChangeLayerProperty(_selectedLayerProperty);
// }
// }
// private void GetProfileElements()
// {
// ProfileElements.Clear();
// if (_node.Script.Context is not Profile profile)
// return;
//
// List<RenderProfileElement> elements = new(profile.GetAllRenderElements());
//
// ProfileElements.AddRange(elements.OrderBy(e => e.Order));
// _selectedProfileElement = _node.ProfileElement;
// NotifyOfPropertyChange(nameof(SelectedProfileElement));
// }
//
// private void GetLayerProperties()
// {
// LayerProperties.Clear();
// if (_node.ProfileElement == null)
// return;
//
// LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => !l.IsHidden && l.DataBindingsSupported));
// _selectedLayerProperty = _node.LayerProperty;
// NotifyOfPropertyChange(nameof(SelectedLayerProperty));
// }
//
// public override void OnActivate()
// {
// GetProfileElements();
// GetLayerProperties();
// }
}

View File

@ -0,0 +1,18 @@
using Artemis.Core;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.CustomViewModels;
public class StaticNumericValueNodeCustomViewModel : CustomNodeViewModel
{
public StaticNumericValueNodeCustomViewModel(INode node) : base(node)
{
}
}
public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel
{
public StaticStringValueNodeCustomViewModel(INode node) : base(node)
{
}
}

View File

@ -0,0 +1,25 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.EnumEqualsNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<ComboBox Width="140"
materialDesign:ComboBoxAssist.ClassicMode="True"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
SelectedValue="{Binding Node.Storage}"
ItemsSource="{Binding EnumValues}"
SelectedValuePath="Value"
DisplayMemberPath="Description">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,17 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.LayerPropertyNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel>
<ComboBox Margin="8 0"
ItemsSource="{Binding ProfileElements}"
SelectedValue="{Binding SelectedProfileElement}" />
<ComboBox Margin="8 0"
ItemsSource="{Binding LayerProperties}"
SelectedValue="{Binding SelectedLayerProperty}"
DisplayMemberPath="PropertyDescription.Name" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,15 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:StringToNumericConverter x:Key="StringToNumericConverter" />
</UserControl.Resources>
<TextBox VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Text="{Binding Node.Storage, Converter={StaticResource StringToNumericConverter}}" />
</UserControl>

View File

@ -0,0 +1,11 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticStringValueNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<TextBox VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Text="{Binding Node.Storage}" />
</UserControl>

View File

@ -0,0 +1,72 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels;
public class DataModelEventNodeCustomViewModel : CustomNodeViewModel
{
private readonly DataModelEventNode _node;
private ObservableCollection<Module> _modules;
public DataModelEventNodeCustomViewModel(DataModelEventNode node, ISettingsService settingsService) : base(node)
{
_node = node;
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
}
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public ObservableCollection<Type> FilterTypes { get; } = new() {typeof(IDataModelEvent)};
// public ObservableCollection<Module> Modules
// {
// get => _modules;
// set => SetAndNotify(ref _modules, value);
// }
public DataModelPath DataModelPath
{
get => _node.DataModelPath;
set
{
if (ReferenceEquals(_node.DataModelPath, value))
return;
_node.DataModelPath?.Dispose();
_node.DataModelPath = value;
_node.DataModelPath.Save();
_node.Storage = _node.DataModelPath.Entity;
}
}
// public override void OnActivate()
// {
// if (Modules != null)
// return;
//
// Modules = new ObservableCollection<Module>();
// if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
// Modules.Add(scriptProfile.Configuration.Module);
// else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
// Modules.Add(profileConfiguration.Module);
//
// _node.PropertyChanged += NodeOnPropertyChanged;
// }
//
// public override void OnDeactivate()
// {
// _node.PropertyChanged -= NodeOnPropertyChanged;
// }
//
// private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
// {
// if (e.PropertyName == nameof(DataModelNode.DataModelPath))
// OnPropertyChanged(nameof(DataModelPath));
// }
}

View File

@ -0,0 +1,72 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels;
public class DataModelNodeCustomViewModel : CustomNodeViewModel
{
private readonly DataModelNode _node;
private ObservableCollection<Module> _modules;
public DataModelNodeCustomViewModel(DataModelNode node, ISettingsService settingsService) : base(node)
{
_node = node;
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
}
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
// public ObservableCollection<Module> Modules
// {
// get => _modules;
// set => SetAndNotify(ref _modules, value);
// }
public DataModelPath DataModelPath
{
get => _node.DataModelPath;
set
{
if (ReferenceEquals(_node.DataModelPath, value))
return;
_node.DataModelPath?.Dispose();
_node.DataModelPath = value;
_node.DataModelPath.Save();
_node.Storage = _node.DataModelPath.Entity;
_node.UpdateOutputPin(false);
}
}
// public override void OnActivate()
// {
// if (Modules != null)
// return;
//
// Modules = new ObservableCollection<Module>();
// if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
// Modules.Add(scriptProfile.Configuration.Module);
// else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
// Modules.Add(profileConfiguration.Module);
//
// _node.PropertyChanged += NodeOnPropertyChanged;
// }
//
// public override void OnDeactivate()
// {
// _node.PropertyChanged -= NodeOnPropertyChanged;
// }
//
// private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
// {
// if (e.PropertyName == nameof(DataModelNode.DataModelPath))
// OnPropertyChanged(nameof(DataModelPath));
// }
}

View File

@ -0,0 +1,18 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelEventNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel Orientation="Vertical">
<controls:DataModelPicker DataModelPath="{Binding DataModelPath}"
Modules="{Binding Modules}"
ShowFullPath="{Binding ShowFullPaths.Value}"
ShowDataModelValues="{Binding ShowDataModelValues.Value}"
FilterTypes="{Binding FilterTypes}"
ButtonBrush="DarkGoldenrod" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.DataModel.CustomViews.DataModelNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<controls:DataModelPicker DataModelPath="{Binding DataModelPath}"
Modules="{Binding Modules}"
ShowFullPath="{Binding ShowFullPaths.Value}"
ShowDataModelValues="{Binding ShowDataModelValues.Value}"
ButtonBrush="#434343" />
</UserControl>

View File

@ -0,0 +1,187 @@
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels;
namespace Artemis.VisualScripting.Nodes.DataModel;
[Node("Data Model-Event", "Responds to a data model event trigger", "Data Model", OutputType = typeof(bool))]
public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
{
private int _currentIndex;
private Type _currentType;
private DataModelPath _dataModelPath;
private DateTime _lastTrigger;
private bool _updating;
public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger")
{
_currentType = typeof(object);
CreateCycleValues(typeof(object), 1);
Output = CreateOutputPin(typeof(object));
}
public InputPinCollection CycleValues { get; set; }
public OutputPin Output { get; set; }
public INodeScript Script { get; set; }
public DataModelPath DataModelPath
{
get => _dataModelPath;
set => SetAndNotify(ref _dataModelPath, value);
}
public override void Initialize(INodeScript script)
{
Script = script;
if (Storage == null)
return;
DataModelPath = new DataModelPath(Storage);
}
public override void Evaluate()
{
object outputValue = null;
if (DataModelPath?.GetValue() is IDataModelEvent dataModelEvent)
{
if (dataModelEvent.LastTrigger > _lastTrigger)
{
_lastTrigger = dataModelEvent.LastTrigger;
_currentIndex++;
if (_currentIndex >= CycleValues.Count())
_currentIndex = 0;
}
outputValue = CycleValues.ElementAt(_currentIndex).PinValue;
}
if (Output.Type.IsInstanceOfType(outputValue))
Output.Value = outputValue;
else if (Output.Type.IsValueType)
Output.Value = Output.Type.GetDefault()!;
}
private void CreateCycleValues(Type type, int initialCount)
{
if (CycleValues != null)
{
CycleValues.PinAdded -= CycleValuesOnPinAdded;
CycleValues.PinRemoved -= CycleValuesOnPinRemoved;
foreach (IPin pin in CycleValues)
{
pin.PinConnected -= OnPinConnected;
pin.PinDisconnected -= OnPinDisconnected;
}
RemovePinCollection(CycleValues);
}
CycleValues = CreateInputPinCollection(type, "", initialCount);
CycleValues.PinAdded += CycleValuesOnPinAdded;
CycleValues.PinRemoved += CycleValuesOnPinRemoved;
foreach (IPin pin in CycleValues)
{
pin.PinConnected += OnPinConnected;
pin.PinDisconnected += OnPinDisconnected;
}
}
private void CycleValuesOnPinAdded(object sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected += OnPinConnected;
e.Value.PinDisconnected += OnPinDisconnected;
}
private void CycleValuesOnPinRemoved(object sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected -= OnPinConnected;
e.Value.PinDisconnected -= OnPinDisconnected;
}
private void OnPinDisconnected(object sender, SingleValueEventArgs<IPin> e)
{
ProcessPinDisconnected();
}
private void OnPinConnected(object sender, SingleValueEventArgs<IPin> e)
{
IPin source = e.Value;
IPin target = (IPin) sender;
ProcessPinConnected(source, target);
}
private void ProcessPinConnected(IPin source, IPin target)
{
if (_updating)
return;
try
{
_updating = true;
// No need to change anything if the types haven't changed
if (_currentType == source.Type)
return;
int reconnectIndex = CycleValues.ToList().IndexOf(target);
ChangeCurrentType(source.Type);
if (reconnectIndex != -1)
{
CycleValues.ToList()[reconnectIndex].ConnectTo(source);
source.ConnectTo(CycleValues.ToList()[reconnectIndex]);
}
}
finally
{
_updating = false;
}
}
private void ChangeCurrentType(Type type)
{
CreateCycleValues(type, CycleValues.Count());
List<IPin> oldOutputConnections = Output.ConnectedTo.ToList();
RemovePin(Output);
Output = CreateOutputPin(type);
foreach (IPin oldOutputConnection in oldOutputConnections.Where(o => o.Type.IsAssignableFrom(Output.Type)))
{
oldOutputConnection.DisconnectAll();
oldOutputConnection.ConnectTo(Output);
Output.ConnectTo(oldOutputConnection);
}
_currentType = type;
}
private void ProcessPinDisconnected()
{
if (_updating)
return;
try
{
// If there's still a connected pin, stick to the current type
if (CycleValues.Any(v => v.ConnectedTo.Any()))
return;
ChangeCurrentType(typeof(object));
}
finally
{
_updating = false;
}
}
private void UpdatePinsType(IPin source)
{
}
/// <inheritdoc />
public void Dispose()
{
}
}

View File

@ -0,0 +1,95 @@
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels;
using Avalonia.Threading;
namespace Artemis.VisualScripting.Nodes.DataModel;
[Node("Data Model-Value", "Outputs a selectable data model value.", "Data Model")]
public class DataModelNode : Node<DataModelPathEntity, DataModelNodeCustomViewModel>, IDisposable
{
private DataModelPath _dataModelPath;
public DataModelNode() : base("Data Model", "Outputs a selectable data model value")
{
}
public INodeScript Script { get; private set; }
public OutputPin Output { get; private set; }
public DataModelPath DataModelPath
{
get => _dataModelPath;
set => SetAndNotify(ref _dataModelPath, value);
}
public override void Initialize(INodeScript script)
{
Script = script;
if (Storage == null)
return;
DataModelPath = new DataModelPath(Storage);
DataModelPath.PathValidated += DataModelPathOnPathValidated;
UpdateOutputPin(false);
}
public override void Evaluate()
{
if (DataModelPath.IsValid)
{
if (Output == null)
UpdateOutputPin(false);
object pathValue = DataModelPath.GetValue();
if (pathValue == null)
{
if (!Output.Type.IsValueType)
Output.Value = null;
}
else
{
if (Output.Type == typeof(Numeric))
Output.Value = new Numeric(pathValue);
else
Output.Value = pathValue;
}
}
}
public void UpdateOutputPin(bool loadConnections)
{
Type type = DataModelPath?.GetPropertyType();
if (Numeric.IsTypeCompatible(type))
type = typeof(Numeric);
if (Output != null && Output.Type == type)
return;
if (Output != null)
{
RemovePin(Output);
Output = null;
}
if (type != null)
Output = CreateOutputPin(type);
if (loadConnections && Script is NodeScript nodeScript)
nodeScript.LoadConnections();
}
private void DataModelPathOnPathValidated(object sender, EventArgs e)
{
Dispatcher.UIThread.InvokeAsync(() => UpdateOutputPin(true));
}
/// <inheritdoc />
public void Dispose()
{
DataModelPath?.Dispose();
}
}

View File

@ -0,0 +1,54 @@
using System.Collections.ObjectModel;
using Artemis.Core;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Easing.CustomViewModels;
public class EasingTypeNodeCustomViewModel : CustomNodeViewModel
{
private readonly EasingTypeNode _node;
private NodeEasingViewModel _selectedEasingViewModel;
public EasingTypeNodeCustomViewModel(EasingTypeNode node) : base(node)
{
_node = node;
EasingViewModels = new ObservableCollection<NodeEasingViewModel>(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(e => new NodeEasingViewModel(e)));
}
public ObservableCollection<NodeEasingViewModel> EasingViewModels { get; }
public NodeEasingViewModel SelectedEasingViewModel
{
get => _selectedEasingViewModel;
set
{
_selectedEasingViewModel = value;
_node.Storage = _selectedEasingViewModel.EasingFunction;
}
}
// public override void OnActivate()
// {
// _node.PropertyChanged += NodeOnPropertyChanged;
// SelectedEasingViewModel = GetNodeEasingViewModel();
// }
//
// public override void OnDeactivate()
// {
// _node.PropertyChanged -= NodeOnPropertyChanged;
// }
//
// private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
// {
// if (e.PropertyName == nameof(_node.Storage))
// {
// _selectedEasingViewModel = GetNodeEasingViewModel();
// NotifyOfPropertyChange(nameof(SelectedEasingViewModel));
// }
// }
private NodeEasingViewModel GetNodeEasingViewModel()
{
return EasingViewModels.FirstOrDefault(vm => vm.EasingFunction == _node.Storage);
}
}

View File

@ -0,0 +1,27 @@
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
namespace Artemis.VisualScripting.Nodes.Easing.CustomViewModels;
public class NodeEasingViewModel : ViewModelBase
{
public NodeEasingViewModel(Easings.Functions easingFunction)
{
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
EasingPoints = new List<Point>();
for (int i = 1; i <= 10; i++)
{
int x = i;
double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
public Easings.Functions EasingFunction { get; }
public List<Point> EasingPoints { get; }
public string Description { get; }
}

View File

@ -0,0 +1,34 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.Easing.CustomViews.EasingTypeNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Artemis.VisualScripting.Nodes.Easing.CustomViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<ComboBox VerticalAlignment="Center"
HorizontalAlignment="Stretch"
ItemsSource="{Binding EasingViewModels}"
SelectedItem="{Binding SelectedEasingViewModel}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:NodeEasingViewModel}">
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1.5"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="14"
Height="14"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>

View File

@ -0,0 +1,20 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Easing.CustomViewModels;
namespace Artemis.VisualScripting.Nodes.Easing;
[Node("Easing Type", "Outputs a selectable easing type.", "Easing", OutputType = typeof(Easings.Functions))]
public class EasingTypeNode : Node<Easings.Functions, EasingTypeNodeCustomViewModel>
{
public EasingTypeNode() : base("Easing Type", "Outputs a selectable easing type.")
{
Output = CreateOutputPin<Easings.Functions>();
}
public OutputPin<Easings.Functions> Output { get; }
public override void Evaluate()
{
Output.Value = Storage;
}
}

View File

@ -0,0 +1,66 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes.Easing;
[Node("Numeric Easing", "Outputs an eased numeric value", "Easing", InputType = typeof(Numeric), OutputType = typeof(Numeric))]
public class NumericEasingNode : Node
{
private float _currentValue;
private DateTime _lastEvaluate = DateTime.MinValue;
private float _progress;
private float _sourceValue;
private float _targetValue;
public NumericEasingNode() : base("Numeric Easing", "Outputs an eased numeric value")
{
Input = CreateInputPin<Numeric>();
EasingTime = CreateInputPin<Numeric>("delay");
EasingFunction = CreateInputPin<Easings.Functions>("function");
Output = CreateOutputPin<Numeric>();
}
public InputPin<Numeric> Input { get; set; }
public InputPin<Numeric> EasingTime { get; set; }
public InputPin<Easings.Functions> EasingFunction { get; set; }
public OutputPin<Numeric> Output { get; set; }
public override void Evaluate()
{
DateTime now = DateTime.Now;
// If the value changed reset progress
if (Math.Abs(_targetValue - Input.Value) > 0.001f)
{
_sourceValue = _currentValue;
_targetValue = Input.Value;
_progress = 0f;
}
// Update until finished
if (_progress < 1f)
{
Update();
Output.Value = new Numeric(_currentValue);
}
// Stop updating past 1 and use the target value
else
{
Output.Value = new Numeric(_targetValue);
}
_lastEvaluate = now;
}
private void Update()
{
TimeSpan delta = DateTime.Now - _lastEvaluate;
// In case of odd delta's, keep progress between 0f and 1f
_progress = Math.Clamp(_progress + (float) delta.TotalMilliseconds / EasingTime.Value, 0f, 1f);
double eased = _sourceValue + (_targetValue - _sourceValue) * Easings.Interpolate(_progress, EasingFunction.Value);
_currentValue = (float) eased;
}
}

View File

@ -0,0 +1,65 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Easing;
[Node("Color Easing", "Outputs an eased color value", "Easing", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class SKColorEasingNode : Node
{
private SKColor _currentValue;
private DateTime _lastEvaluate = DateTime.MinValue;
private float _progress;
private SKColor _sourceValue;
private SKColor _targetValue;
public SKColorEasingNode() : base("Color Easing", "Outputs an eased color value")
{
Input = CreateInputPin<SKColor>();
EasingTime = CreateInputPin<Numeric>("delay");
EasingFunction = CreateInputPin<Easings.Functions>("function");
Output = CreateOutputPin<SKColor>();
}
public InputPin<SKColor> Input { get; set; }
public InputPin<Numeric> EasingTime { get; set; }
public InputPin<Easings.Functions> EasingFunction { get; set; }
public OutputPin<SKColor> Output { get; set; }
public override void Evaluate()
{
DateTime now = DateTime.Now;
// If the value changed reset progress
if (_targetValue != Input.Value)
{
_sourceValue = _currentValue;
_targetValue = Input.Value;
_progress = 0f;
}
// Update until finished
if (_progress < 1f)
{
Update();
Output.Value = _currentValue;
}
// Stop updating past 1 and use the target value
else
{
Output.Value = _targetValue;
}
_lastEvaluate = now;
}
private void Update()
{
TimeSpan delta = DateTime.Now - _lastEvaluate;
// In case of odd delta's, keep progress between 0f and 1f
_progress = Math.Clamp(_progress + (float) delta.TotalMilliseconds / EasingTime.Value, 0f, 1f);
_currentValue = _sourceValue.Interpolate(_targetValue, (float) Easings.Interpolate(_progress, EasingFunction.Value));
}
}

View File

@ -0,0 +1,124 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.CustomViewModels;
namespace Artemis.VisualScripting.Nodes;
[Node("Layer/Folder Property", "Outputs the property of a selected layer or folder", "External")]
public class LayerPropertyNode : Node<LayerPropertyNodeEntity, LayerPropertyNodeCustomViewModel>
{
private readonly object _layerPropertyLock = new();
public INodeScript Script { get; private set; }
public RenderProfileElement ProfileElement { get; private set; }
public ILayerProperty LayerProperty { get; private set; }
public override void Evaluate()
{
lock (_layerPropertyLock)
{
// In this case remove the pins so no further evaluations occur
if (LayerProperty == null)
{
CreatePins();
return;
}
List<IDataBindingProperty> list = LayerProperty.BaseDataBinding.Properties.ToList();
int index = 0;
foreach (IPin pin in Pins)
{
OutputPin outputPin = (OutputPin) pin;
IDataBindingProperty dataBindingProperty = list[index];
index++;
// TODO: Is this really non-nullable?
outputPin.Value = dataBindingProperty.GetValue();
}
}
}
public override void Initialize(INodeScript script)
{
Script = script;
if (script.Context is Profile profile)
profile.ChildRemoved += ProfileOnChildRemoved;
LoadLayerProperty();
}
public void LoadLayerProperty()
{
lock (_layerPropertyLock)
{
if (Script.Context is not Profile profile || Storage == null)
return;
RenderProfileElement element = profile.GetAllRenderElements().FirstOrDefault(l => l.EntityId == Storage.ElementId);
ProfileElement = element;
LayerProperty = element?.GetAllLayerProperties().FirstOrDefault(p => p.Path == Storage.PropertyPath);
CreatePins();
}
}
public void ChangeProfileElement(RenderProfileElement profileElement)
{
lock (_layerPropertyLock)
{
ProfileElement = profileElement;
LayerProperty = null;
Storage = new LayerPropertyNodeEntity
{
ElementId = ProfileElement?.EntityId ?? Guid.Empty,
PropertyPath = null
};
CreatePins();
}
}
public void ChangeLayerProperty(ILayerProperty layerProperty)
{
lock (_layerPropertyLock)
{
LayerProperty = layerProperty;
Storage = new LayerPropertyNodeEntity
{
ElementId = ProfileElement?.EntityId ?? Guid.Empty,
PropertyPath = LayerProperty?.Path
};
CreatePins();
}
}
private void CreatePins()
{
while (Pins.Any())
RemovePin((Pin) Pins.First());
if (LayerProperty == null)
return;
foreach (IDataBindingProperty dataBindingRegistration in LayerProperty.BaseDataBinding.Properties)
CreateOutputPin(dataBindingRegistration.ValueType, dataBindingRegistration.DisplayName);
}
private void ProfileOnChildRemoved(object sender, EventArgs e)
{
if (Script.Context is not Profile profile)
return;
if (!profile.GetAllRenderElements().Contains(ProfileElement))
ChangeProfileElement(null);
}
}
public class LayerPropertyNodeEntity
{
public Guid ElementId { get; set; }
public string PropertyPath { get; set; }
}

View File

@ -0,0 +1,11 @@
using Artemis.Core;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Maths.CustomViewModels;
public class MathExpressionNodeCustomViewModel : CustomNodeViewModel
{
public MathExpressionNodeCustomViewModel(INode node) : base(node)
{
}
}

View File

@ -0,0 +1,11 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.Maths.CustomViews.MathExpressionNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<TextBox VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Text="{Binding Node.Storage}" />
</UserControl>

View File

@ -0,0 +1,116 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Maths.CustomViewModels;
using NoStringEvaluating.Contract;
using NoStringEvaluating.Contract.Variables;
using NoStringEvaluating.Models.Values;
namespace Artemis.VisualScripting.Nodes.Maths;
[Node("Math Expression", "Outputs the result of a math expression.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))]
public class MathExpressionNode : Node<string, MathExpressionNodeCustomViewModel>
{
private readonly INoStringEvaluator _evaluator;
private readonly PinsVariablesContainer _variables;
#region Constructors
public MathExpressionNode(INoStringEvaluator evaluator)
: base("Math Expression", "Outputs the result of a math expression.")
{
_evaluator = evaluator;
Output = CreateOutputPin<Numeric>();
Values = CreateInputPinCollection<Numeric>("Values", 2);
Values.PinAdded += (_, _) => SetPinNames();
Values.PinRemoved += (_, _) => SetPinNames();
_variables = new PinsVariablesContainer(Values);
SetPinNames();
}
#endregion
#region Properties & Fields
public OutputPin<Numeric> Output { get; }
public InputPinCollection<Numeric> Values { get; }
#endregion
#region Methods
public override void Evaluate()
{
if (Storage != null)
Output.Value = new Numeric(_evaluator.CalcNumber(Storage, _variables));
}
private void SetPinNames()
{
int index = 1;
foreach (IPin value in Values)
{
value.Name = ExcelColumnFromNumber(index).ToLower();
index++;
}
}
public static string ExcelColumnFromNumber(int column)
{
string columnString = "";
decimal columnNumber = column;
while (columnNumber > 0)
{
decimal currentLetterNumber = (columnNumber - 1) % 26;
char currentLetter = (char) (currentLetterNumber + 65);
columnString = currentLetter + columnString;
columnNumber = (columnNumber - (currentLetterNumber + 1)) / 26;
}
return columnString;
}
#endregion
}
public class PinsVariablesContainer : IVariablesContainer
{
private readonly InputPinCollection<Numeric> _values;
public PinsVariablesContainer(InputPinCollection<Numeric> values)
{
_values = values;
}
#region Implementation of IVariablesContainer
/// <inheritdoc />
public IVariable AddOrUpdate(string name, double value)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public EvaluatorValue GetValue(string name)
{
IPin pin = _values.FirstOrDefault(v => v.Name == name);
if (pin?.PinValue is Numeric numeric)
return new EvaluatorValue(numeric);
return new EvaluatorValue(0);
}
/// <inheritdoc />
public bool TryGetValue(string name, out EvaluatorValue value)
{
IPin pin = _values.FirstOrDefault(v => v.Name == name);
if (pin?.PinValue is Numeric numeric)
{
value = new EvaluatorValue(numeric);
return true;
}
value = new EvaluatorValue(0);
return false;
}
#endregion
}

View File

@ -0,0 +1,26 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes.Maths;
[Node("Round", "Outputs a rounded numeric value.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))]
public class RoundNode : Node
{
public RoundNode() : base("Round", "Outputs a rounded numeric value.")
{
Input = CreateInputPin<Numeric>();
Output = CreateOutputPin<Numeric>();
}
public OutputPin<Numeric> Output { get; set; }
public InputPin<Numeric> Input { get; set; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
Output.Value = new Numeric(MathF.Round(Input.Value, MidpointRounding.AwayFromZero));
}
#endregion
}

View File

@ -0,0 +1,62 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.CustomViewModels;
namespace Artemis.VisualScripting.Nodes;
[Node("Numeric-Value", "Outputs a configurable static numeric value.", "Static", OutputType = typeof(Numeric))]
public class StaticNumericValueNode : Node<Numeric, StaticNumericValueNodeCustomViewModel>
{
#region Constructors
public StaticNumericValueNode()
: base("Numeric", "Outputs a configurable numeric value.")
{
Output = CreateOutputPin<Numeric>();
}
#endregion
#region Properties & Fields
public OutputPin<Numeric> Output { get; }
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = Storage;
}
#endregion
}
[Node("String-Value", "Outputs a configurable static string value.", "Static", OutputType = typeof(string))]
public class StaticStringValueNode : Node<string, StaticStringValueNodeCustomViewModel>
{
#region Constructors
public StaticStringValueNode()
: base("String", "Outputs a configurable string value.")
{
Output = CreateOutputPin<string>();
}
#endregion
#region Properties & Fields
public OutputPin<string> Output { get; }
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = Storage;
}
#endregion
}

View File

@ -0,0 +1,37 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes;
[Node("Format", "Formats the input string.", "Text", InputType = typeof(object), OutputType = typeof(string))]
public class StringFormatNode : Node
{
#region Constructors
public StringFormatNode()
: base("Format", "Formats the input string.")
{
Format = CreateInputPin<string>("Format");
Values = CreateInputPinCollection<object>("Values");
Output = CreateOutputPin<string>("Result");
}
#endregion
#region Methods
public override void Evaluate()
{
Output.Value = string.Format(Format.Value ?? string.Empty, Values.Values.ToArray());
}
#endregion
#region Properties & Fields
public InputPin<string> Format { get; }
public InputPinCollection<object> Values { get; }
public OutputPin<string> Output { get; }
#endregion
}

View File

@ -0,0 +1,35 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes;
[Node("Sum", "Sums the connected numeric values.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))]
public class SumNumericsNode : Node
{
#region Constructors
public SumNumericsNode()
: base("Sum", "Sums the connected numeric values.")
{
Values = CreateInputPinCollection<Numeric>("Values", 2);
Sum = CreateOutputPin<Numeric>("Sum");
}
#endregion
#region Methods
public override void Evaluate()
{
Sum.Value = Values.Values.Sum();
}
#endregion
#region Properties & Fields
public InputPinCollection<Numeric> Values { get; }
public OutputPin<Numeric> Sum { get; }
#endregion
}

File diff suppressed because it is too large Load Diff