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:
parent
2c32abaee5
commit
35b593a31d
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
72
src/Artemis.UI/Models/NodesClipboardModel.cs
Normal file
72
src/Artemis.UI/Models/NodesClipboardModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user