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) // 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); targetPin.ConnectTo(sourcePin);
if (!sourcePin.ConnectedTo.Contains(targetPin) && sourcePin.IsTypeCompatible(targetPin.Type)) if (!sourcePin.ConnectedTo.Contains(targetPin))
sourcePin.ConnectTo(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 /> /// <inheritdoc />

View File

@ -129,6 +129,7 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public bool IsTypeCompatible(Type type) => Type == type public bool IsTypeCompatible(Type type) => Type == type
|| Type == typeof(Enum) && type.IsEnum || Type == typeof(Enum) && type.IsEnum
|| Type.IsEnum && type == typeof(Enum)
|| Direction == PinDirection.Input && Type == typeof(object) || Direction == PinDirection.Input && Type == typeof(object)
|| Direction == PinDirection.Output && 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) foreach (IPin nodePin in Node.Pins)
{ {
if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections)) if (!_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
foreach (IPin connection in connections) continue;
nodePin.ConnectTo(connection); foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
} }
foreach (IPinCollection nodePinCollection in Node.PinCollections) foreach (IPinCollection nodePinCollection in Node.PinCollections)
{ {
foreach (IPin nodePin in nodePinCollection) foreach (IPin nodePin in nodePinCollection)
{ {
if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections)) if (!_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
foreach (IPin connection in connections) continue;
nodePin.ConnectTo(connection); foreach (IPin connection in connections)
nodePin.ConnectTo(connection);
} }
} }

View File

@ -26,6 +26,13 @@
</VisualBrush.Visual> </VisualBrush.Visual>
</VisualBrush> </VisualBrush>
</UserControl.Resources> </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" <paz:ZoomBorder Name="ZoomBorder"
Stretch="None" Stretch="None"
Focusable="True" Focusable="True"
@ -77,11 +84,11 @@
</ItemsControl> </ItemsControl>
<shared:SelectionRectangle Name="SelectionRectangle" <shared:SelectionRectangle Name="SelectionRectangle"
InputElement="{Binding #ZoomBorder}" InputElement="{Binding #ZoomBorder}"
SelectionUpdated="SelectionRectangle_OnSelectionUpdated" SelectionUpdated="SelectionRectangle_OnSelectionUpdated"
SelectionFinished="SelectionRectangle_OnSelectionFinished" SelectionFinished="SelectionRectangle_OnSelectionFinished"
BorderBrush="{DynamicResource SystemAccentColor}" BorderBrush="{DynamicResource SystemAccentColor}"
BorderRadius="8"> BorderRadius="8">
<shared:SelectionRectangle.Background> <shared:SelectionRectangle.Background>
<SolidColorBrush Color="{DynamicResource SystemAccentColorLight1}" Opacity="0.2"></SolidColorBrush> <SolidColorBrush Color="{DynamicResource SystemAccentColorLight1}" Opacity="0.2"></SolidColorBrush>
</shared:SelectionRectangle.Background> </shared:SelectionRectangle.Background>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Artemis.Core; using Artemis.Core;
@ -79,6 +80,12 @@ public class NodeScriptViewModel : ActivatableViewModelBase
.Bind(out ReadOnlyObservableCollection<CableViewModel> cableViewModels) .Bind(out ReadOnlyObservableCollection<CableViewModel> cableViewModels)
.Subscribe(); .Subscribe();
CableViewModels = cableViewModels; 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; } public NodeScript NodeScript { get; }
@ -91,12 +98,18 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public bool IsPreview { get; } 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 public DragCableViewModel? DragViewModel
{ {
get => _dragViewModel; get => _dragViewModel;
set => RaiseAndSetIfChanged(ref _dragViewModel, value); set => RaiseAndSetIfChanged(ref _dragViewModel, value);
} }
public Matrix PanMatrix public Matrix PanMatrix
{ {
get => _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) private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)
{ {
_nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); _nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
@ -235,6 +285,6 @@ public class NodeScriptViewModel : ActivatableViewModelBase
{ {
AutoFitRequested?.Invoke(this, EventArgs.Empty); AutoFitRequested?.Invoke(this, EventArgs.Empty);
} }
public event EventHandler? AutoFitRequested; public event EventHandler? AutoFitRequested;
} }

View File

@ -4,7 +4,7 @@ using Artemis.VisualScripting.Nodes.Operators.Screens;
namespace Artemis.VisualScripting.Nodes.Operators; 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))] [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") 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) if (InputPin.Value == null)
OutputPin.Value = false; OutputPin.Value = false;
else 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"> x:DataType="screens:EnumEqualsNodeCustomViewModel">
<ComboBox IsEnabled="{CompiledBinding EnumValues.Count}" <ComboBox IsEnabled="{CompiledBinding EnumValues.Count}"
Items="{CompiledBinding EnumValues}" Items="{CompiledBinding EnumValues}"
SelectedIndex="{CompiledBinding CurrentValue}" SelectedItem="{CompiledBinding CurrentValue}"
VirtualizationMode="Simple"
PlaceholderText="Select a value" PlaceholderText="Select a value"
Classes="condensed" 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> </UserControl>

View File

@ -6,7 +6,10 @@ using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting; using Artemis.UI.Shared.VisualScripting;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using FluentAvalonia.Core;
using Humanizer;
using ReactiveUI; using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Operators.Screens; namespace Artemis.VisualScripting.Nodes.Operators.Screens;
@ -28,13 +31,11 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
{ {
EnumValues.Clear(); EnumValues.Clear();
if (_node.InputPin.ConnectedTo.First().Type.IsEnum) if (_node.InputPin.ConnectedTo.First().Type.IsEnum)
EnumValues.AddRange(Enum.GetValues(_node.InputPin.ConnectedTo.First().Type).Cast<Enum>()); AddEnumValues(_node.InputPin.ConnectedTo.First().Type);
this.RaisePropertyChanged(nameof(CurrentValue));
} }
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinConnected += x, x => _node.InputPin.PinConnected -= x) 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); .DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x) Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x)
.Subscribe(_ => EnumValues.Clear()) .Subscribe(_ => EnumValues.Clear())
@ -42,15 +43,29 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
}); });
} }
public ObservableCollection<Enum> EnumValues { get; } = new(); private void AddEnumValues(Type type)
public int CurrentValue
{ {
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 set
{ {
if (!Equals(_node.Storage, value)) if (!Equals(_node.Storage, value.Item1))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<int>(_node, value)); _nodeEditorService.ExecuteCommand(Script, new UpdateStorage<long>(_node, value.Item1));
} }
} }
} }

View File

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