diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 92ee2d1f1..c0482363a 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -43,6 +43,10 @@ namespace Artemis.Core.Services if (match != null) return match; + // Objects represent an input that can take any type, these are hardcoded white + if (type == typeof(object)) + return new TypeColorRegistration(type, new SKColor(255, 255, 255, 255), Constants.CorePlugin); + // Come up with a random color based on the type name that should be the same each time MD5 md5Hasher = MD5.Create(); byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!)); diff --git a/src/Artemis.Core/Utilities/Numeric.cs b/src/Artemis.Core/Utilities/Numeric.cs index 86bec89b8..ac779d557 100644 --- a/src/Artemis.Core/Utilities/Numeric.cs +++ b/src/Artemis.Core/Utilities/Numeric.cs @@ -11,7 +11,7 @@ namespace Artemis.Core /// Usage outside that context is not recommended due to conversion overhead. /// /// - public readonly struct Numeric : IComparable + public readonly struct Numeric : IComparable, IConvertible { private readonly float _value; @@ -140,6 +140,11 @@ namespace Artemis.Core return p._value; } + public static implicit operator decimal(Numeric p) + { + return (decimal) p._value; + } + public static implicit operator byte(Numeric p) { return (byte) Math.Clamp(p._value, 0, 255); @@ -260,6 +265,112 @@ namespace Artemis.Core type == typeof(int) || type == typeof(byte); } + + #region Implementation of IConvertible + + /// + public TypeCode GetTypeCode() + { + return _value.GetTypeCode(); + } + + /// + public bool ToBoolean(IFormatProvider? provider) + { + return Convert.ToBoolean(_value); + } + + /// + public byte ToByte(IFormatProvider? provider) + { + return (byte) Math.Clamp(_value, 0, 255); + } + + /// + public char ToChar(IFormatProvider? provider) + { + return Convert.ToChar(_value); + } + + /// + public DateTime ToDateTime(IFormatProvider? provider) + { + return Convert.ToDateTime(_value); + } + + /// + public decimal ToDecimal(IFormatProvider? provider) + { + return (decimal) _value; + } + + /// + public double ToDouble(IFormatProvider? provider) + { + return _value; + } + + /// + public short ToInt16(IFormatProvider? provider) + { + return (short) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public int ToInt32(IFormatProvider? provider) + { + return (int) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public long ToInt64(IFormatProvider? provider) + { + return (long) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public sbyte ToSByte(IFormatProvider? provider) + { + return (sbyte) Math.Clamp(_value, 0, 255); + } + + /// + public float ToSingle(IFormatProvider? provider) + { + return _value; + } + + /// + public string ToString(IFormatProvider? provider) + { + return _value.ToString(provider); + } + + /// + public object ToType(Type conversionType, IFormatProvider? provider) + { + return Convert.ChangeType(_value, conversionType); + } + + /// + public ushort ToUInt16(IFormatProvider? provider) + { + return (ushort) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public uint ToUInt32(IFormatProvider? provider) + { + return (uint) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public ulong ToUInt64(IFormatProvider? provider) + { + return (ulong) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + #endregion } /// @@ -279,7 +390,8 @@ namespace Artemis.Core if (source == null) throw new ArgumentNullException(nameof(source)); float sum = 0; - foreach (float v in source) sum += v; + foreach (float v in source) + sum += v; return new Numeric(sum); } diff --git a/src/Artemis.Core/VisualScripting/InputPinCollection.cs b/src/Artemis.Core/VisualScripting/InputPinCollection.cs index 35fa7197f..fd4da2bf0 100644 --- a/src/Artemis.Core/VisualScripting/InputPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/InputPinCollection.cs @@ -14,8 +14,11 @@ namespace Artemis.Core #region Constructors internal InputPinCollection(INode node, string name, int initialCount) - : base(node, name, initialCount) + : base(node, name) { + // Can't do this in the base constructor because the type won't be set yet + for (int i = 0; i < initialCount; i++) + Add(CreatePin()); } #endregion @@ -23,7 +26,7 @@ namespace Artemis.Core #region Methods /// - protected override IPin CreatePin() + public override IPin CreatePin() { return new InputPin(Node, string.Empty); } @@ -59,13 +62,13 @@ namespace Artemis.Core #region Constructors internal InputPinCollection(INode node, Type type, string name, int initialCount) - : base(node, name, 0) + : base(node, name) { Type = type; // Can't do this in the base constructor because the type won't be set yet for (int i = 0; i < initialCount; i++) - AddPin(); + Add(CreatePin()); } #endregion @@ -73,7 +76,7 @@ namespace Artemis.Core #region Methods /// - protected override IPin CreatePin() + public override IPin CreatePin() { return new InputPin(Node, Type, string.Empty); } diff --git a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs index bcbbf55e0..5bca752f7 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs @@ -40,10 +40,15 @@ namespace Artemis.Core event EventHandler> PinRemoved; /// - /// Creates a new pin and adds it to the collection + /// Creates a new pin compatible with this collection /// - /// The newly added pin - IPin AddPin(); + /// The newly created pin + IPin CreatePin(); + + /// + /// Adds the provided to the collection + /// + void Add(IPin pin); /// /// Removes the provided from the collection diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 263d995e8..f67476ceb 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -199,7 +199,7 @@ namespace Artemis.Core while (collection.Count() > entityNodePinCollection.Amount) collection.Remove(collection.Last()); while (collection.Count() < entityNodePinCollection.Amount) - collection.AddPin(); + collection.Add(collection.CreatePin()); } return node; diff --git a/src/Artemis.Core/VisualScripting/OutputPinCollection.cs b/src/Artemis.Core/VisualScripting/OutputPinCollection.cs index 465b37ae0..c479367d3 100644 --- a/src/Artemis.Core/VisualScripting/OutputPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/OutputPinCollection.cs @@ -20,15 +20,19 @@ namespace Artemis.Core #region Constructors internal OutputPinCollection(INode node, string name, int initialCount) - : base(node, name, initialCount) - { } + : base(node, name) + { + // Can't do this in the base constructor because the type won't be set yet + for (int i = 0; i < initialCount; i++) + Add(CreatePin()); + } #endregion #region Methods /// - protected override IPin CreatePin() => new OutputPin(Node, string.Empty); + public override IPin CreatePin() => new OutputPin(Node, string.Empty); #endregion } diff --git a/src/Artemis.Core/VisualScripting/PinCollection.cs b/src/Artemis.Core/VisualScripting/PinCollection.cs index 04c781823..5c060d884 100644 --- a/src/Artemis.Core/VisualScripting/PinCollection.cs +++ b/src/Artemis.Core/VisualScripting/PinCollection.cs @@ -16,15 +16,11 @@ namespace Artemis.Core /// /// The node the pin collection belongs to /// The name of the pin collection - /// The amount of pins to initially add to the collection /// The resulting output pin collection - protected PinCollection(INode node, string name, int initialCount) + protected PinCollection(INode node, string name) { Node = node ?? throw new ArgumentNullException(nameof(node)); Name = name; - - for (int i = 0; i < initialCount; i++) - AddPin(); } #endregion @@ -61,14 +57,18 @@ namespace Artemis.Core #region Methods /// - public IPin AddPin() + public void Add(IPin pin) { - IPin pin = CreatePin(); + if (pin.Direction != Direction) + throw new ArtemisCoreException($"Can't add a {pin.Direction} pin to an {Direction} pin collection."); + if (pin.Type != Type) + throw new ArtemisCoreException($"Can't add a {pin.Type} pin to an {Type} pin collection."); + + if (_pins.Contains(pin)) + return; + _pins.Add(pin); - PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); - - return pin; } /// @@ -89,11 +89,8 @@ namespace Artemis.Core pin.Reset(); } - /// - /// Creates a new pin to be used in this collection - /// - /// The resulting pin - protected abstract IPin CreatePin(); + /// + public abstract IPin CreatePin(); /// public IEnumerator GetEnumerator() diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs index 3de441b2f..26c18bcff 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs @@ -70,7 +70,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper Node.Script.OnScriptUpdated(); } - public void AddPin() => PinCollection.AddPin(); + public void AddPin() => PinCollection.Add(PinCollection.CreatePin()); public void RemovePin(VisualScriptPin pin) => PinCollection.Remove(pin.Pin); @@ -83,4 +83,4 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper #endregion } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 09e102ae0..8094f131e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -62,6 +62,7 @@ public class SelectionRectangle : Control private bool _isSelecting; private IControl? _oldInputElement; private Point _startPosition; + private Point _lastPosition; /// public SelectionRectangle() @@ -168,9 +169,16 @@ public class SelectionRectangle : Control private void ParentOnPointerMoved(object? sender, PointerEventArgs e) { + // Point moved seems to trigger when the element under the mouse changes? + // I'm not sure why this is needed but this check makes sure the position really hasn't changed. + Point position = e.GetCurrentPoint(null).Position; + if (position == _lastPosition) + return; + _lastPosition = position; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; - + // Capture the pointer and initialize dragging the first time it moves if (!ReferenceEquals(e.Pointer.Captured, this)) { diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs index 2ac132a0f..45b4a9b59 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs @@ -13,7 +13,7 @@ public class AddNode : INodeEditorCommand, IDisposable private bool _isRemoved; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The node script the node belongs to. /// The node to delete. diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs new file mode 100644 index 000000000..f6fc2d4f1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs @@ -0,0 +1,38 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to add a pin to a pin collection. +/// +public class AddPin : INodeEditorCommand +{ + private readonly IPinCollection _pinCollection; + private IPin? _pin; + + /// + /// Creates a new instance of the class. + /// + /// The pin collection to add the pin to. + public AddPin(IPinCollection pinCollection) + { + _pinCollection = pinCollection; + } + + /// + public string DisplayName => "Add pin"; + + /// + public void Execute() + { + _pin ??= _pinCollection.CreatePin(); + _pinCollection.Add(_pin); + } + + /// + public void Undo() + { + if (_pin != null) + _pinCollection.Remove(_pin); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs index 7a02da355..6d71769ee 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs @@ -1,14 +1,22 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Artemis.Core; -using Artemis.UI.Shared.Services.NodeEditor; +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to connect two pins. +/// public class ConnectPins : INodeEditorCommand { private readonly IPin _source; private readonly IPin _target; private readonly List? _originalConnections; + /// + /// Creates a new instance of the class. + /// + /// The source of the connection. + /// The target of the connection. public ConnectPins(IPin source, IPin target) { _source = source; diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs index 6c1577427..5886c5993 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs @@ -11,11 +11,11 @@ public class DeleteNode : INodeEditorCommand, IDisposable { private readonly INode _node; private readonly INodeScript _nodeScript; - private readonly Dictionary> _pinConnections = new(); + private readonly Dictionary> _pinConnections = new(); private bool _isRemoved; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The node script the node belongs to. /// The node to delete. @@ -30,7 +30,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable _pinConnections.Clear(); foreach (IPin nodePin in _node.Pins) { - _pinConnections.Add(nodePin, nodePin.ConnectedTo); + _pinConnections.Add(nodePin, new List(nodePin.ConnectedTo)); nodePin.DisconnectAll(); } @@ -38,7 +38,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable { foreach (IPin nodePin in nodePinCollection) { - _pinConnections.Add(nodePin, nodePin.ConnectedTo); + _pinConnections.Add(nodePin, new List(nodePin.ConnectedTo)); nodePin.DisconnectAll(); } } @@ -48,18 +48,22 @@ public class DeleteNode : INodeEditorCommand, IDisposable { foreach (IPin nodePin in _node.Pins) { - if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections)) + if (_pinConnections.TryGetValue(nodePin, out List? connections)) + { foreach (IPin connection in connections) nodePin.ConnectTo(connection); + } } foreach (IPinCollection nodePinCollection in _node.PinCollections) { foreach (IPin nodePin in nodePinCollection) { - if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections)) + if (_pinConnections.TryGetValue(nodePin, out List? connections)) + { foreach (IPin connection in connections) nodePin.ConnectTo(connection); + } } } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs new file mode 100644 index 000000000..9e4906fe1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to remove a pin from a pin collection. +/// +public class RemovePin : INodeEditorCommand +{ + private readonly IPinCollection _pinCollection; + private readonly IPin _pin; + private readonly List _originalConnections; + + /// + /// Creates a new instance of the class. + /// + /// The pin collection to add the pin to. + /// The pin to remove. + public RemovePin(IPinCollection pinCollection, IPin pin) + { + if (!pinCollection.Contains(pin)) + throw new ArtemisSharedUIException("Can't remove a pin from a collection it isn't contained in."); + + _pinCollection = pinCollection; + _pin = pin; + + _originalConnections = new List(_pin.ConnectedTo); + } + + /// + public string DisplayName => "Remove pin"; + + /// + public void Execute() + { + _pin.DisconnectAll(); + _pinCollection.Remove(_pin); + } + + /// + public void Undo() + { + _pinCollection.Add(_pin); + foreach (IPin originalConnection in _originalConnections) + _pin.ConnectTo(originalConnection); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml index 012270a2a..51a728924 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml @@ -59,6 +59,19 @@ + + + + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 4bada05d2..e1cde58ae 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -101,9 +101,9 @@ namespace Artemis.UI.Ninject.Factories public interface INodePinVmFactory { - PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection); + PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); PinViewModel InputPinViewModel(IPin inputPin); - PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection); + PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel); PinViewModel OutputPinViewModel(IPin outputPin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index c60dda5ff..b42c97c1d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -4,32 +4,78 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:skiaSharp="clr-namespace:SkiaSharp;assembly=SkiaSharp" + xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.CableView" x:DataType="visualScripting:CableViewModel" - ClipToBounds="False"> + ClipToBounds="False" + IsVisible="{CompiledBinding Connected}"> + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index c8f92e883..4d29600d2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -15,15 +15,19 @@ public class CableView : ReactiveUserControl { private const double CABLE_OFFSET = 24 * 4; private readonly Path _cablePath; + private readonly Border _valueBorder; public CableView() { InitializeComponent(); _cablePath = this.Get("CablePath"); + _valueBorder = this.Get("ValueBorder"); // Not using bindings here to avoid a warnings this.WhenActivated(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.ToPoint).Subscribe(_ => Update()).DisposeWith(d); Update(); diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 38991d633..2a872e8c0 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -15,13 +15,15 @@ namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { + private readonly ObservableAsPropertyHelper _connected; private readonly ObservableAsPropertyHelper _cableColor; private readonly ObservableAsPropertyHelper _fromPoint; private readonly ObservableAsPropertyHelper _toPoint; + private readonly ObservableAsPropertyHelper _valuePoint; private PinViewModel? _fromViewModel; private PinViewModel? _toViewModel; - + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) { if (from.Direction != PinDirection.Output) @@ -44,10 +46,19 @@ public class CableViewModel : ActivatableViewModelBase .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) .Switch() .ToProperty(this, vm => vm.ToPoint); + _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); _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel) .Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)) .ToProperty(this, vm => vm.CableColor); + + // 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)) + .ToProperty(this, vm => vm.Connected); } public PinViewModel? FromViewModel @@ -62,7 +73,10 @@ public class CableViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _toViewModel, value); } + public bool Connected => _connected.Value; + public Point FromPoint => _fromPoint.Value; public Point ToPoint => _toPoint.Value; + public Point ValuePoint => _valuePoint.Value; public Color CableColor => _cableColor.Value; } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 37aa77d57..ce07fef19 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -10,13 +10,15 @@ - - - - - - - + PointerMoved="InputElement_OnPointerMoved" + ClipToBounds="True" + Background="{DynamicResource ContentDialogBackground}"> + + + + + + + + - + - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 144b76d5a..9cb3dab1e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Reactive; using System.Reactive.Linq; using Artemis.Core; @@ -25,10 +26,13 @@ public class NodeViewModel : ActivatableViewModelBase private double _dragOffsetX; private double _dragOffsetY; private bool _isSelected; - private ObservableAsPropertyHelper? _isStaticNode; private double _startX; private double _startY; + private ObservableAsPropertyHelper? _isStaticNode; + private ObservableAsPropertyHelper? _hasInputPins; + private ObservableAsPropertyHelper? _hasOutputPins; + public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { NodeScriptViewModel = nodeScriptViewModel; @@ -57,12 +61,12 @@ public class NodeViewModel : ActivatableViewModelBase // Same again but for pin collections nodePinCollections.Connect() .Filter(n => n.Direction == PinDirection.Input) - .Transform(nodePinVmFactory.InputPinCollectionViewModel) + .Transform(c => nodePinVmFactory.InputPinCollectionViewModel(c, nodeScriptViewModel)) .Bind(out ReadOnlyObservableCollection inputPinCollections) .Subscribe(); nodePinCollections.Connect() .Filter(n => n.Direction == PinDirection.Output) - .Transform(nodePinVmFactory.OutputPinCollectionViewModel) + .Transform(c => nodePinVmFactory.OutputPinCollectionViewModel(c, nodeScriptViewModel)) .Bind(out ReadOnlyObservableCollection outputPinCollections) .Subscribe(); InputPinCollectionViewModels = inputPinCollections; @@ -84,6 +88,16 @@ public class NodeViewModel : ActivatableViewModelBase .Select(tuple => tuple.Item1 || tuple.Item2) .ToProperty(this, model => model.IsStaticNode) .DisposeWith(d); + _hasInputPins = InputPinViewModels.ToObservableChangeSet() + .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Any() + .ToProperty(this, vm => vm.HasInputPins) + .DisposeWith(d); + _hasOutputPins = OutputPinViewModels.ToObservableChangeSet() + .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Any() + .ToProperty(this, vm => vm.HasOutputPins) + .DisposeWith(d); // Subscribe to pin changes Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source => @@ -97,10 +111,15 @@ public class NodeViewModel : ActivatableViewModelBase source.Clear(); source.AddRange(c); })).DisposeWith(d); + + if (Node is Node coreNode) + CustomNodeViewModel = coreNode.GetCustomViewModel(); }); } public bool IsStaticNode => _isStaticNode?.Value ?? true; + public bool HasInputPins => _hasInputPins?.Value ?? false; + public bool HasOutputPins => _hasOutputPins?.Value ?? false; public NodeScriptViewModel NodeScriptViewModel { get; } public INode Node { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml index a6a0f7e98..2e313b667 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml @@ -13,7 +13,20 @@ Command="{CompiledBinding AddPin}"> - + + + + + + + + + + - diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs index 0edea5c81..cbaf8145a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -7,7 +8,8 @@ public class InputPinCollectionViewModel : PinCollectionViewModel { public InputPinCollection InputPinCollection { get; } - public InputPinCollectionViewModel(InputPinCollection inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory) + public InputPinCollectionViewModel(InputPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) + : base(inputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService) { InputPinCollection = inputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml index 091246cd8..43d3bfbde 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml @@ -14,6 +14,20 @@ Command="{CompiledBinding AddPin}"> - + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs index c268a3bfd..6a9d86b8e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -7,7 +8,8 @@ public class OutputPinCollectionViewModel : PinCollectionViewModel { public OutputPinCollection OutputPinCollection { get; } - public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory) + public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) + : base(outputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService) { OutputPinCollection = outputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs index e521ecc8e..44b199fa2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs @@ -7,6 +7,8 @@ using Artemis.Core; using Artemis.Core.Events; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia.Controls.Mixins; using DynamicData; using ReactiveUI; @@ -16,10 +18,8 @@ namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinCollectionViewModel : ActivatableViewModelBase { private readonly INodePinVmFactory _nodePinVmFactory; - public IPinCollection PinCollection { get; } - public ObservableCollection PinViewModels { get; } - protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory) + protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { _nodePinVmFactory = nodePinVmFactory; @@ -39,13 +39,20 @@ public abstract class PinCollectionViewModel : ActivatableViewModelBase .DisposeWith(d); }); - AddPin = ReactiveCommand.Create(() => PinCollection.AddPin()); + AddPin = ReactiveCommand.Create(() => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new AddPin(pinCollection))); + RemovePin = ReactiveCommand.Create((IPin pin) => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new RemovePin(pinCollection, pin))); } - public ReactiveCommand AddPin { get; } + public IPinCollection PinCollection { get; } + public ReactiveCommand AddPin { get; } + public ReactiveCommand RemovePin { get; } + + public ObservableCollection PinViewModels { get; } private PinViewModel CreatePinViewModel(IPin pin) { - return PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); + PinViewModel vm = PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); + vm.RemovePin = RemovePin; + return vm; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs index 099936779..5d7cb3bb5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs @@ -78,12 +78,12 @@ public class PinView : ReactiveUserControl private void UpdatePosition() { - if (_container == null || ViewModel == null) + if (_container == null || _pinPoint == null || ViewModel == null) return; - Matrix? transform = this.TransformToVisual(_container); + Matrix? transform = _pinPoint.TransformToVisual(_container); if (transform != null) - ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + ViewModel.Position = new Point(_pinPoint.Bounds.Width / 2, _pinPoint.Bounds.Height / 2).Transform(transform.Value); } #endregion diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index ba6ad4d4e..5133fef3c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; @@ -16,6 +17,7 @@ namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinViewModel : ActivatableViewModelBase { private Point _position; + private ReactiveCommand? _removePin; protected PinViewModel(IPin pin, INodeService nodeService) { @@ -52,6 +54,12 @@ public abstract class PinViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _position, value); } + public ReactiveCommand? RemovePin + { + get => _removePin; + set => RaiseAndSetIfChanged(ref _removePin, value); + } + public bool IsCompatibleWith(PinViewModel pinViewModel) { if (pinViewModel.Pin.Direction == Pin.Direction) diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 930f8bdf0..0aab5dd47 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Ninject.Factories; @@ -9,6 +10,7 @@ using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Artemis.VisualScripting.Nodes; using Avalonia.Input; +using Avalonia.Threading; using ReactiveUI; using SkiaSharp; @@ -16,6 +18,8 @@ namespace Artemis.UI.Screens.Workshop { public class WorkshopViewModel : MainScreenViewModel { + private static NodeScript? _testScript = null; + private readonly INotificationService _notificationService; private StandardCursorType _selectedCursor; private readonly ObservableAsPropertyHelper _cursor; @@ -38,16 +42,26 @@ namespace Artemis.UI.Screens.Workshop DisplayName = "Workshop"; ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); - NodeScript testScript = new("Test script", "A test script"); - INode exitNode = testScript.Nodes.Last(); - exitNode.X = 300; - exitNode.Y = 150; + if (_testScript == null) + { + _testScript = new NodeScript("Test script", "A test script"); + INode exitNode = _testScript.Nodes.Last(); + exitNode.X = 300; + exitNode.Y = 150; - OrNode orNode = new() {X = 100, Y = 100}; - testScript.AddNode(orNode); - orNode.Result.ConnectTo(exitNode.Pins.First()); + OrNode orNode = new() {X = 100, Y = 100}; + _testScript.AddNode(orNode); + orNode.Result.ConnectTo(exitNode.Pins.First()); + } - VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(testScript); + VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(_testScript); + + this.WhenActivated(d => + { + DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(20), DispatcherPriority.Normal, (_, _) => _testScript?.Run()); + updateTimer.Start(); + Disposable.Create(() => updateTimer.Stop()).DisposeWith(d); + }); } public NodeScriptViewModel VisualEditorViewModel { get; } diff --git a/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs new file mode 100644 index 000000000..4673f2912 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using Artemis.Core; +using Avalonia.Data.Converters; + +namespace Artemis.VisualScripting.Converters; + +/// +/// Converts input into . +/// +public class NumericConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(Numeric)) + return new Numeric(value); + + return value; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(Numeric)) + return new Numeric(value); + + return value; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml deleted file mode 100644 index da9a0378f..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml deleted file mode 100644 index 2b526f152..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml new file mode 100644 index 000000000..53052b696 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs new file mode 100644 index 000000000..ea3efe812 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.VisualScripting.Nodes.CustomViews +{ + public partial class StaticNumericValueNodeCustomView : UserControl + { + public StaticNumericValueNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml deleted file mode 100644 index 6c43dfcbf..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml deleted file mode 100644 index 54335410e..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file