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:
parent
c325a29cc0
commit
041ed8e0a0
@ -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 />
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user