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 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 string SourceType { get; set; }
public Guid SourceNode { get; set; } public Guid SourceNode { get; set; }
public Guid TargetNode { get; set; } public Guid TargetNode { get; set; }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.Storage.Entities.Profile.Nodes;
@ -10,6 +11,22 @@ public class NodeEntity
PinCollections = new List<NodePinCollectionEntity>(); 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 Guid Id { get; set; }
public string Type { get; set; } public string Type { get; set; }
public Guid PluginId { get; set; } public Guid PluginId { get; set; }

View File

@ -2,6 +2,17 @@
public class NodePinCollectionEntity 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 Id { get; set; }
public int Direction { set; get; } public int Direction { set; get; }
public int Amount { get; set; } public int Amount { get; set; }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Artemis.Core; using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands; namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
@ -36,7 +37,8 @@ public class AddNode : INodeEditorCommand, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public void Execute() public void Execute()
{ {
_nodeScript.AddNode(_node); if (!_nodeScript.Nodes.Contains(_node))
_nodeScript.AddNode(_node);
_isRemoved = false; _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}}" Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="4" StrokeThickness="4"
StrokeLineCap="Round"> StrokeLineCap="Round">
<Path.Transitions>
<Transitions>
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
</Transitions>
</Path.Transitions>
<Path.Data> <Path.Data>
<PathGeometry> <PathGeometry>
<PathGeometry.Figures> <PathGeometry.Figures>
@ -47,8 +42,6 @@
BorderThickness="2" BorderThickness="2"
CornerRadius="3" CornerRadius="3"
Padding="4" Padding="4"
Canvas.Left="{CompiledBinding ValuePoint.X}"
Canvas.Top="{CompiledBinding ValuePoint.Y}"
IsVisible="{CompiledBinding DisplayValue}"> IsVisible="{CompiledBinding DisplayValue}">
<ContentControl Content="{CompiledBinding FromViewModel.Pin.PinValue}"> <ContentControl Content="{CompiledBinding FromViewModel.Pin.PinValue}">
<ContentControl.DataTemplates> <ContentControl.DataTemplates>

View File

@ -8,6 +8,7 @@ using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Rendering;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting; 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); _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.FromPoint).Subscribe(_ => Update(true)).DisposeWith(d);
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update(false)).DisposeWith(d);
Update(); Update(true);
}); });
} }
@ -40,10 +41,12 @@ public class CableView : ReactiveUserControl<CableViewModel>
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void Update() private void Update(bool from)
{ {
// Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 // 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(); PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First();
BezierSegment segment = (BezierSegment) pathFigure.Segments!.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.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y);
segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y); segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y);
segment.Point3 = new Point(ViewModel.ToPoint.X, 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) private void OnPointerEnter(object? sender, PointerEventArgs e)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
@ -27,7 +28,6 @@ public class CableViewModel : ActivatableViewModelBase
private PinViewModel? _fromViewModel; private PinViewModel? _fromViewModel;
private ObservableAsPropertyHelper<Point>? _toPoint; private ObservableAsPropertyHelper<Point>? _toPoint;
private PinViewModel? _toViewModel; private PinViewModel? _toViewModel;
private ObservableAsPropertyHelper<Point>? _valuePoint;
public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to, ISettingsService settingsService) public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to, ISettingsService settingsService)
{ {
@ -63,11 +63,6 @@ public class CableViewModel : ActivatableViewModelBase
.Switch() .Switch()
.ToProperty(this, vm => vm.ToPoint) .ToProperty(this, vm => vm.ToPoint)
.DisposeWith(d); .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) // 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) _connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint)
@ -104,11 +99,10 @@ public class CableViewModel : ActivatableViewModelBase
} }
public bool Connected => _connected?.Value ?? false; 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 FromPoint => _fromPoint?.Value ?? new Point();
public Point ToPoint => _toPoint?.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 Color CableColor => _cableColor?.Value ?? new Color(255, 255, 255, 255);
public void UpdateDisplayValue(bool hoveringOver) public void UpdateDisplayValue(bool hoveringOver)

View File

@ -30,6 +30,8 @@
<KeyBinding Command="{CompiledBinding ClearSelection}" Gesture="Escape" /> <KeyBinding Command="{CompiledBinding ClearSelection}" Gesture="Escape" />
<KeyBinding Command="{CompiledBinding DeleteSelected}" Gesture="Delete" /> <KeyBinding Command="{CompiledBinding DeleteSelected}" Gesture="Delete" />
<KeyBinding Command="{CompiledBinding DuplicateSelected}" Gesture="Ctrl+D" /> <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.Undo}" Gesture="Ctrl+Z" />
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" /> <KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
</UserControl.KeyBindings> </UserControl.KeyBindings>

View File

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

View File

@ -6,9 +6,12 @@ using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Events; using Artemis.Core.Events;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Models;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -16,6 +19,7 @@ using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia; using Avalonia;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.Input;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
@ -24,6 +28,8 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase public class NodeScriptViewModel : ActivatableViewModelBase
{ {
public const string CLIPBOARD_DATA_FORMAT = "Artemis.Nodes";
private readonly INodeEditorService _nodeEditorService; private readonly INodeEditorService _nodeEditorService;
private readonly INodeService _nodeService; private readonly INodeService _nodeService;
private readonly SourceList<NodeViewModel> _nodeViewModels; private readonly SourceList<NodeViewModel> _nodeViewModels;
@ -33,6 +39,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase
private DragCableViewModel? _dragViewModel; private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _initialNodeSelection; private List<NodeViewModel>? _initialNodeSelection;
private Matrix _panMatrix; private Matrix _panMatrix;
private Point _pastePosition;
public NodeScriptViewModel(NodeScript nodeScript, bool isPreview, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService) public NodeScriptViewModel(NodeScript nodeScript, bool isPreview, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService)
{ {
@ -86,8 +93,8 @@ public class NodeScriptViewModel : ActivatableViewModelBase
ClearSelection = ReactiveCommand.Create(ExecuteClearSelection); ClearSelection = ReactiveCommand.Create(ExecuteClearSelection);
DeleteSelected = ReactiveCommand.Create(ExecuteDeleteSelected); DeleteSelected = ReactiveCommand.Create(ExecuteDeleteSelected);
DuplicateSelected = ReactiveCommand.Create(ExecuteDuplicateSelected); DuplicateSelected = ReactiveCommand.Create(ExecuteDuplicateSelected);
CopySelected = ReactiveCommand.Create(ExecuteCopySelected); CopySelected = ReactiveCommand.CreateFromTask(ExecuteCopySelected);
PasteSelected = ReactiveCommand.Create(ExecutePasteSelected); PasteSelected = ReactiveCommand.CreateFromTask(ExecutePasteSelected);
} }
public NodeScript NodeScript { get; } public NodeScript NodeScript { get; }
@ -118,6 +125,12 @@ public class NodeScriptViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _panMatrix, value); set => RaiseAndSetIfChanged(ref _panMatrix, value);
} }
public Point PastePosition
{
get => _pastePosition;
set => RaiseAndSetIfChanged(ref _pastePosition, value);
}
public void DeleteSelectedNodes() public void DeleteSelectedNodes()
{ {
List<NodeViewModel> toRemove = NodeViewModels.Where(vm => vm.IsSelected && !vm.Node.IsDefaultNode && !vm.Node.IsExitNode).ToList(); 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);
} }
} }