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

Visual Scripting - Fixed storage issues where connections would be lost

Visual Scripting - Added node duplication
This commit is contained in:
Robert 2022-08-13 11:17:47 +02:00
parent c325a29cc0
commit 041ed8e0a0
11 changed files with 256 additions and 60 deletions

View File

@ -264,11 +264,23 @@ namespace Artemis.Core
}
// Only connect the nodes if they aren't already connected (LoadConnections may be called twice or more)
if (!targetPin.ConnectedTo.Contains(sourcePin) && targetPin.IsTypeCompatible(sourcePin.Type))
// Type checking is done later when all connections are in place
if (!targetPin.ConnectedTo.Contains(sourcePin))
targetPin.ConnectTo(sourcePin);
if (!sourcePin.ConnectedTo.Contains(targetPin) && sourcePin.IsTypeCompatible(targetPin.Type))
if (!sourcePin.ConnectedTo.Contains(targetPin))
sourcePin.ConnectTo(targetPin);
}
// With all connections restored, ensure types match (connecting pins may affect types so the check is done afterwards)
foreach (INode node in nodes)
{
foreach (IPin nodePin in node.Pins.Concat(node.PinCollections.SelectMany(p => p)))
{
List<IPin> toDisconnect = nodePin.ConnectedTo.Where(c => !c.IsTypeCompatible(nodePin.Type)).ToList();
foreach (IPin pin in toDisconnect)
pin.DisconnectFrom(nodePin);
}
}
}
/// <inheritdoc />

View File

@ -129,6 +129,7 @@ namespace Artemis.Core
/// <inheritdoc />
public bool IsTypeCompatible(Type type) => Type == type
|| Type == typeof(Enum) && type.IsEnum
|| Type.IsEnum && type == typeof(Enum)
|| Direction == PinDirection.Input && Type == typeof(object)
|| Direction == PinDirection.Output && type == typeof(object);

View File

@ -1,29 +0,0 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Shared.Converters;
/// <summary>
/// Converts <see cref="SKColor" /> into <see cref="Color2" />.
/// </summary>
public class SKColorToColor2Converter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is SKColor skColor)
return new Color2(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
return new Color2();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color2 color2)
return new SKColor(color2.R, color2.G, color2.B, color2.A);
return SKColor.Empty;
}
}

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using FluentAvalonia.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to duplicate a node.
/// </summary>
public class DuplicateNode : INodeEditorCommand, IDisposable
{
private readonly INode _node;
private readonly bool _copyIncomingConnections;
private readonly INodeService _nodeService;
private readonly INodeScript _nodeScript;
private INode? _copy;
private NodeConnectionStore? _connections;
private bool _executed;
/// <summary>
/// Creates a new instance of the <see cref="DeleteNode" /> class.
/// </summary>
/// <param name="nodeScript">The node script the node belongs to.</param>
/// <param name="node">The node to delete.</param>
/// <param name="copyIncomingConnections">A boolean indicating whether incoming connections should be copied.</param>
/// <param name="nodeService">The service to use to duplicate the node.</param>
public DuplicateNode(INodeScript nodeScript, INode node, bool copyIncomingConnections, INodeService nodeService)
{
_nodeScript = nodeScript;
_node = node;
_copyIncomingConnections = copyIncomingConnections;
_nodeService = nodeService;
}
/// <inheritdoc />
public void Dispose()
{
if (!_executed && _copy is IDisposable disposableNode)
disposableNode.Dispose();
}
/// <inheritdoc />
public string DisplayName => $"Duplicate '{_node.Name}' node";
/// <inheritdoc />
public void Execute()
{
if (_copy == null)
{
_copy = CreateCopy();
_nodeScript.AddNode(_copy);
}
else if (_connections != null)
{
_nodeScript.AddNode(_copy);
_connections.Restore();
}
_executed = true;
}
/// <inheritdoc />
public void Undo()
{
if (_copy != null && _connections != null)
{
_connections.Store();
_nodeScript.RemoveNode(_copy);
}
_executed = false;
}
private INode CreateCopy()
{
NodeData? nodeData = _nodeService.AvailableNodes.FirstOrDefault(d => d.Type == _node.GetType());
if (nodeData == null)
throw new ArtemisSharedUIException($"Can't create a copy of node of type {_node.GetType()} because there is no matching node data.");
// Create the node
INode node = nodeData.CreateNode(_nodeScript, null);
// Move it slightly
node.X = _node.X + 20;
node.Y = _node.Y + 20;
// Add pins to collections
for (int i = 0; i < _node.PinCollections.Count; i++)
{
IPinCollection sourceCollection = _node.PinCollections.ElementAt(i);
IPinCollection? targetCollection = node.PinCollections.ElementAtOrDefault(i);
if (targetCollection == null)
continue;
while (targetCollection.Count() < sourceCollection.Count())
targetCollection.CreatePin();
}
// Copy the storage
if (_node is Node sourceNode && node is Node targetNode)
targetNode.DeserializeStorage(sourceNode.SerializeStorage());
// Connect input pins
if (_copyIncomingConnections)
{
List<IPin> sourceInputPins = _node.Pins.Concat(_node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Input).ToList();
List<IPin> targetInputPins = node.Pins.Concat(node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Input).ToList();
for (int i = 0; i < sourceInputPins.Count; i++)
{
IPin source = sourceInputPins.ElementAt(i);
IPin? target = targetInputPins.ElementAtOrDefault(i);
if (target == null)
continue;
foreach (IPin pin in source.ConnectedTo)
target.ConnectTo(pin);
}
}
_connections = new NodeConnectionStore(node);
return node;
}
}

View File

@ -53,18 +53,20 @@ public class NodeConnectionStore
{
foreach (IPin nodePin in Node.Pins)
{
if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
if (!_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
continue;
foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
}
foreach (IPinCollection nodePinCollection in Node.PinCollections)
{
foreach (IPin nodePin in nodePinCollection)
{
if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
if (!_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
continue;
foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
}
}

View File

@ -26,6 +26,13 @@
</VisualBrush.Visual>
</VisualBrush>
</UserControl.Resources>
<UserControl.KeyBindings>
<KeyBinding Command="{CompiledBinding ClearSelection}" Gesture="Escape" />
<KeyBinding Command="{CompiledBinding DeleteSelected}" Gesture="Delete" />
<KeyBinding Command="{CompiledBinding DuplicateSelected}" Gesture="Ctrl+D" />
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
</UserControl.KeyBindings>
<paz:ZoomBorder Name="ZoomBorder"
Stretch="None"
Focusable="True"
@ -77,11 +84,11 @@
</ItemsControl>
<shared:SelectionRectangle Name="SelectionRectangle"
InputElement="{Binding #ZoomBorder}"
SelectionUpdated="SelectionRectangle_OnSelectionUpdated"
SelectionFinished="SelectionRectangle_OnSelectionFinished"
BorderBrush="{DynamicResource SystemAccentColor}"
BorderRadius="8">
InputElement="{Binding #ZoomBorder}"
SelectionUpdated="SelectionRectangle_OnSelectionUpdated"
SelectionFinished="SelectionRectangle_OnSelectionFinished"
BorderBrush="{DynamicResource SystemAccentColor}"
BorderRadius="8">
<shared:SelectionRectangle.Background>
<SolidColorBrush Color="{DynamicResource SystemAccentColorLight1}" Opacity="0.2"></SolidColorBrush>
</shared:SelectionRectangle.Background>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Artemis.Core;
@ -79,6 +80,12 @@ public class NodeScriptViewModel : ActivatableViewModelBase
.Bind(out ReadOnlyObservableCollection<CableViewModel> cableViewModels)
.Subscribe();
CableViewModels = cableViewModels;
ClearSelection = ReactiveCommand.Create(ExecuteClearSelection);
DeleteSelected = ReactiveCommand.Create(ExecuteDeleteSelected);
DuplicateSelected = ReactiveCommand.Create(ExecuteDuplicateSelected);
CopySelected = ReactiveCommand.Create(ExecuteCopySelected);
PasteSelected = ReactiveCommand.Create(ExecutePasteSelected);
}
public NodeScript NodeScript { get; }
@ -91,12 +98,18 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public bool IsPreview { get; }
public ReactiveCommand<Unit, Unit> ClearSelection { get; }
public ReactiveCommand<Unit, Unit> DeleteSelected { get; }
public ReactiveCommand<Unit, Unit> DuplicateSelected { get; }
public ReactiveCommand<Unit, Unit> CopySelected { get; }
public ReactiveCommand<Unit, Unit> PasteSelected { get; }
public DragCableViewModel? DragViewModel
{
get => _dragViewModel;
set => RaiseAndSetIfChanged(ref _dragViewModel, value);
}
public Matrix PanMatrix
{
get => _panMatrix;
@ -219,6 +232,43 @@ public class NodeScriptViewModel : ActivatableViewModelBase
}
}
private void ExecuteClearSelection()
{
ClearNodeSelection();
}
private void ExecuteDeleteSelected()
{
List<INode> nodes = NodeViewModels.Where(vm => vm.IsSelected).Select(vm => vm.Node).ToList();
using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, "Delete nodes");
foreach (INode node in nodes.Where(n => !n.IsDefaultNode && !n.IsExitNode))
_nodeEditorService.ExecuteCommand(NodeScript, new DeleteNode(NodeScript, node));
}
private void ExecuteDuplicateSelected()
{
int nodeCount = NodeViewModels.Count;
List<INode> nodes = NodeViewModels.Where(vm => vm.IsSelected).Select(vm => vm.Node).ToList();
using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, "Duplicate nodes");
foreach (INode node in nodes.Where(n => !n.IsDefaultNode && !n.IsExitNode))
_nodeEditorService.ExecuteCommand(NodeScript, new DuplicateNode(NodeScript, node, false, _nodeService));
// Select only the new nodes
for (int index = 0; index < NodeViewModels.Count; index++)
{
NodeViewModel nodeViewModel = NodeViewModels[index];
nodeViewModel.IsSelected = index >= nodeCount;
}
}
private void ExecuteCopySelected()
{
}
private void ExecutePasteSelected()
{
}
private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)
{
_nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
@ -235,6 +285,6 @@ public class NodeScriptViewModel : ActivatableViewModelBase
{
AutoFitRequested?.Invoke(this, EventArgs.Empty);
}
public event EventHandler? AutoFitRequested;
}

View File

@ -4,7 +4,7 @@ using Artemis.VisualScripting.Nodes.Operators.Screens;
namespace Artemis.VisualScripting.Nodes.Operators;
[Node("Enum Equals", "Determines the equality between an input and a selected enum value", "Operators", InputType = typeof(Enum), OutputType = typeof(bool))]
public class EnumEqualsNode : Node<int, EnumEqualsNodeCustomViewModel>
public class EnumEqualsNode : Node<long, EnumEqualsNodeCustomViewModel>
{
public EnumEqualsNode() : base("Enum Equals", "Determines the equality between an input and a selected enum value")
{
@ -21,6 +21,6 @@ public class EnumEqualsNode : Node<int, EnumEqualsNodeCustomViewModel>
if (InputPin.Value == null)
OutputPin.Value = false;
else
OutputPin.Value = Convert.ToInt32(InputPin.Value) == Storage;
OutputPin.Value = Convert.ToInt64(InputPin.Value) == Storage;
}
}

View File

@ -8,8 +8,20 @@
x:DataType="screens:EnumEqualsNodeCustomViewModel">
<ComboBox IsEnabled="{CompiledBinding EnumValues.Count}"
Items="{CompiledBinding EnumValues}"
SelectedIndex="{CompiledBinding CurrentValue}"
SelectedItem="{CompiledBinding CurrentValue}"
VirtualizationMode="Simple"
PlaceholderText="Select a value"
Classes="condensed"
VerticalAlignment="Center" />
VerticalAlignment="Center">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding [1]}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</UserControl>

View File

@ -6,7 +6,10 @@ using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
using Avalonia.Controls.Mixins;
using Avalonia.Threading;
using DynamicData;
using FluentAvalonia.Core;
using Humanizer;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Operators.Screens;
@ -28,13 +31,11 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
{
EnumValues.Clear();
if (_node.InputPin.ConnectedTo.First().Type.IsEnum)
EnumValues.AddRange(Enum.GetValues(_node.InputPin.ConnectedTo.First().Type).Cast<Enum>());
this.RaisePropertyChanged(nameof(CurrentValue));
AddEnumValues(_node.InputPin.ConnectedTo.First().Type);
}
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinConnected += x, x => _node.InputPin.PinConnected -= x)
.Subscribe(p => EnumValues.AddRange(Enum.GetValues(p.EventArgs.Value.Type).Cast<Enum>()))
.Subscribe(p => AddEnumValues(p.EventArgs.Value.Type))
.DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x)
.Subscribe(_ => EnumValues.Clear())
@ -42,15 +43,29 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
});
}
public ObservableCollection<Enum> EnumValues { get; } = new();
public int CurrentValue
private void AddEnumValues(Type type)
{
get => _node.Storage;
Dispatcher.UIThread.Post(() =>
{
List<(long, string)> values = Enum.GetValues(type).Cast<Enum>().Select(e => (Convert.ToInt64(e), e.Humanize())).ToList();
if (values.Count > 20)
EnumValues.AddRange(values.OrderBy(v => v.Item2));
else
EnumValues.AddRange(Enum.GetValues(type).Cast<Enum>().Select(e => (Convert.ToInt64(e), e.Humanize())));
this.RaisePropertyChanged(nameof(CurrentValue));
}, DispatcherPriority.Background);
}
public ObservableCollection<(long, string)> EnumValues { get; } = new();
public (long, string) CurrentValue
{
get => EnumValues.FirstOrDefault(v => v.Item1 == _node.Storage);
set
{
if (!Equals(_node.Storage, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<int>(_node, value));
if (!Equals(_node.Storage, value.Item1))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<long>(_node, value.Item1));
}
}
}

View File

@ -11,7 +11,7 @@
x:DataType="screens:StaticSKColorValueNodeCustomViewModel">
<UserControl.Resources>
<converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
<converters:SKColorToColor2Converter x:Key="SKColorToColor2Converter" />
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources>
<Grid Height="24" ColumnDefinitions="*" Width="110">
<TextBox Classes="condensed"
@ -22,7 +22,7 @@
</Interaction.Behaviors>
</TextBox>
<controls:ColorPickerButton Classes="contained-color-picker-button"
Color="{CompiledBinding CurrentValue, Converter={StaticResource SKColorToColor2Converter}}"
Color="{CompiledBinding CurrentValue, Converter={StaticResource SKColorToColorConverter}}"
ShowAcceptDismissButtons="False"
FlyoutOpened="ColorPickerButton_OnFlyoutOpened"
FlyoutClosed="ColorPickerButton_OnFlyoutClosed" />