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

Node editor - Added copy/pasting

This commit is contained in:
Robert 2022-10-04 21:33:11 +02:00 committed by RobertBeekman
parent 2c32abaee5
commit 35b593a31d
11 changed files with 191 additions and 26 deletions

View File

@ -4,6 +4,22 @@ namespace Artemis.Storage.Entities.Profile.Nodes;
public class NodeConnectionEntity
{
public NodeConnectionEntity()
{
}
public NodeConnectionEntity(NodeConnectionEntity nodeConnectionEntity)
{
SourceType = nodeConnectionEntity.SourceType;
SourceNode = nodeConnectionEntity.SourceNode;
TargetNode = nodeConnectionEntity.TargetNode;
SourcePinCollectionId = nodeConnectionEntity.SourcePinCollectionId;
SourcePinId = nodeConnectionEntity.SourcePinId;
TargetType = nodeConnectionEntity.TargetType;
TargetPinCollectionId = nodeConnectionEntity.TargetPinCollectionId;
TargetPinId = nodeConnectionEntity.TargetPinId;
}
public string SourceType { get; set; }
public Guid SourceNode { get; set; }
public Guid TargetNode { get; set; }

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.Storage.Entities.Profile.Nodes;
@ -10,6 +11,22 @@ public class NodeEntity
PinCollections = new List<NodePinCollectionEntity>();
}
public NodeEntity(NodeEntity nodeEntity)
{
Id = nodeEntity.Id;
Type = nodeEntity.Type;
PluginId = nodeEntity.PluginId;
Name = nodeEntity.Name;
Description = nodeEntity.Description;
IsExitNode = nodeEntity.IsExitNode;
X = nodeEntity.X;
Y = nodeEntity.Y;
Storage = nodeEntity.Storage;
PinCollections = nodeEntity.PinCollections.Select(p => new NodePinCollectionEntity(p)).ToList();
}
public Guid Id { get; set; }
public string Type { get; set; }
public Guid PluginId { get; set; }

View File

@ -2,6 +2,17 @@
public class NodePinCollectionEntity
{
public NodePinCollectionEntity()
{
}
public NodePinCollectionEntity(NodePinCollectionEntity nodePinCollectionEntity)
{
Id = nodePinCollectionEntity.Id;
Direction = nodePinCollectionEntity.Direction;
Amount = nodePinCollectionEntity.Amount;
}
public int Id { get; set; }
public int Direction { set; get; }
public int Amount { get; set; }

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
@ -36,7 +37,8 @@ public class AddNode : INodeEditorCommand, IDisposable
/// <inheritdoc />
public void Execute()
{
_nodeScript.AddNode(_node);
if (!_nodeScript.Nodes.Contains(_node))
_nodeScript.AddNode(_node);
_isRemoved = false;
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
using Artemis.Storage.Entities.Profile.Nodes;
using FluentAvalonia.Core;
namespace Artemis.UI.Models;
public class NodesClipboardModel
{
public NodesClipboardModel(NodeScript nodeScript, List<INode> nodes)
{
nodeScript.Save();
// Grab all entities belonging to provided nodes
Nodes = nodeScript.Entity.Nodes.Where(e => nodes.Any(n => n.Id == e.Id)).ToList();
// Grab all connections between provided nodes
Connections = nodeScript.Entity.Connections.Where(e => nodes.Any(n => n.Id == e.SourceNode) && nodes.Any(n => n.Id == e.TargetNode)).ToList();
}
public NodesClipboardModel()
{
Nodes = new List<NodeEntity>();
Connections = new List<NodeConnectionEntity>();
}
public List<NodeEntity> Nodes { get; set; }
public List<NodeConnectionEntity> Connections { get; set; }
public List<INode> Paste(NodeScript nodeScript, double x, double y)
{
if (!Nodes.Any())
return new List<INode>();
nodeScript.Save();
// Copy the entities, not messing with the originals
List<NodeEntity> nodes = Nodes.Select(n => new NodeEntity(n)).ToList();
List<NodeConnectionEntity> connections = Connections.Select(c => new NodeConnectionEntity(c)).ToList();
double xOffset = x - nodes.Min(n => n.X);
double yOffset = y - nodes.Min(n => n.Y);
foreach (NodeEntity node in nodes)
{
// Give each node a new GUID, updating any connections to it
Guid newGuid = Guid.NewGuid();
foreach (NodeConnectionEntity connection in connections)
{
if (connection.SourceNode == node.Id)
connection.SourceNode = newGuid;
else if (connection.TargetNode == node.Id)
connection.TargetNode = newGuid;
// Only add the connection if this is the first time we hit it
if (!nodeScript.Entity.Connections.Contains(connection))
nodeScript.Entity.Connections.Add(connection);
}
node.Id = newGuid;
node.X += xOffset;
node.Y += yOffset;
nodeScript.Entity.Nodes.Add(node);
}
nodeScript.Load();
// Return the newly created nodes
return nodeScript.Nodes.Where(n => nodes.Any(e => e.Id == n.Id)).ToList();
}
}

View File

@ -24,11 +24,6 @@
Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="4"
StrokeLineCap="Round">
<Path.Transitions>
<Transitions>
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
</Transitions>
</Path.Transitions>
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
@ -47,8 +42,6 @@
BorderThickness="2"
CornerRadius="3"
Padding="4"
Canvas.Left="{CompiledBinding ValuePoint.X}"
Canvas.Top="{CompiledBinding ValuePoint.Y}"
IsVisible="{CompiledBinding DisplayValue}">
<ContentControl Content="{CompiledBinding FromViewModel.Pin.PinValue}">
<ContentControl.DataTemplates>

View File

@ -8,6 +8,7 @@ using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.Rendering;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting;
@ -29,9 +30,9 @@ public class CableView : ReactiveUserControl<CableViewModel>
{
_valueBorder.GetObservable(BoundsProperty).Subscribe(rect => _valueBorder.RenderTransform = new TranslateTransform(rect.Width / 2 * -1, rect.Height / 2 * -1)).DisposeWith(d);
ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d);
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d);
Update();
ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update(true)).DisposeWith(d);
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update(false)).DisposeWith(d);
Update(true);
});
}
@ -40,10 +41,12 @@ public class CableView : ReactiveUserControl<CableViewModel>
AvaloniaXamlLoader.Load(this);
}
private void Update()
private void Update(bool from)
{
// Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748
_cablePath.Margin = _cablePath.Margin != new Thickness(0, 0, 0, 0) ? new Thickness(0, 0, 0, 0) : new Thickness(1, 1, 0, 0);
_cablePath.Margin = new Thickness(_cablePath.Margin.Left + 1, _cablePath.Margin.Top + 1, 0, 0);
if (_cablePath.Margin.Left > 2)
_cablePath.Margin = new Thickness(0, 0, 0, 0);
PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First();
BezierSegment segment = (BezierSegment) pathFigure.Segments!.First();
@ -51,6 +54,11 @@ public class CableView : ReactiveUserControl<CableViewModel>
segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y);
segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y);
segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y);
Canvas.SetLeft(_valueBorder, ViewModel.FromPoint.X + (ViewModel.ToPoint.X - ViewModel.FromPoint.X) / 2);
Canvas.SetTop(_valueBorder, ViewModel.FromPoint.Y + (ViewModel.ToPoint.Y - ViewModel.FromPoint.Y) / 2);
_cablePath.InvalidateVisual();
}
private void OnPointerEnter(object? sender, PointerEventArgs e)

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
@ -27,7 +28,6 @@ public class CableViewModel : ActivatableViewModelBase
private PinViewModel? _fromViewModel;
private ObservableAsPropertyHelper<Point>? _toPoint;
private PinViewModel? _toViewModel;
private ObservableAsPropertyHelper<Point>? _valuePoint;
public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to, ISettingsService settingsService)
{
@ -63,12 +63,7 @@ public class CableViewModel : ActivatableViewModelBase
.Switch()
.ToProperty(this, vm => vm.ToPoint)
.DisposeWith(d);
_valuePoint = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint).Select(tuple => new Point(
tuple.Item1.X + (tuple.Item2.X - tuple.Item1.X) / 2,
tuple.Item1.Y + (tuple.Item2.Y - tuple.Item1.Y) / 2
)).ToProperty(this, vm => vm.ValuePoint)
.DisposeWith(d);
// Not a perfect solution but this makes sure the cable never renders at 0,0 (can happen when the cable spawns before the pin ever rendered)
_connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint)
.Select(tuple => tuple.Item1 != new Point(0, 0) && tuple.Item2 != new Point(0, 0))
@ -104,11 +99,10 @@ public class CableViewModel : ActivatableViewModelBase
}
public bool Connected => _connected?.Value ?? false;
public bool IsFirst => _from.ConnectedTo[0] == _to;
public bool IsFirst => _from.ConnectedTo.FirstOrDefault() == _to;
public Point FromPoint => _fromPoint?.Value ?? new Point();
public Point ToPoint => _toPoint?.Value ?? new Point();
public Point ValuePoint => _valuePoint?.Value ?? new Point();
public Color CableColor => _cableColor?.Value ?? new Color(255, 255, 255, 255);
public void UpdateDisplayValue(bool hoveringOver)

View File

@ -30,6 +30,8 @@
<KeyBinding Command="{CompiledBinding ClearSelection}" Gesture="Escape" />
<KeyBinding Command="{CompiledBinding DeleteSelected}" Gesture="Delete" />
<KeyBinding Command="{CompiledBinding DuplicateSelected}" Gesture="Ctrl+D" />
<KeyBinding Command="{CompiledBinding CopySelected}" Gesture="Ctrl+C" />
<KeyBinding Command="{CompiledBinding PasteSelected}" Gesture="Ctrl+V" />
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
</UserControl.KeyBindings>

View File

@ -14,6 +14,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using Avalonia.VisualTree;
using DynamicData.Binding;
using ReactiveUI;
@ -39,6 +40,8 @@ public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
_zoomBorder.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
_zoomBorder.AddHandler(PointerWheelChangedEvent, ZoomOnPointerWheelChanged, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
_zoomBorder.AddHandler(PointerMovedEvent, ZoomOnPointerMoved, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
this.WhenActivated(d =>
{
ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested;
@ -67,6 +70,12 @@ public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
e.Handled = true;
}
private void ZoomOnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel != null)
ViewModel.PastePosition = e.GetPosition(_grid);
}
private void ShowPickerAt(Point point)
{
if (ViewModel == null)

View File

@ -6,9 +6,12 @@ using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Models;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
@ -16,6 +19,7 @@ using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
@ -24,6 +28,8 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase
{
public const string CLIPBOARD_DATA_FORMAT = "Artemis.Nodes";
private readonly INodeEditorService _nodeEditorService;
private readonly INodeService _nodeService;
private readonly SourceList<NodeViewModel> _nodeViewModels;
@ -33,6 +39,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase
private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _initialNodeSelection;
private Matrix _panMatrix;
private Point _pastePosition;
public NodeScriptViewModel(NodeScript nodeScript, bool isPreview, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService)
{
@ -86,8 +93,8 @@ public class NodeScriptViewModel : ActivatableViewModelBase
ClearSelection = ReactiveCommand.Create(ExecuteClearSelection);
DeleteSelected = ReactiveCommand.Create(ExecuteDeleteSelected);
DuplicateSelected = ReactiveCommand.Create(ExecuteDuplicateSelected);
CopySelected = ReactiveCommand.Create(ExecuteCopySelected);
PasteSelected = ReactiveCommand.Create(ExecutePasteSelected);
CopySelected = ReactiveCommand.CreateFromTask(ExecuteCopySelected);
PasteSelected = ReactiveCommand.CreateFromTask(ExecutePasteSelected);
}
public NodeScript NodeScript { get; }
@ -118,6 +125,12 @@ public class NodeScriptViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _panMatrix, value);
}
public Point PastePosition
{
get => _pastePosition;
set => RaiseAndSetIfChanged(ref _pastePosition, value);
}
public void DeleteSelectedNodes()
{
List<NodeViewModel> toRemove = NodeViewModels.Where(vm => vm.IsSelected && !vm.Node.IsDefaultNode && !vm.Node.IsExitNode).ToList();
@ -279,11 +292,39 @@ public class NodeScriptViewModel : ActivatableViewModelBase
}
}
private void ExecuteCopySelected()
private async Task ExecuteCopySelected()
{
if (Application.Current?.Clipboard == null)
return;
List<INode> nodes = NodeViewModels.Where(vm => vm.IsSelected).Select(vm => vm.Node).Where(n => !n.IsDefaultNode && !n.IsExitNode).ToList();
DataObject dataObject = new();
string copy = CoreJson.SerializeObject(new NodesClipboardModel(NodeScript, nodes), true);
dataObject.Set(CLIPBOARD_DATA_FORMAT, copy);
await Application.Current.Clipboard.SetDataObjectAsync(dataObject);
}
private void ExecutePasteSelected()
private async Task ExecutePasteSelected()
{
if (Application.Current?.Clipboard == null)
return;
byte[]? bytes = (byte[]?) await Application.Current.Clipboard.GetDataAsync(CLIPBOARD_DATA_FORMAT);
if (bytes == null!)
return;
NodesClipboardModel? nodesClipboardModel = CoreJson.DeserializeObject<NodesClipboardModel>(Encoding.Unicode.GetString(bytes), true);
if (nodesClipboardModel == null)
return;
List<INode> nodes = nodesClipboardModel.Paste(NodeScript, PastePosition.X, PastePosition.Y);
using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, "Paste nodes");
foreach (INode node in nodes)
_nodeEditorService.ExecuteCommand(NodeScript, new AddNode(NodeScript, node));
// Select only the new nodes
foreach (NodeViewModel nodeViewModel in NodeViewModels)
nodeViewModel.IsSelected = nodes.Contains(nodeViewModel.Node);
}
}