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

Node editor - Added node collection pin add/remove

Node editor - Added first static value node
This commit is contained in:
Robert 2022-03-20 15:54:36 +01:00
parent 2907b86174
commit d9d237e0eb
36 changed files with 575 additions and 188 deletions

View File

@ -43,6 +43,10 @@ namespace Artemis.Core.Services
if (match != null) if (match != null)
return match; 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 // Come up with a random color based on the type name that should be the same each time
MD5 md5Hasher = MD5.Create(); MD5 md5Hasher = MD5.Create();
byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!)); byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!));

View File

@ -11,7 +11,7 @@ namespace Artemis.Core
/// Usage outside that context is not recommended due to conversion overhead. /// Usage outside that context is not recommended due to conversion overhead.
/// </para> /// </para>
/// </summary> /// </summary>
public readonly struct Numeric : IComparable<Numeric> public readonly struct Numeric : IComparable<Numeric>, IConvertible
{ {
private readonly float _value; private readonly float _value;
@ -140,6 +140,11 @@ namespace Artemis.Core
return p._value; return p._value;
} }
public static implicit operator decimal(Numeric p)
{
return (decimal) p._value;
}
public static implicit operator byte(Numeric p) public static implicit operator byte(Numeric p)
{ {
return (byte) Math.Clamp(p._value, 0, 255); return (byte) Math.Clamp(p._value, 0, 255);
@ -260,6 +265,112 @@ namespace Artemis.Core
type == typeof(int) || type == typeof(int) ||
type == typeof(byte); type == typeof(byte);
} }
#region Implementation of IConvertible
/// <inheritdoc />
public TypeCode GetTypeCode()
{
return _value.GetTypeCode();
}
/// <inheritdoc />
public bool ToBoolean(IFormatProvider? provider)
{
return Convert.ToBoolean(_value);
}
/// <inheritdoc />
public byte ToByte(IFormatProvider? provider)
{
return (byte) Math.Clamp(_value, 0, 255);
}
/// <inheritdoc />
public char ToChar(IFormatProvider? provider)
{
return Convert.ToChar(_value);
}
/// <inheritdoc />
public DateTime ToDateTime(IFormatProvider? provider)
{
return Convert.ToDateTime(_value);
}
/// <inheritdoc />
public decimal ToDecimal(IFormatProvider? provider)
{
return (decimal) _value;
}
/// <inheritdoc />
public double ToDouble(IFormatProvider? provider)
{
return _value;
}
/// <inheritdoc />
public short ToInt16(IFormatProvider? provider)
{
return (short) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
public int ToInt32(IFormatProvider? provider)
{
return (int) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
public long ToInt64(IFormatProvider? provider)
{
return (long) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
public sbyte ToSByte(IFormatProvider? provider)
{
return (sbyte) Math.Clamp(_value, 0, 255);
}
/// <inheritdoc />
public float ToSingle(IFormatProvider? provider)
{
return _value;
}
/// <inheritdoc />
public string ToString(IFormatProvider? provider)
{
return _value.ToString(provider);
}
/// <inheritdoc />
public object ToType(Type conversionType, IFormatProvider? provider)
{
return Convert.ChangeType(_value, conversionType);
}
/// <inheritdoc />
public ushort ToUInt16(IFormatProvider? provider)
{
return (ushort) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
public uint ToUInt32(IFormatProvider? provider)
{
return (uint) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
/// <inheritdoc />
public ulong ToUInt64(IFormatProvider? provider)
{
return (ulong) MathF.Round(_value, MidpointRounding.AwayFromZero);
}
#endregion
} }
/// <summary> /// <summary>
@ -279,7 +390,8 @@ namespace Artemis.Core
if (source == null) throw new ArgumentNullException(nameof(source)); if (source == null) throw new ArgumentNullException(nameof(source));
float sum = 0; float sum = 0;
foreach (float v in source) sum += v; foreach (float v in source)
sum += v;
return new Numeric(sum); return new Numeric(sum);
} }

View File

@ -14,8 +14,11 @@ namespace Artemis.Core
#region Constructors #region Constructors
internal InputPinCollection(INode node, string name, int initialCount) 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 #endregion
@ -23,7 +26,7 @@ namespace Artemis.Core
#region Methods #region Methods
/// <inheritdoc /> /// <inheritdoc />
protected override IPin CreatePin() public override IPin CreatePin()
{ {
return new InputPin<T>(Node, string.Empty); return new InputPin<T>(Node, string.Empty);
} }
@ -59,13 +62,13 @@ namespace Artemis.Core
#region Constructors #region Constructors
internal InputPinCollection(INode node, Type type, string name, int initialCount) internal InputPinCollection(INode node, Type type, string name, int initialCount)
: base(node, name, 0) : base(node, name)
{ {
Type = type; Type = type;
// Can't do this in the base constructor because the type won't be set yet // Can't do this in the base constructor because the type won't be set yet
for (int i = 0; i < initialCount; i++) for (int i = 0; i < initialCount; i++)
AddPin(); Add(CreatePin());
} }
#endregion #endregion
@ -73,7 +76,7 @@ namespace Artemis.Core
#region Methods #region Methods
/// <inheritdoc /> /// <inheritdoc />
protected override IPin CreatePin() public override IPin CreatePin()
{ {
return new InputPin(Node, Type, string.Empty); return new InputPin(Node, Type, string.Empty);
} }

View File

@ -40,10 +40,15 @@ namespace Artemis.Core
event EventHandler<SingleValueEventArgs<IPin>> PinRemoved; event EventHandler<SingleValueEventArgs<IPin>> PinRemoved;
/// <summary> /// <summary>
/// Creates a new pin and adds it to the collection /// Creates a new pin compatible with this collection
/// </summary> /// </summary>
/// <returns>The newly added pin</returns> /// <returns>The newly created pin</returns>
IPin AddPin(); IPin CreatePin();
/// <summary>
/// Adds the provided <paramref name="pin" /> to the collection
/// </summary>
void Add(IPin pin);
/// <summary> /// <summary>
/// Removes the provided <paramref name="pin" /> from the collection /// Removes the provided <paramref name="pin" /> from the collection

View File

@ -199,7 +199,7 @@ namespace Artemis.Core
while (collection.Count() > entityNodePinCollection.Amount) while (collection.Count() > entityNodePinCollection.Amount)
collection.Remove(collection.Last()); collection.Remove(collection.Last());
while (collection.Count() < entityNodePinCollection.Amount) while (collection.Count() < entityNodePinCollection.Amount)
collection.AddPin(); collection.Add(collection.CreatePin());
} }
return node; return node;

View File

@ -20,15 +20,19 @@ namespace Artemis.Core
#region Constructors #region Constructors
internal OutputPinCollection(INode node, string name, int initialCount) 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 #endregion
#region Methods #region Methods
/// <inheritdoc /> /// <inheritdoc />
protected override IPin CreatePin() => new OutputPin<T>(Node, string.Empty); public override IPin CreatePin() => new OutputPin<T>(Node, string.Empty);
#endregion #endregion
} }

View File

@ -16,15 +16,11 @@ namespace Artemis.Core
/// </summary> /// </summary>
/// <param name="node">The node the pin collection belongs to</param> /// <param name="node">The node the pin collection belongs to</param>
/// <param name="name">The name of the pin collection</param> /// <param name="name">The name of the pin collection</param>
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
/// <returns>The resulting output pin collection</returns> /// <returns>The resulting output pin collection</returns>
protected PinCollection(INode node, string name, int initialCount) protected PinCollection(INode node, string name)
{ {
Node = node ?? throw new ArgumentNullException(nameof(node)); Node = node ?? throw new ArgumentNullException(nameof(node));
Name = name; Name = name;
for (int i = 0; i < initialCount; i++)
AddPin();
} }
#endregion #endregion
@ -61,14 +57,18 @@ namespace Artemis.Core
#region Methods #region Methods
/// <inheritdoc /> /// <inheritdoc />
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); _pins.Add(pin);
PinAdded?.Invoke(this, new SingleValueEventArgs<IPin>(pin)); PinAdded?.Invoke(this, new SingleValueEventArgs<IPin>(pin));
return pin;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -89,11 +89,8 @@ namespace Artemis.Core
pin.Reset(); pin.Reset();
} }
/// <summary> /// <inheritdoc />
/// Creates a new pin to be used in this collection public abstract IPin CreatePin();
/// </summary>
/// <returns>The resulting pin</returns>
protected abstract IPin CreatePin();
/// <inheritdoc /> /// <inheritdoc />
public IEnumerator<IPin> GetEnumerator() public IEnumerator<IPin> GetEnumerator()

View File

@ -70,7 +70,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper
Node.Script.OnScriptUpdated(); Node.Script.OnScriptUpdated();
} }
public void AddPin() => PinCollection.AddPin(); public void AddPin() => PinCollection.Add(PinCollection.CreatePin());
public void RemovePin(VisualScriptPin pin) => PinCollection.Remove(pin.Pin); public void RemovePin(VisualScriptPin pin) => PinCollection.Remove(pin.Pin);
@ -83,4 +83,4 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper
#endregion #endregion
} }
} }

View File

@ -62,6 +62,7 @@ public class SelectionRectangle : Control
private bool _isSelecting; private bool _isSelecting;
private IControl? _oldInputElement; private IControl? _oldInputElement;
private Point _startPosition; private Point _startPosition;
private Point _lastPosition;
/// <inheritdoc /> /// <inheritdoc />
public SelectionRectangle() public SelectionRectangle()
@ -168,9 +169,16 @@ public class SelectionRectangle : Control
private void ParentOnPointerMoved(object? sender, PointerEventArgs e) 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) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return; return;
// Capture the pointer and initialize dragging the first time it moves // Capture the pointer and initialize dragging the first time it moves
if (!ReferenceEquals(e.Pointer.Captured, this)) if (!ReferenceEquals(e.Pointer.Captured, this))
{ {

View File

@ -13,7 +13,7 @@ public class AddNode : INodeEditorCommand, IDisposable
private bool _isRemoved; private bool _isRemoved;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class. /// Creates a new instance of the <see cref="AddNode" /> class.
/// </summary> /// </summary>
/// <param name="nodeScript">The node script the node belongs to.</param> /// <param name="nodeScript">The node script the node belongs to.</param>
/// <param name="node">The node to delete.</param> /// <param name="node">The node to delete.</param>

View File

@ -0,0 +1,38 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to add a pin to a pin collection.
/// </summary>
public class AddPin : INodeEditorCommand
{
private readonly IPinCollection _pinCollection;
private IPin? _pin;
/// <summary>
/// Creates a new instance of the <see cref="AddPin" /> class.
/// </summary>
/// <param name="pinCollection">The pin collection to add the pin to.</param>
public AddPin(IPinCollection pinCollection)
{
_pinCollection = pinCollection;
}
/// <inheritdoc />
public string DisplayName => "Add pin";
/// <inheritdoc />
public void Execute()
{
_pin ??= _pinCollection.CreatePin();
_pinCollection.Add(_pin);
}
/// <inheritdoc />
public void Undo()
{
if (_pin != null)
_pinCollection.Remove(_pin);
}
}

View File

@ -1,14 +1,22 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to connect two pins.
/// </summary>
public class ConnectPins : INodeEditorCommand public class ConnectPins : INodeEditorCommand
{ {
private readonly IPin _source; private readonly IPin _source;
private readonly IPin _target; private readonly IPin _target;
private readonly List<IPin>? _originalConnections; private readonly List<IPin>? _originalConnections;
/// <summary>
/// Creates a new instance of the <see cref="ConnectPins" /> class.
/// </summary>
/// <param name="source">The source of the connection.</param>
/// <param name="target">The target of the connection.</param>
public ConnectPins(IPin source, IPin target) public ConnectPins(IPin source, IPin target)
{ {
_source = source; _source = source;

View File

@ -11,11 +11,11 @@ public class DeleteNode : INodeEditorCommand, IDisposable
{ {
private readonly INode _node; private readonly INode _node;
private readonly INodeScript _nodeScript; private readonly INodeScript _nodeScript;
private readonly Dictionary<IPin, IReadOnlyList<IPin>> _pinConnections = new(); private readonly Dictionary<IPin, List<IPin>> _pinConnections = new();
private bool _isRemoved; private bool _isRemoved;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="MoveNode" /> class. /// Creates a new instance of the <see cref="DeleteNode" /> class.
/// </summary> /// </summary>
/// <param name="nodeScript">The node script the node belongs to.</param> /// <param name="nodeScript">The node script the node belongs to.</param>
/// <param name="node">The node to delete.</param> /// <param name="node">The node to delete.</param>
@ -30,7 +30,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable
_pinConnections.Clear(); _pinConnections.Clear();
foreach (IPin nodePin in _node.Pins) foreach (IPin nodePin in _node.Pins)
{ {
_pinConnections.Add(nodePin, nodePin.ConnectedTo); _pinConnections.Add(nodePin, new List<IPin>(nodePin.ConnectedTo));
nodePin.DisconnectAll(); nodePin.DisconnectAll();
} }
@ -38,7 +38,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable
{ {
foreach (IPin nodePin in nodePinCollection) foreach (IPin nodePin in nodePinCollection)
{ {
_pinConnections.Add(nodePin, nodePin.ConnectedTo); _pinConnections.Add(nodePin, new List<IPin>(nodePin.ConnectedTo));
nodePin.DisconnectAll(); nodePin.DisconnectAll();
} }
} }
@ -48,18 +48,22 @@ public class DeleteNode : INodeEditorCommand, IDisposable
{ {
foreach (IPin nodePin in _node.Pins) foreach (IPin nodePin in _node.Pins)
{ {
if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList<IPin>? connections)) if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
{
foreach (IPin connection in connections) foreach (IPin connection in connections)
nodePin.ConnectTo(connection); nodePin.ConnectTo(connection);
}
} }
foreach (IPinCollection nodePinCollection in _node.PinCollections) foreach (IPinCollection nodePinCollection in _node.PinCollections)
{ {
foreach (IPin nodePin in nodePinCollection) foreach (IPin nodePin in nodePinCollection)
{ {
if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList<IPin>? connections)) if (_pinConnections.TryGetValue(nodePin, out List<IPin>? connections))
{
foreach (IPin connection in connections) foreach (IPin connection in connections)
nodePin.ConnectTo(connection); nodePin.ConnectTo(connection);
}
} }
} }

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core;
namespace Artemis.UI.Shared.Services.NodeEditor.Commands;
/// <summary>
/// Represents a node editor command that can be used to remove a pin from a pin collection.
/// </summary>
public class RemovePin : INodeEditorCommand
{
private readonly IPinCollection _pinCollection;
private readonly IPin _pin;
private readonly List<IPin> _originalConnections;
/// <summary>
/// Creates a new instance of the <see cref="RemovePin" /> class.
/// </summary>
/// <param name="pinCollection">The pin collection to add the pin to.</param>
/// <param name="pin">The pin to remove.</param>
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<IPin>(_pin.ConnectedTo);
}
/// <inheritdoc />
public string DisplayName => "Remove pin";
/// <inheritdoc />
public void Execute()
{
_pin.DisconnectAll();
_pinCollection.Remove(_pin);
}
/// <inheritdoc />
public void Undo()
{
_pinCollection.Add(_pin);
foreach (IPin originalConnection in _originalConnections)
_pin.ConnectTo(originalConnection);
}
}

View File

@ -59,6 +59,19 @@
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="Transparent" />
</Style> </Style>
<Style Selector=":is(Button).icon-button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppBarButtonBackgroundPointerOver}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AppBarButtonForegroundPointerOver}" />
</Style>
<Style Selector=":is(Button).icon-button:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppBarButtonBackgroundPressed}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AppBarButtonForegroundPressed}" />
</Style>
<Style Selector=":is(Button).icon-button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AppBarButtonBackgroundDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AppBarButtonForegroundDisabled}" />
</Style>
<Style Selector=":is(Button).icon-button-small"> <Style Selector=":is(Button).icon-button-small">
<Setter Property="Padding" Value="4" /> <Setter Property="Padding" Value="4" />
</Style> </Style>

View File

@ -101,9 +101,9 @@ namespace Artemis.UI.Ninject.Factories
public interface INodePinVmFactory public interface INodePinVmFactory
{ {
PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection); PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
PinViewModel InputPinViewModel(IPin inputPin); PinViewModel InputPinViewModel(IPin inputPin);
PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection); PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
PinViewModel OutputPinViewModel(IPin outputPin); PinViewModel OutputPinViewModel(IPin outputPin);
} }
} }

View File

@ -4,32 +4,78 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
xmlns:converters="clr-namespace:Artemis.UI.Converters" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.CableView" x:Class="Artemis.UI.Screens.VisualScripting.CableView"
x:DataType="visualScripting:CableViewModel" x:DataType="visualScripting:CableViewModel"
ClipToBounds="False"> ClipToBounds="False"
IsVisible="{CompiledBinding Connected}">
<UserControl.Resources> <UserControl.Resources>
<converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" /> <converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
<shared:SKColorToStringConverter x:Key="SKColorToStringConverter" />
<shared:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources> </UserControl.Resources>
<Path Name="CablePath" <Canvas>
Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}" <Path Name="CablePath"
StrokeThickness="4" Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeLineCap="Round"> StrokeThickness="4"
<Path.Transitions> StrokeLineCap="Round">
<Transitions> <Path.Transitions>
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition> <Transitions>
</Transitions> <ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
</Path.Transitions> </Transitions>
<Path.Data> </Path.Transitions>
<PathGeometry> <Path.Data>
<PathGeometry.Figures> <PathGeometry>
<PathFigure IsClosed="False"> <PathGeometry.Figures>
<PathFigure.Segments> <PathFigure IsClosed="False">
<BezierSegment /> <PathFigure.Segments>
</PathFigure.Segments> <BezierSegment />
</PathFigure> </PathFigure.Segments>
</PathGeometry.Figures> </PathFigure>
</PathGeometry> </PathGeometry.Figures>
</Path.Data> </PathGeometry>
</Path> </Path.Data>
</Path>
<Border Name="ValueBorder"
Background="{DynamicResource ContentDialogBackground}"
BorderBrush="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
BorderThickness="2"
CornerRadius="3"
Padding="4"
Canvas.Left="{CompiledBinding ValuePoint.X}"
Canvas.Top="{CompiledBinding ValuePoint.Y}">
<ContentControl Content="{CompiledBinding FromViewModel.Pin.PinValue}">
<ContentControl.DataTemplates>
<DataTemplate DataType="skiaSharp:SKColor">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock x:Name="HexDisplay"
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch" />
<Border Margin="5 0 0 0"
VerticalAlignment="Center"
HorizontalAlignment="Right"
BorderThickness="1"
MinWidth="18"
MinHeight="18"
Background="{DynamicResource CheckerboardBrush}"
BorderBrush="{DynamicResource ColorPickerButtonOutline}"
CornerRadius="2">
<Ellipse Stroke="{DynamicResource NormalBorderBrush}">
<Ellipse.Fill>
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}}" />
</Ellipse.Fill>
</Ellipse>
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Border>
</Canvas>
</UserControl> </UserControl>

View File

@ -15,15 +15,19 @@ public class CableView : ReactiveUserControl<CableViewModel>
{ {
private const double CABLE_OFFSET = 24 * 4; private const double CABLE_OFFSET = 24 * 4;
private readonly Path _cablePath; private readonly Path _cablePath;
private readonly Border _valueBorder;
public CableView() public CableView()
{ {
InitializeComponent(); InitializeComponent();
_cablePath = this.Get<Path>("CablePath"); _cablePath = this.Get<Path>("CablePath");
_valueBorder = this.Get<Border>("ValueBorder");
// Not using bindings here to avoid a warnings // Not using bindings here to avoid a warnings
this.WhenActivated(d => 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.FromPoint).Subscribe(_ => Update()).DisposeWith(d);
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d);
Update(); Update();

View File

@ -15,13 +15,15 @@ namespace Artemis.UI.Screens.VisualScripting;
public class CableViewModel : ActivatableViewModelBase public class CableViewModel : ActivatableViewModelBase
{ {
private readonly ObservableAsPropertyHelper<bool> _connected;
private readonly ObservableAsPropertyHelper<Color> _cableColor; private readonly ObservableAsPropertyHelper<Color> _cableColor;
private readonly ObservableAsPropertyHelper<Point> _fromPoint; private readonly ObservableAsPropertyHelper<Point> _fromPoint;
private readonly ObservableAsPropertyHelper<Point> _toPoint; private readonly ObservableAsPropertyHelper<Point> _toPoint;
private readonly ObservableAsPropertyHelper<Point> _valuePoint;
private PinViewModel? _fromViewModel; private PinViewModel? _fromViewModel;
private PinViewModel? _toViewModel; private PinViewModel? _toViewModel;
public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to)
{ {
if (from.Direction != PinDirection.Output) if (from.Direction != PinDirection.Output)
@ -44,10 +46,19 @@ public class CableViewModel : ActivatableViewModelBase
.Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<Point>()) .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<Point>())
.Switch() .Switch()
.ToProperty(this, vm => vm.ToPoint); .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) _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel)
.Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)) .Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255))
.ToProperty(this, vm => vm.CableColor); .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 public PinViewModel? FromViewModel
@ -62,7 +73,10 @@ public class CableViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _toViewModel, value); set => RaiseAndSetIfChanged(ref _toViewModel, value);
} }
public bool Connected => _connected.Value;
public Point FromPoint => _fromPoint.Value; public Point FromPoint => _fromPoint.Value;
public Point ToPoint => _toPoint.Value; public Point ToPoint => _toPoint.Value;
public Point ValuePoint => _valuePoint.Value;
public Color CableColor => _cableColor.Value; public Color CableColor => _cableColor.Value;
} }

View File

@ -10,13 +10,15 @@
<UserControl.Styles> <UserControl.Styles>
<Style Selector="Border.node-container"> <Style Selector="Border.node-container">
<Setter Property="CornerRadius" Value="6" /> <Setter Property="CornerRadius" Value="6" />
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" /> <Setter Property="Background">
<SolidColorBrush Color="{DynamicResource SolidBackgroundFillColorBase}" Opacity="0.75"></SolidColorBrush>
</Setter>
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="Transitions"> <Setter Property="Transitions">
<Setter.Value> <Setter.Value>
<Transitions> <Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.2" Easing="CubicEaseOut"/> <BrushTransition Property="BorderBrush" Duration="0:0:0.2" Easing="CubicEaseOut" />
</Transitions> </Transitions>
</Setter.Value> </Setter.Value>
</Setter> </Setter>
@ -24,46 +26,50 @@
<Style Selector="Border.node-container-selected"> <Style Selector="Border.node-container-selected">
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" /> <Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
</Style> </Style>
<Style Selector="ContentControl#CustomViewModelContainer"> <Style Selector="ContentControl#CustomViewModelContainer">
<Setter Property="Margin" Value="20 0"></Setter> <Setter Property="Margin" Value="20 0"></Setter>
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
<Border Classes="node-container" Classes.node-container-selected="{CompiledBinding IsSelected}"> <Border Classes="node-container" Classes.node-container-selected="{CompiledBinding IsSelected}">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" <Border Grid.Row="0"
Background="{DynamicResource TaskDialogHeaderBackground}"
CornerRadius="6 6 0 0" CornerRadius="6 6 0 0"
Cursor="Hand" Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased" PointerReleased="InputElement_OnPointerReleased"
PointerMoved="InputElement_OnPointerMoved"> PointerMoved="InputElement_OnPointerMoved"
<Grid Classes="node-header" ClipToBounds="True"
VerticalAlignment="Top" Background="{DynamicResource ContentDialogBackground}">
ColumnDefinitions="*,Auto"> <Border Background="{DynamicResource TaskDialogHeaderBackground}">
<TextBlock VerticalAlignment="Center" <Grid Classes="node-header"
TextAlignment="Center" VerticalAlignment="Top"
Margin="5" ColumnDefinitions="*,Auto">
Text="{CompiledBinding Node.Name}" <TextBlock VerticalAlignment="Center"
ToolTip.Tip="{CompiledBinding Node.Description}"> TextAlignment="Center"
</TextBlock> Margin="5"
<Button VerticalAlignment="Center" Text="{CompiledBinding Node.Name}"
Classes="icon-button icon-button-small" ToolTip.Tip="{CompiledBinding Node.Description}">
Grid.Column="1" </TextBlock>
Margin="5" <Button VerticalAlignment="Center"
Command="{CompiledBinding DeleteNode}"> Classes="icon-button icon-button-small"
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon> Grid.Column="1"
</Button> Margin="5"
</Grid> Command="{CompiledBinding DeleteNode}">
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
</Button>
</Grid>
</Border>
</Border> </Border>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="5"> <Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="5">
<StackPanel Grid.Column="0"> <StackPanel Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" /> <ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" /> <ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
</StackPanel> </StackPanel>
<ContentControl Name="CustomViewModelContainer" Grid.Column="1" Content="{CompiledBinding CustomNodeViewModel}" IsVisible="{CompiledBinding CustomNodeViewModel}" /> <ContentControl Grid.Column="1" Name="CustomViewModelContainer" Content="{CompiledBinding CustomNodeViewModel}" IsVisible="{CompiledBinding CustomNodeViewModel}" />
<StackPanel Grid.Column="2"> <StackPanel Grid.Column="2" IsVisible="{CompiledBinding HasInputPins}">
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" /> <ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" /> <ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
</StackPanel> </StackPanel>

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
@ -25,10 +26,13 @@ public class NodeViewModel : ActivatableViewModelBase
private double _dragOffsetX; private double _dragOffsetX;
private double _dragOffsetY; private double _dragOffsetY;
private bool _isSelected; private bool _isSelected;
private ObservableAsPropertyHelper<bool>? _isStaticNode;
private double _startX; private double _startX;
private double _startY; private double _startY;
private ObservableAsPropertyHelper<bool>? _isStaticNode;
private ObservableAsPropertyHelper<bool>? _hasInputPins;
private ObservableAsPropertyHelper<bool>? _hasOutputPins;
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{ {
NodeScriptViewModel = nodeScriptViewModel; NodeScriptViewModel = nodeScriptViewModel;
@ -57,12 +61,12 @@ public class NodeViewModel : ActivatableViewModelBase
// Same again but for pin collections // Same again but for pin collections
nodePinCollections.Connect() nodePinCollections.Connect()
.Filter(n => n.Direction == PinDirection.Input) .Filter(n => n.Direction == PinDirection.Input)
.Transform(nodePinVmFactory.InputPinCollectionViewModel) .Transform(c => nodePinVmFactory.InputPinCollectionViewModel(c, nodeScriptViewModel))
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> inputPinCollections) .Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> inputPinCollections)
.Subscribe(); .Subscribe();
nodePinCollections.Connect() nodePinCollections.Connect()
.Filter(n => n.Direction == PinDirection.Output) .Filter(n => n.Direction == PinDirection.Output)
.Transform(nodePinVmFactory.OutputPinCollectionViewModel) .Transform(c => nodePinVmFactory.OutputPinCollectionViewModel(c, nodeScriptViewModel))
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> outputPinCollections) .Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> outputPinCollections)
.Subscribe(); .Subscribe();
InputPinCollectionViewModels = inputPinCollections; InputPinCollectionViewModels = inputPinCollections;
@ -84,6 +88,16 @@ public class NodeViewModel : ActivatableViewModelBase
.Select(tuple => tuple.Item1 || tuple.Item2) .Select(tuple => tuple.Item1 || tuple.Item2)
.ToProperty(this, model => model.IsStaticNode) .ToProperty(this, model => model.IsStaticNode)
.DisposeWith(d); .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 // Subscribe to pin changes
Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source => Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source =>
@ -97,10 +111,15 @@ public class NodeViewModel : ActivatableViewModelBase
source.Clear(); source.Clear();
source.AddRange(c); source.AddRange(c);
})).DisposeWith(d); })).DisposeWith(d);
if (Node is Node coreNode)
CustomNodeViewModel = coreNode.GetCustomViewModel();
}); });
} }
public bool IsStaticNode => _isStaticNode?.Value ?? true; public bool IsStaticNode => _isStaticNode?.Value ?? true;
public bool HasInputPins => _hasInputPins?.Value ?? false;
public bool HasOutputPins => _hasOutputPins?.Value ?? false;
public NodeScriptViewModel NodeScriptViewModel { get; } public NodeScriptViewModel NodeScriptViewModel { get; }
public INode Node { get; } public INode Node { get; }

View File

@ -13,7 +13,20 @@
Command="{CompiledBinding AddPin}"> Command="{CompiledBinding AddPin}">
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon> <avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
</Button> </Button>
<ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0"></ItemsControl> <ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="pins:PinViewModel">
<StackPanel Orientation="Horizontal">
<ContentControl Content="{CompiledBinding}"></ContentControl>
<Button Classes="icon-button icon-button-small"
ToolTip.Tip="Remove pin"
Command="{CompiledBinding RemovePin}"
CommandParameter="{CompiledBinding Pin}">
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
</Button>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -1,5 +1,6 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.UI.Screens.VisualScripting.Pins; namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -7,7 +8,8 @@ public class InputPinCollectionViewModel<T> : PinCollectionViewModel
{ {
public InputPinCollection<T> InputPinCollection { get; } public InputPinCollection<T> InputPinCollection { get; }
public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory) public InputPinCollectionViewModel(InputPinCollection<T> inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
: base(inputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
{ {
InputPinCollection = inputPinCollection; InputPinCollection = inputPinCollection;
} }

View File

@ -14,6 +14,20 @@
Command="{CompiledBinding AddPin}"> Command="{CompiledBinding AddPin}">
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon> <avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
</Button> </Button>
<ItemsControl Items="{CompiledBinding PinViewModels}" HorizontalAlignment="Right" Margin="4 0"></ItemsControl> <ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0" HorizontalAlignment="Right">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="pins:PinViewModel">
<StackPanel Orientation="Horizontal">
<Button Classes="icon-button icon-button-small"
ToolTip.Tip="Remove pin"
Command="{CompiledBinding RemovePin}"
CommandParameter="{CompiledBinding Pin}">
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
</Button>
<ContentControl Content="{CompiledBinding}"></ContentControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -1,5 +1,6 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.UI.Screens.VisualScripting.Pins; namespace Artemis.UI.Screens.VisualScripting.Pins;
@ -7,7 +8,8 @@ public class OutputPinCollectionViewModel<T> : PinCollectionViewModel
{ {
public OutputPinCollection<T> OutputPinCollection { get; } public OutputPinCollection<T> OutputPinCollection { get; }
public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory) public OutputPinCollectionViewModel(OutputPinCollection<T> outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
: base(outputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService)
{ {
OutputPinCollection = outputPinCollection; OutputPinCollection = outputPinCollection;
} }

View File

@ -7,6 +7,8 @@ using Artemis.Core;
using Artemis.Core.Events; using Artemis.Core.Events;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using DynamicData; using DynamicData;
using ReactiveUI; using ReactiveUI;
@ -16,10 +18,8 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
public abstract class PinCollectionViewModel : ActivatableViewModelBase public abstract class PinCollectionViewModel : ActivatableViewModelBase
{ {
private readonly INodePinVmFactory _nodePinVmFactory; private readonly INodePinVmFactory _nodePinVmFactory;
public IPinCollection PinCollection { get; }
public ObservableCollection<PinViewModel> PinViewModels { get; }
protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory) protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{ {
_nodePinVmFactory = nodePinVmFactory; _nodePinVmFactory = nodePinVmFactory;
@ -39,13 +39,20 @@ public abstract class PinCollectionViewModel : ActivatableViewModelBase
.DisposeWith(d); .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<Unit, IPin> AddPin { get; } public IPinCollection PinCollection { get; }
public ReactiveCommand<Unit, Unit> AddPin { get; }
public ReactiveCommand<IPin, Unit> RemovePin { get; }
public ObservableCollection<PinViewModel> PinViewModels { get; }
private PinViewModel CreatePinViewModel(IPin pin) 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;
} }
} }

View File

@ -78,12 +78,12 @@ public class PinView : ReactiveUserControl<PinViewModel>
private void UpdatePosition() private void UpdatePosition()
{ {
if (_container == null || ViewModel == null) if (_container == null || _pinPoint == null || ViewModel == null)
return; return;
Matrix? transform = this.TransformToVisual(_container); Matrix? transform = _pinPoint.TransformToVisual(_container);
if (transform != null) 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 #endregion

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Events; using Artemis.Core.Events;
@ -16,6 +17,7 @@ namespace Artemis.UI.Screens.VisualScripting.Pins;
public abstract class PinViewModel : ActivatableViewModelBase public abstract class PinViewModel : ActivatableViewModelBase
{ {
private Point _position; private Point _position;
private ReactiveCommand<IPin, Unit>? _removePin;
protected PinViewModel(IPin pin, INodeService nodeService) protected PinViewModel(IPin pin, INodeService nodeService)
{ {
@ -52,6 +54,12 @@ public abstract class PinViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _position, value); set => RaiseAndSetIfChanged(ref _position, value);
} }
public ReactiveCommand<IPin, Unit>? RemovePin
{
get => _removePin;
set => RaiseAndSetIfChanged(ref _removePin, value);
}
public bool IsCompatibleWith(PinViewModel pinViewModel) public bool IsCompatibleWith(PinViewModel pinViewModel)
{ {
if (pinViewModel.Pin.Direction == Pin.Direction) if (pinViewModel.Pin.Direction == Pin.Direction)

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
@ -9,6 +10,7 @@ using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.Interfaces;
using Artemis.VisualScripting.Nodes; using Artemis.VisualScripting.Nodes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
using SkiaSharp; using SkiaSharp;
@ -16,6 +18,8 @@ namespace Artemis.UI.Screens.Workshop
{ {
public class WorkshopViewModel : MainScreenViewModel public class WorkshopViewModel : MainScreenViewModel
{ {
private static NodeScript<bool>? _testScript = null;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private StandardCursorType _selectedCursor; private StandardCursorType _selectedCursor;
private readonly ObservableAsPropertyHelper<Cursor> _cursor; private readonly ObservableAsPropertyHelper<Cursor> _cursor;
@ -38,16 +42,26 @@ namespace Artemis.UI.Screens.Workshop
DisplayName = "Workshop"; DisplayName = "Workshop";
ShowNotification = ReactiveCommand.Create<NotificationSeverity>(ExecuteShowNotification); ShowNotification = ReactiveCommand.Create<NotificationSeverity>(ExecuteShowNotification);
NodeScript<bool> testScript = new("Test script", "A test script"); if (_testScript == null)
INode exitNode = testScript.Nodes.Last(); {
exitNode.X = 300; _testScript = new NodeScript<bool>("Test script", "A test script");
exitNode.Y = 150; INode exitNode = _testScript.Nodes.Last();
exitNode.X = 300;
exitNode.Y = 150;
OrNode orNode = new() {X = 100, Y = 100}; OrNode orNode = new() {X = 100, Y = 100};
testScript.AddNode(orNode); _testScript.AddNode(orNode);
orNode.Result.ConnectTo(exitNode.Pins.First()); 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; } public NodeScriptViewModel VisualEditorViewModel { get; }

View File

@ -0,0 +1,29 @@
using System.Globalization;
using Artemis.Core;
using Avalonia.Data.Converters;
namespace Artemis.VisualScripting.Converters;
/// <summary>
/// Converts input into <see cref="Numeric" />.
/// </summary>
public class NumericConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(Numeric))
return new Numeric(value);
return value;
}
/// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (targetType == typeof(Numeric))
return new Numeric(value);
return value;
}
}

View File

@ -1,25 +0,0 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.EnumEqualsNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<ComboBox Width="140"
materialDesign:ComboBoxAssist.ClassicMode="True"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
SelectedValue="{Binding Node.Storage}"
ItemsSource="{Binding EnumValues}"
SelectedValuePath="Value"
DisplayMemberPath="Description">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</Grid>
</UserControl>

View File

@ -1,17 +0,0 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.LayerPropertyNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel>
<ComboBox Margin="8 0"
ItemsSource="{Binding ProfileElements}"
SelectedValue="{Binding SelectedProfileElement}" />
<ComboBox Margin="8 0"
ItemsSource="{Binding LayerProperties}"
SelectedValue="{Binding SelectedLayerProperty}"
DisplayMemberPath="PropertyDescription.Name" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels"
xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView"
x:DataType="customViewModels:StaticNumericValueNodeCustomViewModel">
<UserControl.Resources>
<converters:NumericConverter x:Key="NumericConverter" />
</UserControl.Resources>
<NumericUpDown VerticalAlignment="Center" Value="{Binding Node.Storage, Converter={StaticResource NumericConverter}}"></NumericUpDown>
</UserControl>

View File

@ -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);
}
}
}

View File

@ -1,15 +0,0 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:StringToNumericConverter x:Key="StringToNumericConverter" />
</UserControl.Resources>
<TextBox VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Text="{Binding Node.Storage, Converter={StaticResource StringToNumericConverter}}" />
</UserControl>

View File

@ -1,11 +0,0 @@
<UserControl x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticStringValueNodeCustomView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<TextBox VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Text="{Binding Node.Storage}" />
</UserControl>