1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Core - Implemented node providers and removed nodes from the main project

This commit is contained in:
RobertBeekman 2024-02-23 21:16:08 +01:00 committed by GitHub
commit 93e74ef8d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
181 changed files with 340 additions and 6465 deletions

View File

@ -5,7 +5,7 @@ using Artemis.Core.DryIoc.Factories;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.Storage;
using Artemis.Storage.Migrations.Interfaces;
using Artemis.Storage.Migrations;
using Artemis.Storage.Repositories.Interfaces;
using DryIoc;
@ -36,6 +36,7 @@ public static class ContainerExtensions
// Bind migrations
container.RegisterMany(storageAssembly, type => type.IsAssignableTo<IStorageMigration>(), Reuse.Singleton, nonPublicServiceTypes: true);
container.RegisterMany(storageAssembly, type => type.IsAssignableTo<IProfileMigration>(), Reuse.Singleton, nonPublicServiceTypes: true);
container.RegisterMany(coreAssembly, type => type.IsAssignableTo<ILayoutProvider>(), Reuse.Singleton);
container.Register<IPluginSettingsFactory, PluginSettingsFactory>(Reuse.Singleton);

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using SkiaSharp;
namespace Artemis.Core.Nodes;
/// <summary>
/// Allows you to register one or more <see cref="INode" />s usable by node scripts.
/// </summary>
public abstract class NodeProvider : PluginFeature
{
private readonly List<NodeData> _nodeDescriptors;
/// <summary>
/// Creates a new instance of the <see cref="NodeProvider"/> class.
/// </summary>
public NodeProvider()
{
_nodeDescriptors = new List<NodeData>();
NodeDescriptors = new ReadOnlyCollection<NodeData>(_nodeDescriptors);
Disabled += OnDisabled;
}
/// <summary>
/// A read-only collection of all nodes added with <see cref="RegisterNodeType{T}" />
/// </summary>
public ReadOnlyCollection<NodeData> NodeDescriptors { get; set; }
/// <summary>
/// Adds a node descriptor for a given node, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <typeparam name="T">The type of the node you wish to register</typeparam>
protected void RegisterNodeType<T>() where T : INode
{
RegisterNodeType(typeof(T));
}
/// <summary>
/// Adds a node descriptor for a given node, so that it appears in the UI.
/// <para>Note: You do not need to manually remove these on disable</para>
/// </summary>
/// <param name="nodeType">The type of the node you wish to register</param>
protected void RegisterNodeType(Type nodeType)
{
if (!IsEnabled)
throw new ArtemisPluginFeatureException(this, "Can only add a node descriptor when the plugin is enabled");
if (nodeType == null)
throw new ArgumentNullException(nameof(nodeType));
if (!nodeType.IsAssignableTo(typeof(INode)))
throw new ArgumentException("Node has to be a base type of the Node-Type.", nameof(nodeType));
NodeAttribute? nodeAttribute = nodeType.GetCustomAttribute<NodeAttribute>();
string name = nodeAttribute?.Name ?? nodeType.Name;
string description = nodeAttribute?.Description ?? string.Empty;
string category = nodeAttribute?.Category ?? string.Empty;
string helpUrl = nodeAttribute?.HelpUrl ?? string.Empty;
NodeData nodeData = new(this, nodeType, name, description, category, helpUrl, nodeAttribute?.InputType, nodeAttribute?.OutputType);
_nodeDescriptors.Add(nodeData);
NodeTypeStore.Add(nodeData);
}
/// <summary>
/// Adds a color for lines of the provided type.
/// </summary>
/// <param name="color">The color to add.</param>
/// <typeparam name="T">The type to use the color for.</typeparam>
protected TypeColorRegistration RegisterTypeColor<T>(SKColor color)
{
return NodeTypeStore.AddColor(typeof(T), color, this);
}
private void OnDisabled(object? sender, EventArgs e)
{
// The store will clean up the registrations by itself, the plugin feature just needs to clear its own list
_nodeDescriptors.Clear();
}
}

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Artemis.Storage.Entities.Profile.Nodes;
using DryIoc;
using Newtonsoft.Json;
using SkiaSharp;
@ -13,31 +11,8 @@ namespace Artemis.Core.Services;
internal class NodeService : INodeService
{
#region Constants
private static readonly Type TypeNode = typeof(INode);
#endregion
private readonly IContainer _container;
#region Constructors
public NodeService(IContainer container)
{
_container = container;
}
#endregion
#region Properties & Fields
public IEnumerable<NodeData> AvailableNodes => NodeTypeStore.GetAll();
#endregion
#region Methods
/// <inheritdoc />
public List<Type> GetRegisteredTypes()
{
@ -53,7 +28,7 @@ internal class NodeService : INodeService
// Objects represent an input that can take any type, these are hardcoded white
if (type == typeof(object))
return new TypeColorRegistration(type, new SKColor(255, 255, 255, 255), Constants.CorePlugin);
return new TypeColorRegistration(type, new SKColor(255, 255, 255, 255), Constants.CorePluginFeature);
// Come up with a random color based on the type name that should be the same each time
MD5 md5Hasher = MD5.Create();
@ -61,32 +36,7 @@ internal class NodeService : INodeService
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)
{
if (plugin == null) throw new ArgumentNullException(nameof(plugin));
if (nodeType == null) throw new ArgumentNullException(nameof(nodeType));
if (!TypeNode.IsAssignableFrom(nodeType)) throw new ArgumentException("Node has to be a base type of the Node-Type.", nameof(nodeType));
NodeAttribute? nodeAttribute = nodeType.GetCustomAttribute<NodeAttribute>();
string name = nodeAttribute?.Name ?? nodeType.Name;
string description = nodeAttribute?.Description ?? string.Empty;
string category = nodeAttribute?.Category ?? string.Empty;
string helpUrl = nodeAttribute?.HelpUrl ?? string.Empty;
NodeData nodeData = new(plugin, nodeType, name, description, category, helpUrl, nodeAttribute?.InputType, nodeAttribute?.OutputType, (s, e) => CreateNode(s, e, nodeType));
return NodeTypeStore.Add(nodeData);
}
public TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color)
{
if (plugin == null) throw new ArgumentNullException(nameof(plugin));
if (type == null) throw new ArgumentNullException(nameof(type));
return NodeTypeStore.AddColor(type, color, plugin);
return new TypeColorRegistration(type, baseColor, Constants.CorePluginFeature);
}
public string ExportScript(NodeScript nodeScript)
@ -103,33 +53,6 @@ internal class NodeService : INodeService
target.LoadFromEntity(entity);
}
private INode CreateNode(INodeScript script, NodeEntity? entity, Type nodeType)
{
INode node = _container.Resolve(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode");
if (node is Node concreteNode)
concreteNode.Container = _container;
if (entity != null)
{
node.X = entity.X;
node.Y = entity.Y;
try
{
if (node is Node nodeImplementation)
nodeImplementation.DeserializeStorage(entity.Storage);
}
catch
{
// ignored
}
}
node.TryInitialize(script);
return node;
}
#endregion
}
/// <summary>
@ -153,21 +76,6 @@ public interface INodeService : IArtemisService
/// </summary>
TypeColorRegistration GetTypeColorRegistration(Type type);
/// <summary>
/// Registers a node of the provided <paramref name="nodeType" />
/// </summary>
/// <param name="plugin">The plugin the node belongs to</param>
/// <param name="nodeType">The type of node to initialize</param>
NodeTypeRegistration RegisterNodeType(Plugin plugin, Type nodeType);
/// <summary>
/// Registers a type with a provided color for use in the node editor
/// </summary>
/// <param name="plugin">The plugin making the registration</param>
/// <param name="type">The type to associate the color with</param>
/// <param name="color">The color to display</param>
TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color);
/// <summary>
/// Exports the provided node script to JSON.
/// </summary>

View File

@ -8,8 +8,10 @@ using System.Text;
using System.Threading.Tasks;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Migrations;
using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using SkiaSharp;
@ -24,9 +26,10 @@ internal class ProfileService : IProfileService
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
private readonly List<ProfileCategory> _profileCategories;
private readonly IProfileRepository _profileRepository;
private readonly List<IProfileMigration> _profileMigrators;
private readonly List<Exception> _renderExceptions = new();
private readonly List<Exception> _updateExceptions = new();
private DateTime _lastRenderExceptionLog;
private DateTime _lastUpdateExceptionLog;
@ -35,13 +38,15 @@ internal class ProfileService : IProfileService
IPluginManagementService pluginManagementService,
IInputService inputService,
IDeviceService deviceService,
IProfileRepository profileRepository)
IProfileRepository profileRepository,
List<IProfileMigration> profileMigrators)
{
_logger = logger;
_profileCategoryRepository = profileCategoryRepository;
_pluginManagementService = pluginManagementService;
_deviceService = deviceService;
_profileRepository = profileRepository;
_profileMigrators = profileMigrators;
_profileCategories = new List<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order));
_deviceService.LedsChanged += DeviceServiceOnLedsChanged;
@ -58,7 +63,7 @@ internal class ProfileService : IProfileService
public ProfileConfiguration? FocusProfile { get; set; }
public ProfileElement? FocusProfileElement { get; set; }
public bool UpdateFocusProfile { get; set; }
public bool ProfileRenderingDisabled { get; set; }
/// <inheritdoc />
@ -461,7 +466,12 @@ internal class ProfileService : IProfileService
await using Stream profileStream = profileEntry.Open();
using StreamReader profileReader = new(profileStream);
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(await profileReader.ReadToEndAsync(), IProfileService.ExportSettings);
JObject? profileJson = JsonConvert.DeserializeObject<JObject>(await profileReader.ReadToEndAsync(), IProfileService.ExportSettings);
// Before deserializing, apply any pending migrations
MigrateProfile(configurationEntity, profileJson);
ProfileEntity? profileEntity = profileJson?.ToObject<ProfileEntity>(JsonSerializer.Create(IProfileService.ExportSettings));
if (profileEntity == null)
throw new ArtemisCoreException("Could not import profile, failed to deserialize profile.json");
@ -514,10 +524,10 @@ internal class ProfileService : IProfileService
public async Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration)
{
ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration.Order + 1);
DeleteProfile(profileConfiguration);
SaveProfileCategory(imported.Category);
return imported;
}
@ -545,6 +555,21 @@ internal class ProfileService : IProfileService
}
}
private void MigrateProfile(ProfileConfigurationEntity configurationEntity, JObject? profileJson)
{
if (profileJson == null)
return;
foreach (IProfileMigration profileMigrator in _profileMigrators.OrderBy(m => m.Version))
{
if (profileMigrator.Version <= configurationEntity.Version)
continue;
profileMigrator.Migrate(profileJson);
configurationEntity.Version = profileMigrator.Version;
}
}
/// <summary>
/// Populates all missing LEDs on all currently active profiles
/// </summary>

View File

@ -13,16 +13,13 @@ internal class NodeTypeStore
public static NodeTypeRegistration Add(NodeData nodeData)
{
if (nodeData.Plugin == null)
throw new ArtemisCoreException("Cannot add a data binding modifier type that is not associated with a plugin");
NodeTypeRegistration typeRegistration;
lock (Registrations)
{
if (Registrations.Any(r => r.NodeData == nodeData))
throw new ArtemisCoreException($"Data binding modifier type store already contains modifier '{nodeData.Name}'");
typeRegistration = new NodeTypeRegistration(nodeData, nodeData.Plugin) {IsInStore = true};
typeRegistration = new NodeTypeRegistration(nodeData, nodeData.Provider) {IsInStore = true};
Registrations.Add(typeRegistration);
}
@ -60,24 +57,12 @@ internal class NodeTypeStore
}
}
public static Plugin? GetPlugin(INode node)
{
Type nodeType = node.GetType();
lock (Registrations)
{
return Registrations.FirstOrDefault(r => r.NodeData.Type == nodeType)?.Plugin;
}
}
public static TypeColorRegistration AddColor(Type type, SKColor color, Plugin plugin)
public static TypeColorRegistration AddColor(Type type, SKColor color, PluginFeature pluginFeature)
{
TypeColorRegistration typeColorRegistration;
lock (ColorRegistrations)
{
if (ColorRegistrations.Any(r => r.Type == type))
throw new ArtemisCoreException($"Node color store already contains a color for '{type.Name}'");
typeColorRegistration = new TypeColorRegistration(type, color, plugin) {IsInStore = true};
typeColorRegistration = new TypeColorRegistration(type, color, pluginFeature) {IsInStore = true};
ColorRegistrations.Add(typeColorRegistration);
}

View File

@ -9,12 +9,12 @@ namespace Artemis.Core;
/// </summary>
public class NodeTypeRegistration
{
internal NodeTypeRegistration(NodeData nodeData, Plugin plugin)
internal NodeTypeRegistration(NodeData nodeData, PluginFeature pluginFeature)
{
NodeData = nodeData;
Plugin = plugin;
PluginFeature = pluginFeature;
Plugin.Disabled += OnDisabled;
PluginFeature.Disabled += OnDisabled;
}
/// <summary>
@ -23,9 +23,9 @@ public class NodeTypeRegistration
public NodeData NodeData { get; }
/// <summary>
/// Gets the plugin the node is associated with
/// Gets the plugin feature the node is associated with
/// </summary>
public Plugin Plugin { get; }
public PluginFeature PluginFeature { get; }
/// <summary>
/// Gets a boolean indicating whether the registration is in the internal Core store
@ -39,12 +39,12 @@ public class NodeTypeRegistration
/// <returns><see langword="true" /> if the entity matches this registration; otherwise <see langword="false" />.</returns>
public bool MatchesEntity(NodeEntity entity)
{
return Plugin.Guid == entity.PluginId && NodeData.Type.Name == entity.Type;
return PluginFeature.Id == entity.ProviderId && NodeData.Type.Name == entity.Type;
}
private void OnDisabled(object? sender, EventArgs e)
{
Plugin.Disabled -= OnDisabled;
PluginFeature.Disabled -= OnDisabled;
if (IsInStore)
NodeTypeStore.Remove(this);
}
@ -55,13 +55,13 @@ public class NodeTypeRegistration
/// </summary>
public class TypeColorRegistration
{
internal TypeColorRegistration(Type type, SKColor color, Plugin plugin)
internal TypeColorRegistration(Type type, SKColor color, PluginFeature pluginFeature)
{
Type = type;
Color = color;
Plugin = plugin;
PluginFeature = pluginFeature;
Plugin.Disabled += OnDisabled;
PluginFeature.Disabled += OnDisabled;
}
/// <summary>
@ -80,9 +80,9 @@ public class TypeColorRegistration
public SKColor DarkenedColor => Color.Darken(0.35f);
/// <summary>
/// Gets the plugin type color is associated with
/// Gets the plugin feature this type color is associated with
/// </summary>
public Plugin Plugin { get; }
public PluginFeature PluginFeature { get; }
/// <summary>
/// Gets a boolean indicating whether the registration is in the internal Core store
@ -91,7 +91,7 @@ public class TypeColorRegistration
private void OnDisabled(object? sender, EventArgs e)
{
Plugin.Disabled -= OnDisabled;
PluginFeature.Disabled -= OnDisabled;
if (IsInStore)
NodeTypeStore.RemoveColor(this);
}

View File

@ -14,6 +14,11 @@ public interface INode : INotifyPropertyChanged, IBreakableModel
/// Gets or sets the ID of the node.
/// </summary>
Guid Id { get; set; }
/// <summary>
/// Gets or sets the node data with information about this node
/// </summary>
NodeData? NodeData { get; set; }
/// <summary>
/// Gets the name of the node

View File

@ -1,4 +1,5 @@
using System;
using Artemis.Core.Nodes;
using Artemis.Storage.Entities.Profile.Nodes;
namespace Artemis.Core;
@ -10,9 +11,9 @@ public class NodeData
{
#region Constructors
internal NodeData(Plugin plugin, Type type, string name, string description, string category, string helpUrl, Type? inputType, Type? outputType, Func<INodeScript, NodeEntity?, INode> create)
internal NodeData(NodeProvider provider, Type type, string name, string description, string category, string helpUrl, Type? inputType, Type? outputType)
{
Plugin = plugin;
Provider = provider;
Type = type;
Name = name;
Description = description;
@ -20,7 +21,6 @@ public class NodeData
HelpUrl = helpUrl;
InputType = inputType;
OutputType = outputType;
_create = create;
}
#endregion
@ -35,14 +35,31 @@ public class NodeData
/// <returns>The returning node of type <see cref="Type" /></returns>
public INode CreateNode(INodeScript script, NodeEntity? entity)
{
INode node = _create(script, entity);
INode node = (INode) Provider.Plugin.Resolve(Type);
node.NodeData = this;
if (string.IsNullOrWhiteSpace(node.Name))
node.Name = Name;
if (string.IsNullOrWhiteSpace(node.Description))
node.Description = Description;
if (string.IsNullOrWhiteSpace(node.HelpUrl))
node.HelpUrl = HelpUrl;
if (entity != null)
{
node.X = entity.X;
node.Y = entity.Y;
try
{
if (node is Node nodeImplementation)
nodeImplementation.DeserializeStorage(entity.Storage);
}
catch
{
// ignored
}
}
node.TryInitialize(script);
return node;
}
@ -91,11 +108,11 @@ public class NodeData
}
#region Properties & Fields
/// <summary>
/// Gets the plugin that provided this node data
/// Gets the node provider that provided this node data
/// </summary>
public Plugin Plugin { get; }
public NodeProvider Provider { get; }
/// <summary>
/// Gets the type of <see cref="INode" /> this data represents
@ -132,7 +149,5 @@ public class NodeData
/// </summary>
public Type? OutputType { get; }
private readonly Func<INodeScript, NodeEntity?, INode> _create;
#endregion
}

View File

@ -161,6 +161,7 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
{
foreach (INode node in _nodes)
{
// ReSharper disable once SuspiciousTypeConversion.Global - Provided by plugins
if (node is IDisposable disposable)
disposable.Dispose();
}
@ -181,6 +182,7 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
foreach (INode removeNode in removeNodes)
{
RemoveNode(removeNode);
// ReSharper disable once SuspiciousTypeConversion.Global - Provided by plugins
if (removeNode is IDisposable disposable)
disposable.Dispose();
}
@ -312,7 +314,7 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
NodeEntity nodeEntity = new()
{
Id = node.Id,
PluginId = NodeTypeStore.GetPlugin(node)?.Guid ?? Constants.CorePlugin.Guid,
ProviderId = node.NodeData?.Provider.Id ?? Constants.CorePluginFeature.Id,
Type = node.GetType().Name,
X = node.X,
Y = node.Y,

View File

@ -41,6 +41,9 @@ public abstract class Node : BreakableModel, INode
set => SetAndNotify(ref _id, value);
}
/// <inheritdoc />
public NodeData? NodeData { get; set; }
private string _name;
/// <inheritdoc />
@ -104,8 +107,6 @@ public abstract class Node : BreakableModel, INode
/// <inheritdoc />
public override string BrokenDisplayName => Name;
internal IContainer Container { get; set; } = null!;
#endregion
#region Construtors

View File

@ -25,7 +25,9 @@ public abstract class Node<TStorage, TViewModel> : Node<TStorage>, ICustomViewMo
/// <param name="nodeScript"></param>
public virtual TViewModel GetViewModel(NodeScript nodeScript)
{
return Container.Resolve<TViewModel>(args: new object[] {this, nodeScript});
if (NodeData == null)
throw new ArtemisCoreException("Nodes without node data (default nodes or exit nodes) cannot have custom view models");
return NodeData.Provider.Plugin.Container.Resolve<TViewModel>(args: new object[] {this, nodeScript});
}
/// <summary>

View File

@ -15,7 +15,7 @@ public class NodeEntity
{
Id = nodeEntity.Id;
Type = nodeEntity.Type;
PluginId = nodeEntity.PluginId;
ProviderId = nodeEntity.ProviderId;
Name = nodeEntity.Name;
Description = nodeEntity.Description;
@ -29,7 +29,7 @@ public class NodeEntity
public Guid Id { get; set; }
public string Type { get; set; } = string.Empty;
public Guid PluginId { get; set; }
public string ProviderId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;

View File

@ -26,4 +26,5 @@ public class ProfileConfigurationEntity
public Guid ProfileId { get; set; }
public bool FadeInAndOut { get; set; }
public int Version { get; set; }
}

View File

@ -1,11 +1,9 @@
using LiteDB;
using Newtonsoft.Json.Linq;
namespace Artemis.Storage.Migrations;
internal interface IProfileMigration
public interface IProfileMigration
{
int Version { get; }
void Migrate(JObject profileJson);
void Migrate(BsonDocument profileBson);
}

View File

@ -1,6 +1,6 @@
using LiteDB;
namespace Artemis.Storage.Migrations.Interfaces;
namespace Artemis.Storage.Migrations;
public interface IStorageMigration
{

View File

@ -1,4 +1,3 @@
using LiteDB;
using Newtonsoft.Json.Linq;
namespace Artemis.Storage.Migrations.Profile;
@ -21,9 +20,7 @@ internal class M0001NodeProviders : IProfileMigration
if (folders != null)
{
foreach (JToken folder in folders)
{
MigrateProfileElement(folder);
}
}
if (layers != null)
@ -38,12 +35,6 @@ internal class M0001NodeProviders : IProfileMigration
}
}
/// <inheritdoc />
public void Migrate(BsonDocument profileBson)
{
throw new System.NotImplementedException();
}
private void MigrateProfileElement(JToken profileElement)
{
JArray? layerEffects = (JArray?) profileElement["LayerEffects"]?["$values"];

View File

@ -1,9 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
namespace Artemis.Storage.Migrations.Storage;
public class M0020AvaloniaReset : IStorageMigration
{

View File

@ -3,10 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Nodes;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
namespace Artemis.Storage.Migrations.Storage;
public class M0021GradientNodes : IStorageMigration
{
@ -22,7 +21,7 @@ public class M0021GradientNodes : IStorageMigration
{
Id = Guid.NewGuid(),
Type = "ColorGradientNode",
PluginId = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"),
ProviderId = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78",
Name = "Color Gradient",
Description = "Outputs a color gradient with the given colors",
X = exitNode.X,

View File

@ -3,10 +3,9 @@ using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
using Artemis.Storage.Entities.Profile.Nodes;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
namespace Artemis.Storage.Migrations.Storage;
public class M0022TransitionNodes : IStorageMigration
{

View File

@ -1,8 +1,7 @@
using System.Collections.Generic;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
namespace Artemis.Storage.Migrations.Storage;
public class M0023LayoutProviders : IStorageMigration
{

View File

@ -0,0 +1,100 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Profile;
using LiteDB;
namespace Artemis.Storage.Migrations.Storage;
public class M0024NodeProviders : IStorageMigration
{
public int UserVersion => 24;
public void Apply(LiteRepository repository)
{
List<ProfileCategoryEntity> profileCategories = repository.Query<ProfileCategoryEntity>().ToList();
foreach (ProfileCategoryEntity profileCategory in profileCategories)
{
foreach (ProfileConfigurationEntity profileConfigurationEntity in profileCategory.ProfileConfigurations)
{
profileConfigurationEntity.Version = 1;
}
repository.Update(profileCategory);
}
ILiteCollection<BsonDocument> collection = repository.Database.GetCollection("ProfileEntity");
foreach (BsonDocument profileBson in collection.FindAll())
{
BsonArray? folders = profileBson["Folders"]?.AsArray;
BsonArray? layers = profileBson["Layers"]?.AsArray;
if (folders != null)
{
foreach (BsonValue folder in folders)
MigrateProfileElement(folder.AsDocument);
}
if (layers != null)
{
foreach (BsonValue layer in layers)
{
MigrateProfileElement(layer.AsDocument);
MigratePropertyGroup(layer.AsDocument["GeneralPropertyGroup"].AsDocument);
MigratePropertyGroup(layer.AsDocument["TransformPropertyGroup"].AsDocument);
MigratePropertyGroup(layer.AsDocument["LayerBrush"]?["PropertyGroup"].AsDocument);
}
}
collection.Update(profileBson);
}
}
private void MigrateProfileElement(BsonDocument profileElement)
{
BsonArray? layerEffects = profileElement["LayerEffects"]?.AsArray;
if (layerEffects != null)
{
foreach (BsonValue layerEffect in layerEffects)
MigratePropertyGroup(layerEffect.AsDocument["PropertyGroup"].AsDocument);
}
BsonValue? displayCondition = profileElement["DisplayCondition"];
if (displayCondition != null)
MigrateNodeScript(displayCondition.AsDocument["Script"].AsDocument);
}
private void MigratePropertyGroup(BsonDocument? propertyGroup)
{
if (propertyGroup == null || propertyGroup.Keys.Count == 0)
return;
BsonArray? properties = propertyGroup["Properties"]?.AsArray;
BsonArray? propertyGroups = propertyGroup["PropertyGroups"]?.AsArray;
if (properties != null)
{
foreach (BsonValue property in properties)
MigrateNodeScript(property.AsDocument["DataBinding"]?["NodeScript"]?.AsDocument);
}
if (propertyGroups != null)
{
foreach (BsonValue childPropertyGroup in propertyGroups)
MigratePropertyGroup(childPropertyGroup.AsDocument);
}
}
private void MigrateNodeScript(BsonDocument? nodeScript)
{
if (nodeScript == null || nodeScript.Keys.Count == 0)
return;
BsonArray? nodes = nodeScript["Nodes"]?.AsArray;
if (nodes == null)
return;
foreach (BsonValue node in nodes)
{
node.AsDocument["Type"] = node.AsDocument["Type"]?.AsString?.Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes");
node.AsDocument["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78";
}
}
}

View File

@ -29,7 +29,6 @@ internal class PluginRepository : IPluginRepository
public void SavePlugin(PluginEntity pluginEntity)
{
_repository.Upsert(pluginEntity);
_repository.Database.Checkpoint();
}
public void AddSetting(PluginSettingEntity pluginSettingEntity)

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Migrations.Interfaces;
using Artemis.Storage.Migrations;
using LiteDB;
using Serilog;

View File

@ -18,16 +18,20 @@ internal class DataModelUIService : IDataModelUIService
private readonly IContainer _container;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelDisplays;
private readonly List<DataModelVisualizationRegistration> _registeredDataModelEditors;
private readonly PluginSetting<bool> _showFullPaths;
private readonly PluginSetting<bool> _showDataModelValues;
public DataModelUIService(IDataModelService dataModelService, IContainer container)
public DataModelUIService(IDataModelService dataModelService, IContainer container, ISettingsService settingsService)
{
_dataModelService = dataModelService;
_container = container;
_registeredDataModelEditors = new List<DataModelVisualizationRegistration>();
_registeredDataModelDisplays = new List<DataModelVisualizationRegistration>();
RegisteredDataModelEditors = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelEditors);
RegisteredDataModelDisplays = new ReadOnlyCollection<DataModelVisualizationRegistration>(_registeredDataModelDisplays);
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
}
private DataModelInputViewModel InstantiateDataModelInputViewModel(DataModelVisualizationRegistration registration, DataModelPropertyAttribute? description, object? initialValue)
@ -43,7 +47,9 @@ internal class DataModelUIService : IDataModelUIService
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelEditors { get; }
public IReadOnlyCollection<DataModelVisualizationRegistration> RegisteredDataModelDisplays { get; }
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public DataModelPropertiesViewModel GetMainDataModelVisualization()
{
DataModelPropertiesViewModel viewModel = new(null, null, null);

View File

@ -104,4 +104,14 @@ public interface IDataModelUIService : IArtemisSharedUIService
/// <param name="updateCallback">A function to call whenever the input was updated (submitted or not)</param>
/// <returns>The most appropriate input view model for the provided <paramref name="propertyType" /></returns>
DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action<object?, bool> updateCallback);
/// <summary>
/// Gets a boolean indicating whether or not to show full paths when displaying data model paths.
/// </summary>
PluginSetting<bool> ShowFullPaths { get; }
/// <summary>
/// Gets a boolean indicating whether or not to show values when displaying data model paths.
/// </summary>
PluginSetting<bool> ShowDataModelValues { get; }
}

View File

@ -27,6 +27,7 @@ public class AddNode : INodeEditorCommand, IDisposable
/// <inheritdoc />
public void Dispose()
{
// ReSharper disable once SuspiciousTypeConversion.Global - Provided by plugins
if (_isRemoved && _node is IDisposable disposableNode)
disposableNode.Dispose();
}

View File

@ -29,6 +29,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable
/// <inheritdoc />
public void Dispose()
{
// ReSharper disable once SuspiciousTypeConversion.Global - Provided by plugins
if (_isRemoved && _node is IDisposable disposableNode)
disposableNode.Dispose();
}

View File

@ -87,6 +87,7 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
/// <inheritdoc />
public void Dispose()
{
// ReSharper disable once SuspiciousTypeConversion.Global - Provided by plugins
if (!_executed && _copy is IDisposable disposableNode)
disposableNode.Dispose();
}

View File

@ -10,7 +10,6 @@
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
<ProjectReference Include="..\Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj" />
<ProjectReference Include="..\Artemis.WebClient.Workshop\Artemis.WebClient.Workshop.csproj" />
</ItemGroup>

View File

@ -10,10 +10,8 @@ using Artemis.UI.Screens.Root;
using Artemis.UI.Shared.DataModelPicker;
using Artemis.UI.Shared.DryIoc;
using Artemis.UI.Shared.Services;
using Artemis.VisualScripting.DryIoc;
using Artemis.WebClient.Updating.DryIoc;
using Artemis.WebClient.Workshop.DryIoc;
using Artemis.WebClient.Workshop.Services;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@ -49,7 +47,6 @@ public static class ArtemisBootstrapper
_container.RegisterSharedUI();
_container.RegisterUpdatingClient();
_container.RegisterWorkshopClient();
_container.RegisterNoStringEvaluating();
configureServices?.Invoke(_container);
_container.UseDryIocDependencyResolver();

View File

@ -18,11 +18,13 @@ using DynamicData;
using DynamicData.Binding;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using Serilog;
namespace Artemis.UI.Screens.VisualScripting;
public partial class NodeViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly INodeEditorService _nodeEditorService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _hasInputPins;
@ -39,8 +41,9 @@ public partial class NodeViewModel : ActivatableViewModelBase
[Notify] private bool _displayCustomViewModelBelow;
[Notify] private VerticalAlignment _customViewModelVerticalAlignment;
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService)
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, ILogger logger, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService)
{
_logger = logger;
_nodeEditorService = nodeEditorService;
_windowService = windowService;
NodeScriptViewModel = nodeScriptViewModel;
@ -137,25 +140,7 @@ public partial class NodeViewModel : ActivatableViewModelBase
});
// Set up the custom node VM if needed
if (Node is ICustomViewModelNode customViewModelNode)
{
CustomNodeViewModel = customViewModelNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.AbovePins)
DisplayCustomViewModelAbove = true;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BelowPins)
DisplayCustomViewModelBelow = true;
else
{
DisplayCustomViewModelBetween = true;
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Top;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Center;
else
CustomViewModelVerticalAlignment = VerticalAlignment.Bottom;
}
}
SetupCustomNodeViewModel();
});
}
@ -209,4 +194,35 @@ public partial class NodeViewModel : ActivatableViewModelBase
if (Node.BrokenState != null && Node.BrokenStateException != null)
_windowService.ShowExceptionDialog(Node.BrokenState, Node.BrokenStateException);
}
private void SetupCustomNodeViewModel()
{
if (Node is not ICustomViewModelNode customViewModelNode)
return;
try
{
CustomNodeViewModel = customViewModelNode.GetCustomViewModel(NodeScriptViewModel.NodeScript);
}
catch (Exception e)
{
_logger.Error(e, "Failed to instantiate custom node view model");
}
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.AbovePins)
DisplayCustomViewModelAbove = true;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BelowPins)
DisplayCustomViewModelBelow = true;
else
{
DisplayCustomViewModelBetween = true;
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Top;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Center;
else
CustomViewModelVerticalAlignment = VerticalAlignment.Bottom;
}
}
}

View File

@ -6,5 +6,4 @@ public interface IRegistrationService : IArtemisUIService
void RegisterBuiltInDataModelInputs();
void RegisterBuiltInPropertyEditors();
void RegisterControllers();
void RegisterBuiltInNodeTypes();
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Controllers;
@ -13,10 +10,8 @@ using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.PropertyInput;
using Artemis.VisualScripting.Nodes.Mathematics;
using Avalonia;
using DryIoc;
using SkiaSharp;
namespace Artemis.UI.Services;
@ -26,7 +21,6 @@ public class RegistrationService : IRegistrationService
private readonly IInputService _inputService;
private readonly IContainer _container;
private readonly IRouter _router;
private readonly INodeService _nodeService;
private readonly IPropertyInputService _propertyInputService;
private readonly IWebServerService _webServerService;
private bool _registeredBuiltInPropertyEditors;
@ -36,7 +30,6 @@ public class RegistrationService : IRegistrationService
IInputService inputService,
IPropertyInputService propertyInputService,
IProfileEditorService profileEditorService,
INodeService nodeService,
IDataModelUIService dataModelUIService,
IWebServerService webServerService,
IDeviceLayoutService deviceLayoutService // here to make sure it is instantiated
@ -46,13 +39,11 @@ public class RegistrationService : IRegistrationService
_router = router;
_inputService = inputService;
_propertyInputService = propertyInputService;
_nodeService = nodeService;
_dataModelUIService = dataModelUIService;
_webServerService = webServerService;
CreateCursorResources();
RegisterRoutes();
RegisterBuiltInNodeTypes();
RegisterControllers();
}
@ -105,22 +96,4 @@ public class RegistrationService : IRegistrationService
{
_webServerService.AddController<RemoteController>(Constants.CorePlugin.Features.First().Instance!);
}
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(Numeric), 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));
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(ColorGradient), new SKColor(0xFF00B2A9));
foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface))
{
if (nodeType.GetCustomAttribute(typeof(NodeAttribute)) != null)
_nodeService.RegisterNodeType(Constants.CorePlugin, nodeType);
}
}
}

View File

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NoStringEvaluating" Version="2.5.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -1,25 +0,0 @@
using System.Globalization;
using Artemis.Core;
using Avalonia.Data.Converters;
using Newtonsoft.Json;
namespace Artemis.VisualScripting.Converters;
/// <summary>
/// Converts input into <see cref="Numeric" />.
/// </summary>
public class JsonConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return JsonConvert.SerializeObject(value, Formatting.Indented);
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
string? json = value?.ToString();
return json == null ? null : JsonConvert.DeserializeObject(json, targetType);
}
}

View File

@ -1,27 +0,0 @@
using System.Globalization;
using Artemis.Core;
using Avalonia.Data.Converters;
namespace Artemis.VisualScripting.Converters;
/// <summary>
/// Converts input into <see cref="Numeric" />.
/// </summary>
public class NumericConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not Numeric numeric)
return value;
object result = Numeric.IsTypeCompatible(targetType) ? numeric.ToType(targetType, NumberFormatInfo.InvariantInfo) : value;
return result;
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return new Numeric(value);
}
}

View File

@ -1,41 +0,0 @@
using DryIoc;
using Microsoft.Extensions.ObjectPool;
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.DryIoc;
/// <summary>
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
/// </summary>
public static class ContainerExtensions
{
/// <summary>
/// Registers NoStringEvaluating services into the container.
/// </summary>
/// <param name="container">The builder building the current container</param>
public static void RegisterNoStringEvaluating(this IContainer container)
{
// Pooling
container.RegisterInstance(ObjectPool.Create<Stack<InternalEvaluatorValue>>());
container.RegisterInstance(ObjectPool.Create<List<InternalEvaluatorValue>>());
container.RegisterInstance(ObjectPool.Create<ValueKeeperContainer>());
// Parser
container.Register<IFormulaCache, FormulaCache>(Reuse.Singleton);
container.Register<IFunctionReader, FunctionReader>(Reuse.Singleton);
container.Register<IFormulaParser, FormulaParser>(Reuse.Singleton);
// Checker
container.Register<IFormulaChecker, FormulaChecker>(Reuse.Singleton);
// Evaluator
container.Register<INoStringEvaluator, NoStringEvaluator>(Reuse.Singleton);
container.Register<INoStringEvaluatorNullable, NoStringEvaluatorNullable>(Reuse.Singleton);
}
}

View File

@ -1,59 +0,0 @@
using Artemis.Core;
using Artemis.Core.Events;
namespace Artemis.VisualScripting.Nodes.Branching;
[Node("Branch", "Forwards one of two values depending on an input boolean", "Branching", InputType = typeof(object), OutputType = typeof(object))]
public class BooleanBranchNode : Node
{
public BooleanBranchNode()
{
BooleanInput = CreateInputPin<bool>();
TrueInput = CreateInputPin(typeof(object), "True");
FalseInput = CreateInputPin(typeof(object), "False");
Output = CreateOutputPin(typeof(object));
TrueInput.PinConnected += InputPinConnected;
FalseInput.PinConnected += InputPinConnected;
TrueInput.PinDisconnected += InputPinDisconnected;
FalseInput.PinDisconnected += InputPinDisconnected;
}
public InputPin<bool> BooleanInput { get; set; }
public InputPin TrueInput { get; set; }
public InputPin FalseInput { get; set; }
public OutputPin Output { get; set; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
Output.Value = BooleanInput.Value ? TrueInput.Value : FalseInput.Value;
}
#endregion
private void InputPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{
if (TrueInput.ConnectedTo.Any())
ChangeType(TrueInput.ConnectedTo.First().Type);
else if (FalseInput.ConnectedTo.Any())
ChangeType(FalseInput.ConnectedTo.First().Type);
}
private void InputPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
if (!TrueInput.ConnectedTo.Any() && !FalseInput.ConnectedTo.Any())
ChangeType(typeof(object));
}
private void ChangeType(Type type)
{
TrueInput.ChangeType(type);
FalseInput.ChangeType(type);
Output.ChangeType(type);
}
}

View File

@ -1,100 +0,0 @@
using Artemis.Core;
using Artemis.Core.Events;
using Humanizer;
namespace Artemis.VisualScripting.Nodes.Branching;
[Node("Switch (Enum)", "Outputs the input that corresponds to the switch value", "Operators", InputType = typeof(Enum), OutputType = typeof(object))]
public class EnumSwitchNode : Node
{
private readonly Dictionary<Enum, InputPin> _inputPins;
public EnumSwitchNode()
{
_inputPins = new Dictionary<Enum, InputPin>();
Output = CreateOutputPin(typeof(object), "Result");
SwitchValue = CreateInputPin<Enum>("Switch");
SwitchValue.PinConnected += OnSwitchPinConnected;
SwitchValue.PinDisconnected += OnSwitchPinDisconnected;
}
public OutputPin Output { get; }
public InputPin<Enum> SwitchValue { get; }
public override void Evaluate()
{
if (SwitchValue.Value is null)
{
Output.Value = null;
return;
}
if (!_inputPins.TryGetValue(SwitchValue.Value, out InputPin? pin))
{
Output.Value = null;
return;
}
if (pin.ConnectedTo.Count == 0)
{
Output.Value = null;
return;
}
Output.Value = pin.Value;
}
private void OnInputPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
// if this is the last pin to disconnect, reset the type.
if (_inputPins.Values.All(i => i.ConnectedTo.Count == 0))
ChangeType(typeof(object));
}
private void OnInputPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{
// change the type of our inputs and output
// depending on the first node the user connects to
ChangeType(e.Value.Type);
}
private void OnSwitchPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{
if (SwitchValue.ConnectedTo.Count == 0)
return;
Type enumType = SwitchValue.ConnectedTo[0].Type;
if (!enumType.IsEnum)
return;
foreach (Enum enumValue in Enum.GetValues(enumType).Cast<Enum>())
{
InputPin pin = CreateOrAddInputPin(typeof(object), enumValue.Humanize(LetterCasing.Sentence));
pin.PinConnected += OnInputPinConnected;
pin.PinDisconnected += OnInputPinDisconnected;
_inputPins[enumValue] = pin;
}
}
private void OnSwitchPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
foreach (InputPin input in _inputPins.Values)
{
input.PinConnected -= OnInputPinConnected;
input.PinDisconnected -= OnInputPinDisconnected;
RemovePin(input);
}
_inputPins.Clear();
ChangeType(typeof(object));
}
private void ChangeType(Type type)
{
foreach (InputPin input in _inputPins.Values)
input.ChangeType(type);
Output.ChangeType(type);
}
}

View File

@ -1,26 +0,0 @@
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()
{
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

@ -1,102 +0,0 @@
using Artemis.Core;
using Artemis.Core.Events;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color Gradient (Advanced)", "Outputs a Color Gradient from colors and positions", "Color", OutputType = typeof(ColorGradient))]
public class ColorGradientFromPinsNode : Node
{
public OutputPin<ColorGradient> Gradient { get; set; }
public InputPinCollection<SKColor> Colors { get; set; }
public InputPinCollection<Numeric> Positions { get; set; }
public ColorGradientFromPinsNode()
{
Colors = CreateInputPinCollection<SKColor>("Colors", 0);
Positions = CreateInputPinCollection<Numeric>("Positions", 0);
Gradient = CreateOutputPin<ColorGradient>("Gradient");
Colors.PinAdded += OnPinAdded;
Colors.PinRemoved += OnPinRemoved;
Positions.PinAdded += OnPinAdded;
Positions.PinRemoved += OnPinRemoved;
}
private void OnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
{
int colorsCount = Colors.Count();
int positionsCount = Positions.Count();
if (colorsCount == positionsCount)
return;
while (colorsCount > positionsCount)
{
IPin pinToRemove = Colors.Last();
Colors.Remove(pinToRemove);
--colorsCount;
}
while (positionsCount > colorsCount)
{
IPin pinToRemove = Positions.Last();
Positions.Remove(pinToRemove);
--positionsCount;
}
RenamePins();
}
private void OnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
{
int colorsCount = Colors.Count();
int positionsCount = Positions.Count();
if (colorsCount == positionsCount)
return;
while (colorsCount < positionsCount)
{
Colors.Add(Colors.CreatePin());
++colorsCount;
}
while (positionsCount < colorsCount)
{
Positions.Add(Positions.CreatePin());
++positionsCount;
}
RenamePins();
}
private void RenamePins()
{
int colors = 0;
foreach (IPin item in Colors)
{
item.Name = $"Color #{++colors}";
}
int positions = 0;
foreach (IPin item in Positions)
{
item.Name = $"Position #{++positions}";
}
}
public override void Evaluate()
{
List<ColorGradientStop> stops = new List<ColorGradientStop>();
InputPin<SKColor>[] colors = Colors.Pins.ToArray();
InputPin<Numeric>[] positions = Positions.Pins.ToArray();
for (int i = 0; i < colors.Length; i++)
{
stops.Add(new ColorGradientStop(colors[i].Value, positions[i].Value));
}
Gradient.Value = new ColorGradient(stops);
}
}

View File

@ -1,108 +0,0 @@
using System.Collections.Specialized;
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Color.Screens;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color Gradient (Simple)", "Outputs a color gradient with the given colors", "Color", OutputType = typeof(ColorGradient))]
public class ColorGradientNode : Node<ColorGradient, ColorGradientNodeCustomViewModel>
{
private readonly List<InputPin> _inputPins;
public ColorGradientNode()
{
_inputPins = new List<InputPin>();
Gradient = ColorGradient.GetUnicornBarf();
Output = CreateOutputPin<ColorGradient>();
ViewModelPosition = CustomNodeViewModelPosition.AbovePins;
}
public ColorGradient Gradient { get; private set; }
public OutputPin<ColorGradient> Output { get; }
public override void Initialize(INodeScript script)
{
UpdateGradient();
ComputeInputPins();
// Not expecting storage to get modified, but lets just make sure
StorageModified += OnStorageModified;
}
public override void Evaluate()
{
ColorGradientStop[] stops = Gradient.ToArray();
if (_inputPins.Count != stops.Length)
return;
for (int i = 0; i < _inputPins.Count; i++)
{
// if nothing is connected, leave the stop alone.
if (_inputPins[i].ConnectedTo.Count == 0)
continue;
// if the pin has a connection, update the stop.
if (_inputPins[i].PinValue is SKColor color)
stops[i].Color = color;
}
Output.Value = Gradient;
}
private void DisconnectAllInputPins()
{
foreach (InputPin item in _inputPins)
item.DisconnectAll();
}
private void UpdateGradient()
{
Gradient.CollectionChanged -= OnGradientCollectionChanged;
if (Storage != null)
Gradient = Storage;
else
Storage = Gradient;
Gradient.CollectionChanged += OnGradientCollectionChanged;
}
private void ComputeInputPins()
{
int newAmount = Gradient.Count;
if (newAmount == _inputPins.Count)
return;
while (newAmount > _inputPins.Count)
_inputPins.Add(CreateOrAddInputPin(typeof(SKColor), string.Empty));
while (newAmount < _inputPins.Count)
{
InputPin pin = _inputPins.Last();
RemovePin(pin);
_inputPins.Remove(pin);
}
int index = 0;
foreach (InputPin item in _inputPins)
item.Name = $"Color #{++index}";
}
private void OnStorageModified(object? sender, EventArgs e)
{
UpdateGradient();
ComputeInputPins();
}
private void OnGradientCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// if the user reorders the gradient, let it slide and do nothing.
// of course, the user might want to change the input pins since they will no longer line up.
if (e.Action == NotifyCollectionChangedAction.Move)
return;
// DisconnectAllInputPins();
ComputeInputPins();
}
}

View File

@ -1,26 +0,0 @@
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()
{
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

@ -1,26 +0,0 @@
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()
{
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

@ -1,50 +0,0 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color
{
[Node("Gradient Builder", "Generates a gradient based on some values", "Color", OutputType = typeof(ColorGradient), HelpUrl = "https://krazydad.com/tutorials/makecolors.php")]
public class GradientBuilderNode : Node
{
public OutputPin<ColorGradient> Output { get; }
public InputPin<Numeric> Frequency1 { get; }
public InputPin<Numeric> Frequency2 { get; }
public InputPin<Numeric> Frequency3 { get; }
public InputPin<Numeric> Phase1 { get; }
public InputPin<Numeric> Phase2 { get; }
public InputPin<Numeric> Phase3 { get; }
public InputPin<Numeric> Center { get; }
public InputPin<Numeric> Width { get; }
public InputPin<Numeric> Length { get; }
public GradientBuilderNode()
{
Output = CreateOutputPin<ColorGradient>();
Frequency1 = CreateInputPin<Numeric>("Frequency 1");
Frequency2 = CreateInputPin<Numeric>("Frequency 2");
Frequency3 = CreateInputPin<Numeric>("Frequency 3");
Phase1 = CreateInputPin<Numeric>("Phase 1");
Phase2 = CreateInputPin<Numeric>("Phase 2");
Phase3 = CreateInputPin<Numeric>("Phase 3");
Center = CreateInputPin<Numeric>("Center");
Width = CreateInputPin<Numeric>("Width");
Length = CreateInputPin<Numeric>("Length");
}
public override void Evaluate()
{
ColorGradient gradient = new ColorGradient();
for (int i = 0; i < Length.Value; i++)
{
Numeric r = Math.Sin(Frequency1.Value * i + Phase1.Value) * Width.Value + Center.Value;
Numeric g = Math.Sin(Frequency2.Value * i + Phase2.Value) * Width.Value + Center.Value;
Numeric b = Math.Sin(Frequency3.Value * i + Phase3.Value) * Width.Value + Center.Value;
gradient.Add(new ColorGradientStop(new SKColor((byte)r, (byte)g, (byte)b), i / Length.Value));
}
Output.Value = gradient;
}
}
}

View File

@ -1,31 +0,0 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("HSL Color", "Creates a color from hue, saturation and lightness numbers", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
public class HslSKColorNode : Node
{
public HslSKColorNode()
{
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

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

View File

@ -1,27 +0,0 @@
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()
{
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

@ -1,57 +0,0 @@
using Artemis.Core;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Lerp (Color)", "Interpolates linear between the two colors A and B", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))]
public class LerpSKColorNode : Node
{
#region Properties & Fields
public InputPin<SKColor> A { get; }
public InputPin<SKColor> B { get; }
public InputPin<Numeric> T { get; }
public OutputPin<SKColor> Result { get; }
#endregion
#region Constructors
public LerpSKColorNode()
{
Name = "Lerp";
A = CreateInputPin<SKColor>("A");
B = CreateInputPin<SKColor>("B");
T = CreateInputPin<Numeric>("T");
Result = CreateOutputPin<SKColor>();
}
#endregion
#region Methods
/// <inheritdoc />
public override void Evaluate()
{
SKColor a = A.Value;
SKColor b = B.Value;
float t = ((float)T.Value).Clamp(0f, 1f);
float aAlpha = a.Alpha.GetPercentageFromByteValue();
float aRed = a.Red.GetPercentageFromByteValue();
float aGreen = a.Green.GetPercentageFromByteValue();
float aBlue = a.Blue.GetPercentageFromByteValue();
float alpha = ((b.Alpha.GetPercentageFromByteValue() - aAlpha) * t) + aAlpha;
float red = ((b.Red.GetPercentageFromByteValue() - aRed) * t) + aRed;
float green = ((b.Green.GetPercentageFromByteValue() - aGreen) * t) + aGreen;
float blue = ((b.Blue.GetPercentageFromByteValue() - aBlue) * t) + aBlue;
Result.Value = new SKColor(red.GetByteValueFromPercentage(), green.GetByteValueFromPercentage(), blue.GetByteValueFromPercentage(), alpha.GetByteValueFromPercentage());
}
#endregion
}

View File

@ -1,42 +0,0 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Color.Screens;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color Ramp", "Maps values to colors with the use of a gradient.", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
public class RampSKColorNode : Node<ColorGradient, RampSKColorNodeCustomViewModel>
{
#region Constructors
public RampSKColorNode()
{
Input = CreateInputPin<Numeric>();
Output = CreateOutputPin<SKColor>();
Storage = ColorGradient.GetUnicornBarf();
}
#endregion
#region Methods
public override void Evaluate()
{
// Wrap the input between 0 and 1
// 1 % 1 = 0, 2 % 1 = 0 etc. but we want that to be 1 but 0 should stay 0, call me stupid but this works and makes sense
float value = Input.Value % 1;
if (value == 0 && Input.Value != 0)
value = 1;
Output.Value = Storage?.GetColor(value) ?? SKColor.Empty;
}
#endregion
#region Properties & Fields
public InputPin<Numeric> Input { get; }
public OutputPin<SKColor> Output { get; }
#endregion
}

View File

@ -1,37 +0,0 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("RGB Color", "Creates a color from red, green and blue values", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
public class RgbSKColorNode : Node
{
#region Properties & Fields
public InputPin<Numeric> R { get; set; }
public InputPin<Numeric> G { get; set; }
public InputPin<Numeric> B { get; set; }
public OutputPin<SKColor> Output { get; }
#endregion
#region Constructors
public RgbSKColorNode()
{
R = CreateInputPin<Numeric>("R");
G = CreateInputPin<Numeric>("G");
B = CreateInputPin<Numeric>("B");
Output = CreateOutputPin<SKColor>();
}
#endregion
#region Methods
/// <inheritdoc />
public override void Evaluate() => Output.Value = new SKColor(R.Value, G.Value, B.Value);
#endregion
}

View File

@ -1,26 +0,0 @@
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()
{
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

@ -1,26 +0,0 @@
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()
{
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

@ -1,15 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Color.Screens"
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Color.Screens.ColorGradientNodeCustomView"
x:DataType="screens:ColorGradientNodeCustomViewModel">
<gradientPicker:GradientPickerButton Classes="condensed"
ColorGradient="{CompiledBinding Gradient}"
VerticalAlignment="Top"
FlyoutOpened="GradientPickerButton_OnFlyoutOpened"
FlyoutClosed="GradientPickerButton_OnFlyoutClosed" />
</UserControl>

View File

@ -1,23 +0,0 @@
using Artemis.UI.Shared.Controls.GradientPicker;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public partial class ColorGradientNodeCustomView : ReactiveUserControl<ColorGradientNodeCustomViewModel>
{
public ColorGradientNodeCustomView()
{
InitializeComponent();
}
private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args)
{
}
private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args)
{
ViewModel?.StoreGradient();
}
}

View File

@ -1,28 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public class ColorGradientNodeCustomViewModel : CustomNodeViewModel
{
private readonly ColorGradientNode _node;
private readonly INodeEditorService _nodeEditorService;
/// <inheritdoc />
public ColorGradientNodeCustomViewModel(ColorGradientNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
Gradient = _node.Gradient;
}
public ColorGradient Gradient { get; }
public void StoreGradient()
{
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<ColorGradient>(_node, Gradient));
}
}

View File

@ -1,16 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Color.Screens"
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Color.Screens.RampSKColorNodeCustomView"
x:DataType="screens:RampSKColorNodeCustomViewModel">
<gradientPicker:GradientPickerButton Width="110"
Classes="condensed"
ColorGradient="{CompiledBinding Gradient}"
VerticalAlignment="Center"
FlyoutOpened="GradientPickerButton_OnFlyoutOpened"
FlyoutClosed="GradientPickerButton_OnFlyoutClosed"/>
</UserControl>

View File

@ -1,23 +0,0 @@
using Artemis.UI.Shared.Controls.GradientPicker;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public partial class RampSKColorNodeCustomView : ReactiveUserControl<RampSKColorNodeCustomViewModel>
{
public RampSKColorNodeCustomView()
{
InitializeComponent();
}
private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args)
{
}
private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args)
{
ViewModel?.StoreGradient();
}
}

View File

@ -1,28 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public class RampSKColorNodeCustomViewModel : CustomNodeViewModel
{
private readonly RampSKColorNode _node;
private readonly INodeEditorService _nodeEditorService;
/// <inheritdoc />
public RampSKColorNodeCustomViewModel(RampSKColorNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
Gradient = _node.Storage ?? new ColorGradient();
}
public ColorGradient Gradient { get; }
public void StoreGradient()
{
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<ColorGradient>(_node, Gradient));
}
}

View File

@ -1,36 +0,0 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color to HSL", "Outputs H, S and L values from a color", "Color", InputType = typeof(SKColor), OutputType = typeof(Numeric))]
public class SkColorHsl : Node
{
public SkColorHsl()
{
Input = CreateInputPin<SKColor>();
H = CreateOutputPin<Numeric>("H");
S = CreateOutputPin<Numeric>("S");
L = CreateOutputPin<Numeric>("L");
}
public InputPin<SKColor> Input { get; }
public OutputPin<Numeric> H { get; }
public OutputPin<Numeric> S { get; }
public OutputPin<Numeric> L { get; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
Input.Value.ToHsl(out float h, out float s, out float l);
H.Value = h;
S.Value = s;
L.Value = l;
}
#endregion
}

View File

@ -1,36 +0,0 @@
using Artemis.Core;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color to HSV", "Outputs H, S and L values from a color", "Color", InputType = typeof(SKColor), OutputType = typeof(Numeric))]
public class SkColorHsv : Node
{
public SkColorHsv()
{
Input = CreateInputPin<SKColor>();
H = CreateOutputPin<Numeric>("H");
S = CreateOutputPin<Numeric>("S");
V = CreateOutputPin<Numeric>("V");
}
public InputPin<SKColor> Input { get; }
public OutputPin<Numeric> H { get; }
public OutputPin<Numeric> S { get; }
public OutputPin<Numeric> V { get; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
Input.Value.ToHsv(out float h, out float s, out float v);
H.Value = h;
S.Value = s;
V.Value = v;
}
#endregion
}

View File

@ -1,57 +0,0 @@
using Artemis.Core;
using Artemis.Core.ColorScience;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color
{
[Node("Sorted Gradient", "Generates a sorted gradient from the given colors", "Color", InputType = typeof(SKColor), OutputType = typeof(ColorGradient))]
public class SortedGradientNode : Node
{
private int lastComputedColorGroup;
public InputPinCollection<SKColor> Inputs { get; }
public OutputPin<ColorGradient> Output { get; }
public SortedGradientNode()
{
Inputs = CreateInputPinCollection<SKColor>();
Output = CreateOutputPin<ColorGradient>();
lastComputedColorGroup = 0;
}
public override void Evaluate()
{
int newHash = GetInputColorHash();
if (newHash == lastComputedColorGroup)
return;
SKColor[] colors = Inputs.Values.ToArray();
if (colors.Length == 0)
{
Output.Value = null;
return;
}
ColorSorter.Sort(colors, SKColors.Black);
ColorGradient gradient = new();
for (int i = 0; i < colors.Length; i++)
{
gradient.Add(new ColorGradientStop(colors[i], (float)i / (colors.Length - 1)));
}
Output.Value = gradient;
lastComputedColorGroup = newHash;
}
private int GetInputColorHash()
{
int hash = 0;
foreach (SKColor color in Inputs.Values)
hash = HashCode.Combine(hash, color.GetHashCode());
return hash;
}
}
}

View File

@ -1,45 +0,0 @@
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()
{
Name = "Sum";
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

@ -1,48 +0,0 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes.Conversion;
[Node("To Numeric", "Converts the input to a numeric.", "Conversion", InputType = typeof(object), OutputType = typeof(Numeric))]
public class ConvertToNumericNode : Node
{
#region Constructors
public ConvertToNumericNode()
{
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),
bool input => new Numeric(input ? 1 : 0),
_ => TryParse(Input.Value)
};
}
private Numeric TryParse(object? input)
{
Numeric.TryParse(input?.ToString(), out Numeric value);
return value;
}
#endregion
}

View File

@ -1,34 +0,0 @@
using Artemis.Core;
namespace Artemis.VisualScripting.Nodes.Conversion;
[Node("To Text", "Converts the input to text.", "Conversion", InputType = typeof(object), OutputType = typeof(string))]
public class ConvertToStringNode : Node
{
#region Constructors
public ConvertToStringNode()
{
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
}

View File

@ -1,171 +0,0 @@
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.Screens;
namespace Artemis.VisualScripting.Nodes.DataModel;
[Node("Data Model-Event Value Cycle", "Cycles through provided values each time the select event fires.", "Data Model", OutputType = typeof(object))]
public class DataModelEventCycleNode : Node<DataModelPathEntity, DataModelEventCycleNodeCustomViewModel>, IDisposable
{
private int _currentIndex;
private Type _currentType;
private DataModelPath? _dataModelPath;
private IDataModelEvent? _subscribedEvent;
private object? _lastPathValue;
private bool _updating;
public DataModelEventCycleNode()
{
_currentType = typeof(object);
CycleValues = CreateInputPinCollection(typeof(object), "", 0);
Output = CreateOutputPin(typeof(object));
CycleValues.PinAdded += OnCycleValuesOnPinAdded;
CycleValues.PinRemoved += OnCycleValuesOnPinRemoved;
CycleValues.Add(CycleValues.CreatePin());
// Monitor storage for changes
StorageModified += (_, _) => UpdateDataModelPath();
}
public INodeScript? Script { get; set; }
public InputPinCollection CycleValues { get; }
public OutputPin Output { get; }
public override void Initialize(INodeScript script)
{
Script = script;
if (Storage != null)
UpdateDataModelPath();
}
public override void Evaluate()
{
object? pathValue = _dataModelPath?.GetValue();
if (pathValue is not IDataModelEvent && EvaluateValue(pathValue))
Cycle();
object? outputValue = CycleValues.ElementAt(_currentIndex).PinValue;
if (Output.Type.IsInstanceOfType(outputValue))
Output.Value = outputValue;
else if (Output.Type.IsValueType)
Output.Value = Output.Type.GetDefault()!;
}
private bool EvaluateValue(object? pathValue)
{
if (Equals(pathValue, _lastPathValue))
return false;
_lastPathValue = pathValue;
return true;
}
private void Cycle()
{
_currentIndex++;
if (_currentIndex >= CycleValues.Count())
_currentIndex = 0;
}
private void UpdateDataModelPath()
{
DataModelPath? old = _dataModelPath;
if (_subscribedEvent != null)
{
_subscribedEvent.EventTriggered -= OnEventTriggered;
_subscribedEvent = null;
}
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
if (_dataModelPath?.GetValue() is IDataModelEvent newEvent)
{
_subscribedEvent = newEvent;
_subscribedEvent.EventTriggered += OnEventTriggered;
}
old?.Dispose();
}
private void ChangeCurrentType(Type type)
{
CycleValues.ChangeType(type);
Output.ChangeType(type);
_currentType = type;
}
private void OnEventTriggered(object? sender, EventArgs e)
{
Cycle();
}
private void OnCycleValuesOnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected += OnPinConnected;
e.Value.PinDisconnected += OnPinDisconnected;
}
private void OnCycleValuesOnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
{
e.Value.PinConnected -= OnPinConnected;
e.Value.PinDisconnected -= OnPinDisconnected;
}
private void OnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
{
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 OnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{
if (_updating)
return;
try
{
_updating = true;
// No need to change anything if the types haven't changed
if (_currentType != e.Value.Type)
ChangeCurrentType(e.Value.Type);
}
finally
{
_updating = false;
}
}
/// <inheritdoc />
public void Dispose()
{
if (_subscribedEvent != null)
{
_subscribedEvent.EventTriggered -= OnEventTriggered;
_subscribedEvent = null;
}
_dataModelPath?.Dispose();
}
}

View File

@ -1,164 +0,0 @@
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.Screens;
namespace Artemis.VisualScripting.Nodes.DataModel;
[Node("Data Model-Event", "Outputs the latest values of a data model event.", "Data Model", OutputType = typeof(object))]
public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
{
private readonly ObjectOutputPins _objectOutputPins;
private IDataModelEvent? _dataModelEvent;
private DataModelPath? _dataModelPath;
private DateTime _lastTrigger;
private object? _lastValue;
private OutputPin? _newValuePin;
private OutputPin? _oldValuePin;
private int _valueChangeCount;
public DataModelEventNode()
{
_objectOutputPins = new ObjectOutputPins(this);
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
// Monitor storage for changes
StorageModified += (_, _) => UpdateDataModelPath();
}
public INodeScript? Script { get; set; }
public OutputPin<Numeric> TimeSinceLastTrigger { get; }
public OutputPin<Numeric> TriggerCount { get; }
public override void Initialize(INodeScript script)
{
Script = script;
if (Storage == null)
return;
UpdateDataModelPath();
}
public override void Evaluate()
{
object? pathValue = _dataModelPath?.GetValue();
// If the path is a data model event, evaluate the event
if (pathValue is IDataModelEvent dataModelEvent)
{
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
TriggerCount.Value = dataModelEvent.TriggerCount;
_objectOutputPins.SetCurrentValue(dataModelEvent.LastEventArgumentsUntyped);
}
// If the path is a regular value, evaluate the current value
else if (_oldValuePin != null && _newValuePin != null)
{
if (_newValuePin.IsNumeric)
pathValue = new Numeric(pathValue);
if (Equals(_lastValue, pathValue))
{
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
return;
}
_valueChangeCount++;
_lastTrigger = DateTime.Now;
_oldValuePin.Value = _lastValue;
_newValuePin.Value = pathValue;
_lastValue = pathValue;
TimeSinceLastTrigger.Value = 0;
TriggerCount.Value = _valueChangeCount;
}
}
private void UpdateDataModelPath()
{
DataModelPath? old = _dataModelPath;
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
if (_dataModelPath != null)
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
if (old != null)
{
old.PathValidated -= DataModelPathOnPathValidated;
old.Dispose();
}
UpdateOutputPins();
}
private void UpdateOutputPins()
{
object? pathValue = _dataModelPath?.GetValue();
if (pathValue is IDataModelEvent dataModelEvent)
CreateEventPins(dataModelEvent);
else
CreateValuePins();
}
private void CreateEventPins(IDataModelEvent dataModelEvent)
{
if (_dataModelEvent == dataModelEvent)
return;
ClearPins();
_dataModelEvent = dataModelEvent;
_objectOutputPins.ChangeType(dataModelEvent.ArgumentsType);
}
private void CreateValuePins()
{
ClearPins();
Type? propertyType = _dataModelPath?.GetPropertyType();
if (propertyType == null)
return;
if (Numeric.IsTypeCompatible(propertyType))
propertyType = typeof(Numeric);
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
_lastValue = null;
_valueChangeCount = 0;
}
private void ClearPins()
{
// Clear the output pins by changing the type to null
_objectOutputPins.ChangeType(null);
if (_oldValuePin != null)
{
RemovePin(_oldValuePin);
_oldValuePin = null;
}
if (_newValuePin != null)
{
RemovePin(_newValuePin);
_newValuePin = null;
}
}
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
{
// Update the output pin now that the type is known and attempt to restore the connection that was likely missing
UpdateOutputPins();
Script?.LoadConnections();
}
/// <inheritdoc />
public void Dispose()
{
_dataModelPath?.Dispose();
}
}

View File

@ -1,88 +0,0 @@
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.VisualScripting.Nodes.DataModel.Screens;
namespace Artemis.VisualScripting.Nodes.DataModel;
[Node("Data Model-Value", "Outputs a selectable data model value.", "Data Model", OutputType = typeof(object))]
public class DataModelNode : Node<DataModelPathEntity, DataModelNodeCustomViewModel>, IDisposable
{
private DataModelPath? _dataModelPath;
public DataModelNode()
{
Output = CreateOutputPin(typeof(object));
StorageModified += (_, _) => UpdateDataModelPath();
}
public INodeScript? Script { get; private set; }
public OutputPin Output { get; }
public override void Initialize(INodeScript script)
{
Script = script;
if (Storage == null)
return;
UpdateDataModelPath();
}
public override void Evaluate()
{
if (_dataModelPath == null || !_dataModelPath.IsValid)
return;
object? pathValue = _dataModelPath.GetValue();
if (pathValue == null)
{
if (!Output.Type.IsValueType)
Output.Value = null;
}
else
{
Output.Value = Output.IsNumeric ? new Numeric(pathValue) : pathValue;
}
}
public void UpdateOutputPin()
{
Type? type = _dataModelPath?.GetPropertyType();
if (type == null)
type = typeof(object);
else if (Numeric.IsTypeCompatible(type))
type = typeof(Numeric);
if (Output.Type != type)
Output.ChangeType(type);
}
private void UpdateDataModelPath()
{
DataModelPath? old = _dataModelPath;
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
if (_dataModelPath != null)
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
if (old != null)
{
old.PathValidated -= DataModelPathOnPathValidated;
old.Dispose();
}
UpdateOutputPin();
}
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
{
// Update the output pin now that the type is known and attempt to restore the connection that was likely missing
UpdateOutputPin();
Script?.LoadConnections();
}
/// <inheritdoc />
public void Dispose()
{
_dataModelPath?.Dispose();
}
}

View File

@ -1,18 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.Screens"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.DataModel.Screens.DataModelEventCycleNodeCustomView"
x:DataType="screens:DataModelEventCycleNodeCustomViewModel">
<dataModelPicker:DataModelPickerButton Classes="condensed"
DataModelPath="{CompiledBinding DataModelPath}"
Modules="{CompiledBinding Modules}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
IsEventPicker="True"
VerticalAlignment="Top"
MaxWidth="300"/>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public partial class DataModelEventCycleNodeCustomView : ReactiveUserControl<DataModelEventCycleNodeCustomViewModel>
{
public DataModelEventCycleNodeCustomView()
{
InitializeComponent();
}
}

View File

@ -1,104 +0,0 @@
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public class DataModelEventCycleNodeCustomViewModel : CustomNodeViewModel
{
private readonly DataModelEventCycleNode _cycleNode;
private readonly INodeEditorService _nodeEditorService;
private DataModelPath? _dataModelPath;
private ObservableCollection<Module>? _modules;
private bool _updating;
public DataModelEventCycleNodeCustomViewModel(DataModelEventCycleNode cycleNode, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService) : base(cycleNode, script)
{
_cycleNode = cycleNode;
_nodeEditorService = nodeEditorService;
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
Modules = new ObservableCollection<Module>();
this.WhenActivated(d =>
{
// Set up extra modules
if (_cycleNode.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
Modules = new ObservableCollection<Module> {scriptProfile.Configuration.Module};
else if (_cycleNode.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
Modules = new ObservableCollection<Module> {profileConfiguration.Module};
// Subscribe to node changes
_cycleNode.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d);
this.WhenAnyValue(vm => vm.DataModelPath).WhereNotNull().Subscribe(ApplyDataModelPath).DisposeWith(d);
Disposable.Create(() =>
{
_dataModelPath?.Dispose();
_dataModelPath = null;
}).DisposeWith(d);
});
}
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public ObservableCollection<Module>? Modules
{
get => _modules;
set => this.RaiseAndSetIfChanged(ref _modules, value);
}
public DataModelPath? DataModelPath
{
get => _dataModelPath;
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
}
private void UpdateDataModelPath(DataModelPathEntity? entity)
{
try
{
if (_updating)
return;
_updating = true;
DataModelPath? old = DataModelPath;
DataModelPath = entity != null ? new DataModelPath(entity) : null;
old?.Dispose();
}
finally
{
_updating = false;
}
}
private void ApplyDataModelPath(DataModelPath path)
{
try
{
if (_updating)
return;
if (path.Path == _cycleNode.Storage?.Path)
return;
_updating = true;
path.Save();
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<DataModelPathEntity>(_cycleNode, path?.Entity, "event"));
}
finally
{
_updating = false;
}
}
}

View File

@ -1,19 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.Screens"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.DataModel.Screens.DataModelEventNodeCustomView"
x:DataType="screens:DataModelEventNodeCustomViewModel">
<dataModelPicker:DataModelPickerButton Classes="condensed"
DataModelPath="{CompiledBinding DataModelPath}"
Modules="{CompiledBinding Modules}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
FilterTypes="{CompiledBinding NodePinTypes}"
IsEventPicker="True"
VerticalAlignment="Top"
MaxWidth="300"/>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public partial class DataModelEventNodeCustomView : ReactiveUserControl<DataModelEventNodeCustomViewModel>
{
public DataModelEventNodeCustomView()
{
InitializeComponent();
}
}

View File

@ -1,111 +0,0 @@
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public class DataModelEventNodeCustomViewModel : CustomNodeViewModel
{
private readonly DataModelEventNode _node;
private readonly INodeEditorService _nodeEditorService;
private DataModelPath? _dataModelPath;
private ObservableCollection<Module>? _modules;
private bool _updating;
public DataModelEventNodeCustomViewModel(DataModelEventNode node, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService, INodeService nodeService)
: base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
Modules = new ObservableCollection<Module>();
List<Type> nodePinTypes = nodeService.GetRegisteredTypes();
nodePinTypes.AddRange(Constants.NumberTypes);
nodePinTypes.Add(typeof(IDataModelEvent));
NodePinTypes = new ObservableCollection<Type>(nodePinTypes);
this.WhenActivated(d =>
{
// Set up extra modules
if (_node.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
Modules = new ObservableCollection<Module> {scriptProfile.Configuration.Module};
else if (_node.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
Modules = new ObservableCollection<Module> {profileConfiguration.Module};
// Subscribe to node changes
_node.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d);
this.WhenAnyValue(vm => vm.DataModelPath).WhereNotNull().Subscribe(ApplyDataModelPath).DisposeWith(d);
Disposable.Create(() =>
{
_dataModelPath?.Dispose();
_dataModelPath = null;
}).DisposeWith(d);
});
}
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public ObservableCollection<Type> NodePinTypes { get; }
public ObservableCollection<Module>? Modules
{
get => _modules;
set => this.RaiseAndSetIfChanged(ref _modules, value);
}
public DataModelPath? DataModelPath
{
get => _dataModelPath;
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
}
private void UpdateDataModelPath(DataModelPathEntity? entity)
{
try
{
if (_updating)
return;
_updating = true;
DataModelPath? old = DataModelPath;
DataModelPath = entity != null ? new DataModelPath(entity) : null;
old?.Dispose();
}
finally
{
_updating = false;
}
}
private void ApplyDataModelPath(DataModelPath path)
{
try
{
if (_updating)
return;
if (path.Path == _node.Storage?.Path)
return;
_updating = true;
path.Save();
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<DataModelPathEntity>(_node, path?.Entity, "event"));
}
finally
{
_updating = false;
}
}
}

View File

@ -1,17 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.DataModel.Screens"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.DataModel.Screens.DataModelNodeCustomView"
x:DataType="screens:DataModelNodeCustomViewModel">
<dataModelPicker:DataModelPickerButton Classes="condensed"
DataModelPath="{CompiledBinding DataModelPath}"
Modules="{CompiledBinding Modules}"
ShowDataModelValues="{CompiledBinding ShowDataModelValues.Value}"
ShowFullPath="{CompiledBinding ShowFullPaths.Value}"
FilterTypes="{CompiledBinding NodePinTypes}"
MaxWidth="300"/>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public partial class DataModelNodeCustomView : ReactiveUserControl<DataModelNodeCustomViewModel>
{
public DataModelNodeCustomView()
{
InitializeComponent();
}
}

View File

@ -1,108 +0,0 @@
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.DataModel.Screens;
public class DataModelNodeCustomViewModel : CustomNodeViewModel
{
private readonly DataModelNode _node;
private readonly INodeEditorService _nodeEditorService;
private DataModelPath? _dataModelPath;
private ObservableCollection<Module>? _modules;
private bool _updating;
public DataModelNodeCustomViewModel(DataModelNode node, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService, INodeService nodeService) : base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true);
ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
List<Type> nodePinTypes = nodeService.GetRegisteredTypes();
nodePinTypes.AddRange(Constants.NumberTypes);
NodePinTypes = new ObservableCollection<Type>(nodePinTypes);
this.WhenActivated(d =>
{
// Set up extra modules
if (_node.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null)
Modules = new ObservableCollection<Module> {scriptProfile.Configuration.Module};
else if (_node.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null)
Modules = new ObservableCollection<Module> {profileConfiguration.Module};
// Subscribe to node changes
_node.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d);
this.WhenAnyValue(vm => vm.DataModelPath).WhereNotNull().Subscribe(ApplyDataModelPath).DisposeWith(d);
Disposable.Create(() =>
{
_dataModelPath?.Dispose();
_dataModelPath = null;
}).DisposeWith(d);
});
}
public PluginSetting<bool> ShowFullPaths { get; }
public PluginSetting<bool> ShowDataModelValues { get; }
public ObservableCollection<Type> NodePinTypes { get; }
public ObservableCollection<Module>? Modules
{
get => _modules;
set => this.RaiseAndSetIfChanged(ref _modules, value);
}
public DataModelPath? DataModelPath
{
get => _dataModelPath;
set => this.RaiseAndSetIfChanged(ref _dataModelPath, value);
}
private void UpdateDataModelPath(DataModelPathEntity? entity)
{
try
{
if (_updating)
return;
_updating = true;
DataModelPath? old = DataModelPath;
DataModelPath = entity != null ? new DataModelPath(entity) : null;
old?.Dispose();
}
finally
{
_updating = false;
}
}
private void ApplyDataModelPath(DataModelPath path)
{
try
{
if (_updating)
return;
if (path.Path == _node.Storage?.Path)
return;
_updating = true;
path.Save();
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<DataModelPathEntity>(_node, path?.Entity, "path"));
}
finally
{
_updating = false;
}
}
}

View File

@ -1,45 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.VisualScripting.Nodes.External.Commands;
public class UpdateLayerPropertyNodeSelectedLayerProperty : INodeEditorCommand
{
private readonly NodeConnectionStore _connections;
private readonly LayerPropertyNode _node;
private readonly ILayerProperty? _oldValue;
private readonly ILayerProperty? _value;
public UpdateLayerPropertyNodeSelectedLayerProperty(LayerPropertyNode node, ILayerProperty? value)
{
_node = node;
_connections = new NodeConnectionStore(_node);
_value = value;
_oldValue = _node.LayerProperty;
}
/// <inheritdoc />
public string DisplayName => "Update node layer property";
/// <inheritdoc />
public void Execute()
{
// Store connections as they currently are
_connections.Store();
// Update the selected profile element
_node.ChangeLayerProperty(_value);
}
/// <inheritdoc />
public void Undo()
{
// Restore the previous layer property
_node.ChangeLayerProperty(_oldValue);
// Restore connections
_connections.Restore();
}
}

View File

@ -1,54 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.VisualScripting.Nodes.External.Commands;
public class UpdateLayerPropertyNodeSelectedProfileElement : INodeEditorCommand
{
private readonly NodeConnectionStore _connections;
private readonly LayerPropertyNode _node;
private readonly ILayerProperty? _oldLayerProperty;
private readonly RenderProfileElement? _oldValue;
private readonly RenderProfileElement? _value;
public UpdateLayerPropertyNodeSelectedProfileElement(LayerPropertyNode node, RenderProfileElement? value)
{
_node = node;
_connections = new NodeConnectionStore(_node);
_value = value;
_oldValue = node.ProfileElement;
_oldLayerProperty = node.LayerProperty;
}
/// <inheritdoc />
public string DisplayName => "Update node profile element";
/// <inheritdoc />
public void Execute()
{
// Store connections as they currently are
_connections.Store();
// Update the selected profile element
_node.ChangeProfileElement(_value);
}
/// <inheritdoc />
public void Undo()
{
// Can't undo it if that profile element is now gone :\
if (_oldValue != null && _oldValue.Disposed)
return;
// Restore the previous profile element
_node.ChangeProfileElement(_oldValue);
// Restore the previous layer property
_node.ChangeLayerProperty(_oldLayerProperty);
// Restore connections
_connections.Restore();
}
}

View File

@ -1,154 +0,0 @@
using Artemis.Core;
using Artemis.VisualScripting.Nodes.External.Screens;
namespace Artemis.VisualScripting.Nodes.External;
// [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();
private readonly List<OutputPin> _pinBucket = new();
public LayerPropertyNode() : base("Layer/Folder Property", "Outputs the property of a selected layer or folder")
{
}
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 = profileElement?.GetAllLayerProperties().FirstOrDefault();
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)
CreateOrAddOutputPin(dataBindingRegistration.ValueType, dataBindingRegistration.DisplayName);
}
/// <summary>
/// Creates or adds an input pin to the node using a bucket.
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
/// editor.
/// </summary>
private new void CreateOrAddOutputPin(Type valueType, string displayName)
{
// Grab the first pin from the bucket that isn't on the node yet
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
// If there is none, create a new one and add it to the bucket
if (pin == null)
{
pin = CreateOutputPin(valueType, displayName);
_pinBucket.Add(pin);
}
// If there was a pin in the bucket, update it's type and display name and reuse it
else
{
pin.ChangeType(valueType);
pin.Name = displayName;
AddPin(pin);
}
}
private void ProfileOnChildRemoved(object? sender, EventArgs e)
{
if (Script?.Context is not Profile profile)
return;
if (ProfileElement == null || !profile.GetAllRenderElements().Contains(ProfileElement))
ChangeProfileElement(null);
}
}
public class LayerPropertyNodeEntity
{
public Guid ElementId { get; set; }
public string? PropertyPath { get; set; }
}

View File

@ -1,24 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.External.Screens"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.External.Screens.LayerPropertyNodeCustomView"
x:DataType="screens:LayerPropertyNodeCustomViewModel">
<Grid>
<StackPanel Spacing="5" VerticalAlignment="Top" IsVisible="{CompiledBinding !OutsideProfileContext}">
<ComboBox Classes="condensed" ItemsSource="{CompiledBinding ProfileElements}" SelectedItem="{CompiledBinding SelectedProfileElement}" />
<ComboBox Classes="condensed" ItemsSource="{CompiledBinding LayerProperties}" SelectedItem="{CompiledBinding SelectedLayerProperty}" />
</StackPanel>
<StackPanel IsVisible="{CompiledBinding OutsideProfileContext}" VerticalAlignment="Center">
<TextBlock Classes="BodyStrongTextBlockStyle" TextAlignment="Center">
Node not available
</TextBlock>
<TextBlock Classes="BodyTextBlockStyle" Foreground="{DynamicResource TextFillColorSecondary}" TextAlignment="Center">
This node cannot be used outside profile scripts.
</TextBlock>
</StackPanel>
</Grid>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.External.Screens;
public partial class LayerPropertyNodeCustomView : ReactiveUserControl<LayerPropertyNodeCustomViewModel>
{
public LayerPropertyNodeCustomView()
{
InitializeComponent();
}
}

View File

@ -1,93 +0,0 @@
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.VisualScripting;
using Artemis.VisualScripting.Nodes.External.Commands;
using DynamicData;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.External.Screens;
public class LayerPropertyNodeCustomViewModel : CustomNodeViewModel
{
private readonly LayerPropertyNode _node;
private readonly INodeEditorService _nodeEditorService;
private bool _outsideProfileContext;
public LayerPropertyNodeCustomViewModel(LayerPropertyNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
this.WhenActivated(d =>
{
if (_node.Script?.Context is not Profile profile)
{
OutsideProfileContext = true;
return;
}
OutsideProfileContext = false;
Observable.FromEventPattern<ProfileElementEventArgs>(x => profile.DescendentAdded += x, x => profile.DescendentAdded -= x).Subscribe(_ => GetProfileElements()).DisposeWith(d);
Observable.FromEventPattern<ProfileElementEventArgs>(x => profile.DescendentRemoved += x, x => profile.DescendentRemoved -= x).Subscribe(_ => GetProfileElements()).DisposeWith(d);
GetProfileElements();
GetLayerProperties();
});
NodeModified += (_, _) => this.RaisePropertyChanged(nameof(SelectedProfileElement));
NodeModified += (_, _) => this.RaisePropertyChanged(nameof(SelectedLayerProperty));
this.WhenAnyValue(vm => vm.SelectedProfileElement).Subscribe(_ => GetLayerProperties());
}
public ObservableCollection<RenderProfileElement> ProfileElements { get; } = new();
public ObservableCollection<ILayerProperty> LayerProperties { get; } = new();
public bool OutsideProfileContext
{
get => _outsideProfileContext;
set => this.RaiseAndSetIfChanged(ref _outsideProfileContext, value);
}
public RenderProfileElement? SelectedProfileElement
{
get => _node.ProfileElement;
set
{
if (value != null && !Equals(_node.ProfileElement, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateLayerPropertyNodeSelectedProfileElement(_node, value));
}
}
public ILayerProperty? SelectedLayerProperty
{
get => _node.LayerProperty;
set
{
if (value != null && !Equals(_node.LayerProperty, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateLayerPropertyNodeSelectedLayerProperty(_node, value));
}
}
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;
}
private void GetLayerProperties()
{
LayerProperties.Clear();
if (_node.ProfileElement == null)
return;
LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => !l.IsHidden && l.DataBindingsSupported));
SelectedLayerProperty = _node.LayerProperty;
}
}

View File

@ -1,74 +0,0 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.VisualScripting.Nodes.Input.Screens;
namespace Artemis.VisualScripting.Nodes.Input;
[Node("Hotkey enable/disable", "Outputs a boolean value enabled and disabled by a set of hotkeys", "Input", OutputType = typeof(bool))]
public class HotkeyEnableDisableNode : Node<HotkeyEnableDisableNodeEntity, HotkeyEnableDisableNodeCustomViewModel>, IDisposable
{
private readonly IInputService _inputService;
private Hotkey? _disableHotkey;
private Hotkey? _enableHotkey;
private bool _value;
private bool _retrievedInitialValue;
public HotkeyEnableDisableNode(IInputService inputService)
{
_inputService = inputService;
_inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
InitialValue = CreateInputPin<bool>();
Output = CreateOutputPin<bool>();
StorageModified += OnStorageModified;
}
public InputPin<bool> InitialValue { get; }
public OutputPin<bool> Output { get; }
public override void Initialize(INodeScript script)
{
LoadHotkeys();
}
public override void Evaluate()
{
if (!_retrievedInitialValue)
{
_value = InitialValue.Value;
_retrievedInitialValue = true;
}
Output.Value = _value;
}
private void OnStorageModified(object? sender, EventArgs e)
{
LoadHotkeys();
}
private void LoadHotkeys()
{
if (Storage == null)
return;
_enableHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null;
_disableHotkey = Storage.DisableHotkey != null ? new Hotkey(Storage.DisableHotkey) : null;
}
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage == null)
return;
if (_disableHotkey != null && _disableHotkey.MatchesEventArgs(e))
_value = false;
else if (_enableHotkey != null && _enableHotkey.MatchesEventArgs(e))
_value = true;
}
public void Dispose()
{
_inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp;
}
}

View File

@ -1,18 +0,0 @@
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
namespace Artemis.VisualScripting.Nodes.Input;
public class HotkeyEnableDisableNodeEntity
{
public HotkeyEnableDisableNodeEntity(Hotkey? enableHotkey, Hotkey? disableHotkey)
{
enableHotkey?.Save();
EnableHotkey = enableHotkey?.Entity;
disableHotkey?.Save();
DisableHotkey = disableHotkey?.Entity;
}
public ProfileConfigurationHotkeyEntity? EnableHotkey { get; set; }
public ProfileConfigurationHotkeyEntity? DisableHotkey { get; set; }
}

View File

@ -1,69 +0,0 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.VisualScripting.Nodes.Input.Screens;
namespace Artemis.VisualScripting.Nodes.Input;
[Node("Hotkey press", "Outputs a boolean value for as long as a hotkey is pressed", "Input", OutputType = typeof(bool))]
public class HotkeyPressNode : Node<HotkeyEnableDisableNodeEntity, HotkeyPressNodeCustomViewModel>, IDisposable
{
private readonly IInputService _inputService;
private Hotkey? _toggleHotkey;
private bool _value;
public HotkeyPressNode(IInputService inputService)
{
_inputService = inputService;
_inputService.KeyboardKeyDown += InputServiceOnKeyboardKeyDown;
_inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
Output = CreateOutputPin<bool>();
StorageModified += OnStorageModified;
}
public OutputPin<bool> Output { get; }
public override void Initialize(INodeScript script)
{
LoadHotkeys();
}
public override void Evaluate()
{
Output.Value = _value;
}
private void OnStorageModified(object? sender, EventArgs e)
{
LoadHotkeys();
}
private void LoadHotkeys()
{
if (Storage == null)
return;
_toggleHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null;
}
private void InputServiceOnKeyboardKeyDown(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage == null)
return;
if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e))
_value = true;
}
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage == null)
return;
if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e))
_value = false;
}
public void Dispose()
{
_inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp;
}
}

View File

@ -1,69 +0,0 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.VisualScripting.Nodes.Input.Screens;
namespace Artemis.VisualScripting.Nodes.Input;
[Node("Hotkey toggle", "Outputs a boolean value toggled by a hotkey", "Input", OutputType = typeof(bool))]
public class HotkeyToggleNode : Node<HotkeyEnableDisableNodeEntity, HotkeyToggleNodeCustomViewModel>, IDisposable
{
private readonly IInputService _inputService;
private Hotkey? _toggleHotkey;
private bool _value;
private bool _retrievedInitialValue;
public HotkeyToggleNode(IInputService inputService)
{
_inputService = inputService;
_inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
InitialValue = CreateInputPin<bool>();
Output = CreateOutputPin<bool>();
StorageModified += OnStorageModified;
}
public InputPin<bool> InitialValue { get; }
public OutputPin<bool> Output { get; }
public override void Initialize(INodeScript script)
{
LoadHotkeys();
}
public override void Evaluate()
{
if (!_retrievedInitialValue)
{
_value = InitialValue.Value;
_retrievedInitialValue = true;
}
Output.Value = _value;
}
private void OnStorageModified(object? sender, EventArgs e)
{
LoadHotkeys();
}
private void LoadHotkeys()
{
if (Storage == null)
return;
_toggleHotkey = Storage.EnableHotkey != null ? new Hotkey(Storage.EnableHotkey) : null;
}
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage == null)
return;
if (_toggleHotkey != null && _toggleHotkey.MatchesEventArgs(e))
_value = !_value;
}
public void Dispose()
{
_inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp;
}
}

View File

@ -1,73 +0,0 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.VisualScripting.Nodes.Input.Screens;
using SkiaSharp;
using static Artemis.VisualScripting.Nodes.Input.PressedKeyPositionNodeEntity;
namespace Artemis.VisualScripting.Nodes.Input;
[Node("Pressed Key Position", "Outputs the position of a pressed key relative to a layer", "Input", OutputType = typeof(Numeric))]
public class PressedKeyPositionNode : Node<PressedKeyPositionNodeEntity, PressedKeyPositionNodeCustomViewModel>, IDisposable
{
private readonly IInputService _inputService;
private Layer? _layer;
private SKPoint _ledPosition;
private Profile? _profile;
public PressedKeyPositionNode(IInputService inputService)
{
_inputService = inputService;
XPosition = CreateOutputPin<Numeric>("X");
YPosition = CreateOutputPin<Numeric>("Y");
StorageModified += OnStorageModified;
_inputService.KeyboardKeyDown += InputServiceOnKeyboardKeyDown;
_inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
}
public OutputPin<Numeric> XPosition { get; }
public OutputPin<Numeric> YPosition { get; }
public override void Initialize(INodeScript script)
{
Storage ??= new PressedKeyPositionNodeEntity();
_profile = script.Context as Profile;
_layer = _profile?.GetAllLayers().FirstOrDefault(l => l.EntityId == Storage.LayerId);
}
public override void Evaluate()
{
XPosition.Value = _ledPosition.X;
YPosition.Value = _ledPosition.Y;
}
private void InputServiceOnKeyboardKeyDown(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage?.RespondTo is KeyPressType.Down or KeyPressType.UpDown)
SetLedPosition(e.Led);
}
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
{
if (Storage?.RespondTo is KeyPressType.Up or KeyPressType.UpDown)
SetLedPosition(e.Led);
}
private void SetLedPosition(ArtemisLed? led)
{
if (_layer != null && led != null)
_ledPosition = new SKPoint((led.AbsoluteRectangle.MidX - _layer.Bounds.Left) / _layer.Bounds.Width, (led.AbsoluteRectangle.MidY - _layer.Bounds.Top) / _layer.Bounds.Height);
}
private void OnStorageModified(object? sender, EventArgs e)
{
_layer = _profile?.GetAllLayers().FirstOrDefault(l => l.EntityId == Storage?.LayerId);
}
public void Dispose()
{
_inputService.KeyboardKeyDown -= InputServiceOnKeyboardKeyDown;
_inputService.KeyboardKeyUp -= InputServiceOnKeyboardKeyUp;
}
}

View File

@ -1,29 +0,0 @@
using System.ComponentModel;
namespace Artemis.VisualScripting.Nodes.Input;
public class PressedKeyPositionNodeEntity
{
public PressedKeyPositionNodeEntity()
{
}
public PressedKeyPositionNodeEntity(Guid layerId, KeyPressType respondTo)
{
LayerId = layerId;
RespondTo = respondTo;
}
public Guid LayerId { get; set; }
public KeyPressType RespondTo { get; set; }
public enum KeyPressType
{
[Description("Up")]
Up,
[Description("Down")]
Down,
[Description("Up/down")]
UpDown
}
}

View File

@ -1,16 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Input.Screens"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Input.Screens.HotkeyEnableDisableNodeCustomView"
x:DataType="screens:HotkeyEnableDisableNodeCustomViewModel">
<StackPanel>
<TextBlock FontSize="13" Margin="0 -2 0 5">Enable</TextBlock>
<shared:HotkeyBox Classes="condensed" Hotkey="{CompiledBinding EnableHotkey}" HotkeyChanged="HotkeyBox_OnHotkeyChanged" MinWidth="75" />
<TextBlock FontSize="13" Margin="0 5">Disable</TextBlock>
<shared:HotkeyBox Classes="condensed" Hotkey="{CompiledBinding DisableHotkey}" HotkeyChanged="HotkeyBox_OnHotkeyChanged" MinWidth="75" />
</StackPanel>
</UserControl>

View File

@ -1,19 +0,0 @@
using Artemis.UI.Shared;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public partial class HotkeyEnableDisableNodeCustomView : ReactiveUserControl<HotkeyEnableDisableNodeCustomViewModel>
{
public HotkeyEnableDisableNodeCustomView()
{
InitializeComponent();
}
private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args)
{
ViewModel?.Save();
}
}

View File

@ -1,65 +0,0 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public class HotkeyEnableDisableNodeCustomViewModel : CustomNodeViewModel
{
private readonly HotkeyEnableDisableNode _enableDisableNode;
private readonly INodeEditorService _nodeEditorService;
private Hotkey? _enableHotkey;
private Hotkey? _disableHotkey;
private bool _updating;
/// <inheritdoc />
public HotkeyEnableDisableNodeCustomViewModel(HotkeyEnableDisableNode enableDisableNode, INodeScript script, INodeEditorService nodeEditorService) : base(enableDisableNode, script)
{
_enableDisableNode = enableDisableNode;
_nodeEditorService = nodeEditorService;
this.WhenActivated(d =>
{
Observable.FromEventPattern(x => _enableDisableNode.StorageModified += x, x => _enableDisableNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d);
Update();
});
}
private void Update()
{
_updating = true;
EnableHotkey = _enableDisableNode.Storage?.EnableHotkey != null ? new Hotkey(_enableDisableNode.Storage.EnableHotkey) : null;
DisableHotkey = _enableDisableNode.Storage?.DisableHotkey != null ? new Hotkey(_enableDisableNode.Storage.DisableHotkey) : null;
_updating = false;
}
public Hotkey? EnableHotkey
{
get => _enableHotkey;
set => this.RaiseAndSetIfChanged(ref _enableHotkey, value);
}
public Hotkey? DisableHotkey
{
get => _disableHotkey;
set => this.RaiseAndSetIfChanged(ref _disableHotkey, value);
}
public void Save()
{
if (_updating)
return;
_nodeEditorService.ExecuteCommand(
Script,
new UpdateStorage<HotkeyEnableDisableNodeEntity>(_enableDisableNode, new HotkeyEnableDisableNodeEntity(EnableHotkey, DisableHotkey), "hotkey")
);
}
}

View File

@ -1,11 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Input.Screens"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Input.Screens.HotkeyPressNodeCustomView"
x:DataType="screens:HotkeyPressNodeCustomViewModel">
<shared:HotkeyBox Classes="condensed" Hotkey="{CompiledBinding ToggleHotkey}" HotkeyChanged="HotkeyBox_OnHotkeyChanged" MinWidth="75" />
</UserControl>

View File

@ -1,19 +0,0 @@
using Artemis.UI.Shared;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public partial class HotkeyPressNodeCustomView : ReactiveUserControl<HotkeyPressNodeCustomViewModel>
{
public HotkeyPressNodeCustomView()
{
InitializeComponent();
}
private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args)
{
ViewModel?.Save();
}
}

View File

@ -1,55 +0,0 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public class HotkeyPressNodeCustomViewModel : CustomNodeViewModel
{
private readonly HotkeyPressNode _pressNode;
private readonly INodeEditorService _nodeEditorService;
private Hotkey? _toggleHotkey;
private bool _updating;
/// <inheritdoc />
public HotkeyPressNodeCustomViewModel(HotkeyPressNode pressNode, INodeScript script, INodeEditorService nodeEditorService) : base(pressNode, script)
{
_pressNode = pressNode;
_nodeEditorService = nodeEditorService;
this.WhenActivated(d =>
{
Observable.FromEventPattern(x => _pressNode.StorageModified += x, x => _pressNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d);
Update();
});
}
private void Update()
{
_updating = true;
ToggleHotkey = _pressNode.Storage?.EnableHotkey != null ? new Hotkey(_pressNode.Storage.EnableHotkey) : null;
_updating = false;
}
public Hotkey? ToggleHotkey
{
get => _toggleHotkey;
set => this.RaiseAndSetIfChanged(ref _toggleHotkey, value);
}
public void Save()
{
if (_updating)
return;
_nodeEditorService.ExecuteCommand(
Script,
new UpdateStorage<HotkeyEnableDisableNodeEntity>(_pressNode, new HotkeyEnableDisableNodeEntity(ToggleHotkey, null), "hotkey")
);
}
}

View File

@ -1,11 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Input.Screens"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Input.Screens.HotkeyToggleNodeCustomView"
x:DataType="screens:HotkeyToggleNodeCustomViewModel">
<shared:HotkeyBox Classes="condensed" Hotkey="{CompiledBinding ToggleHotkey}" HotkeyChanged="HotkeyBox_OnHotkeyChanged" MinWidth="75" />
</UserControl>

View File

@ -1,19 +0,0 @@
using Artemis.UI.Shared;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public partial class HotkeyToggleNodeCustomView : ReactiveUserControl<HotkeyToggleNodeCustomViewModel>
{
public HotkeyToggleNodeCustomView()
{
InitializeComponent();
}
private void HotkeyBox_OnHotkeyChanged(HotkeyBox sender, EventArgs args)
{
ViewModel?.Save();
}
}

View File

@ -1,55 +0,0 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public class HotkeyToggleNodeCustomViewModel : CustomNodeViewModel
{
private readonly HotkeyToggleNode _toggleNode;
private readonly INodeEditorService _nodeEditorService;
private Hotkey? _toggleHotkey;
private bool _updating;
/// <inheritdoc />
public HotkeyToggleNodeCustomViewModel(HotkeyToggleNode toggleNode, INodeScript script, INodeEditorService nodeEditorService) : base(toggleNode, script)
{
_toggleNode = toggleNode;
_nodeEditorService = nodeEditorService;
this.WhenActivated(d =>
{
Observable.FromEventPattern(x => _toggleNode.StorageModified += x, x => _toggleNode.StorageModified -= x).Subscribe(_ => Update()).DisposeWith(d);
Update();
});
}
private void Update()
{
_updating = true;
ToggleHotkey = _toggleNode.Storage?.EnableHotkey != null ? new Hotkey(_toggleNode.Storage.EnableHotkey) : null;
_updating = false;
}
public Hotkey? ToggleHotkey
{
get => _toggleHotkey;
set => this.RaiseAndSetIfChanged(ref _toggleHotkey, value);
}
public void Save()
{
if (_updating)
return;
_nodeEditorService.ExecuteCommand(
Script,
new UpdateStorage<HotkeyEnableDisableNodeEntity>(_toggleNode, new HotkeyEnableDisableNodeEntity(ToggleHotkey, null), "hotkey")
);
}
}

View File

@ -1,29 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Input.Screens"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Input.Screens.PressedKeyPositionNodeCustomView"
x:DataType="screens:PressedKeyPositionNodeCustomViewModel">
<StackPanel>
<TextBlock FontSize="13" Margin="0 -2 0 5">Layer</TextBlock>
<ComboBox Classes="condensed" ItemsSource="{CompiledBinding Layers}" SelectedItem="{CompiledBinding SelectedLayer}" PlaceholderText="Select a layer">
<ComboBox.ItemTemplate>
<DataTemplate DataType="core:Layer">
<StackPanel Spacing="5" Orientation="Horizontal">
<avalonia:MaterialIcon Kind="{CompiledBinding LayerBrush.Descriptor.Icon, FallbackValue=QuestionMark}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock FontSize="13" Margin="0 5">Respond to</TextBlock>
<shared:EnumComboBox Classes="condensed" Value="{CompiledBinding RespondTo}"/>
</StackPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Input.Screens;
public partial class PressedKeyPositionNodeCustomView : ReactiveUserControl<PressedKeyPositionNodeCustomViewModel>
{
public PressedKeyPositionNodeCustomView()
{
InitializeComponent();
}
}

Some files were not shown because too many files have changed in this diff Show More