Add project files.
This commit is contained in:
1023
Nodify/Connections/BaseConnection.cs
Normal file
1023
Nodify/Connections/BaseConnection.cs
Normal file
File diff suppressed because it is too large
Load Diff
143
Nodify/Connections/CircuitConnection.cs
Normal file
143
Nodify/Connections/CircuitConnection.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a line that is controlled by an angle.
|
||||
/// </summary>
|
||||
public class CircuitConnection : LineConnection
|
||||
{
|
||||
protected const double Degrees = Math.PI / 180.0d;
|
||||
|
||||
public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(nameof(Angle), typeof(double), typeof(LineConnection), new FrameworkPropertyMetadata(BoxValue.Double45, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
/// <summary>
|
||||
/// The angle of the connection in degrees.
|
||||
/// </summary>
|
||||
public double Angle
|
||||
{
|
||||
get => (double)GetValue(AngleProperty);
|
||||
set => SetValue(AngleProperty, value);
|
||||
}
|
||||
|
||||
static CircuitConnection()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(CircuitConnection), new FrameworkPropertyMetadata(typeof(CircuitConnection)));
|
||||
NodifyEditor.CuttingConnectionTypes.Add(typeof(CircuitConnection));
|
||||
}
|
||||
|
||||
protected override ((Point ArrowStartSource, Point ArrowStartTarget), (Point ArrowEndSource, Point ArrowEndTarget)) DrawLineGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2) = GetLinePoints(source, target);
|
||||
|
||||
context.BeginFigure(source, false, false);
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
AddSmoothCorner(context, source, p0, p1, CornerRadius);
|
||||
AddSmoothCorner(context, p0, p1, p2, CornerRadius);
|
||||
AddSmoothCorner(context, p1, p2, target, CornerRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.LineTo(p0, true, true);
|
||||
context.LineTo(p1, true, true);
|
||||
context.LineTo(p2, true, true);
|
||||
}
|
||||
context.LineTo(target, true, true);
|
||||
|
||||
if (Spacing < 1d)
|
||||
{
|
||||
return ((p1, source), (p1, target));
|
||||
}
|
||||
|
||||
return ((p0, source), (p1, target));
|
||||
}
|
||||
|
||||
protected override Point GetTextPosition(FormattedText text, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2) = GetLinePoints(source, target);
|
||||
|
||||
Vector deltaSource = p1 - p0;
|
||||
Vector deltaTarget = p2 - p1;
|
||||
|
||||
if (deltaSource.LengthSquared > deltaTarget.LengthSquared)
|
||||
{
|
||||
return new Point((p0.X + p1.X - text.Width) / 2, (p0.Y + p1.Y - text.Height) / 2);
|
||||
}
|
||||
|
||||
return new Point((p2.X + p1.X - text.Width) / 2, (p2.Y + p1.Y - text.Height) / 2);
|
||||
}
|
||||
|
||||
protected override void DrawDirectionalArrowsGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2) = GetLinePoints(source, target);
|
||||
|
||||
double spacing = 1d / (DirectionalArrowsCount + 1);
|
||||
for (int i = 1; i <= DirectionalArrowsCount; i++)
|
||||
{
|
||||
double t = (spacing * i + DirectionalArrowsOffset).WrapToRange(0d, 1d);
|
||||
var (segment, to) = InterpolateLine(p0, p1, p2, t);
|
||||
|
||||
var direction = segment.SegmentStart - segment.SegmentEnd;
|
||||
DrawDirectionalArrowheadGeometry(context, direction, to);
|
||||
}
|
||||
}
|
||||
|
||||
private (Point P0, Point P1, Point P2) GetLinePoints(in Point source, in Point target)
|
||||
{
|
||||
double direction = Direction == ConnectionDirection.Forward ? 1d : -1d;
|
||||
var spacing = new Vector(Spacing * direction, 0d);
|
||||
var spacingVertical = new Vector(spacing.Y, spacing.X);
|
||||
var arrowOffset = new Vector(ArrowSize.Width * direction, 0d);
|
||||
|
||||
if (TargetOrientation == Orientation.Vertical)
|
||||
{
|
||||
(arrowOffset.X, arrowOffset.Y) = (arrowOffset.Y, arrowOffset.X);
|
||||
}
|
||||
|
||||
Point endPoint = Spacing > 0 ? target - arrowOffset : target;
|
||||
|
||||
Point p1 = source + (SourceOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
Point p3 = endPoint - (TargetOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
Point p2 = GetControlPoint(p1, p3);
|
||||
|
||||
return (p1, p2, p3);
|
||||
}
|
||||
|
||||
private Point GetControlPoint(in Point source, in Point target)
|
||||
{
|
||||
Vector delta = target - source;
|
||||
double tangent = Math.Tan(Angle * Degrees);
|
||||
|
||||
double dx = Math.Abs(delta.X);
|
||||
double dy = Math.Abs(delta.Y);
|
||||
|
||||
double slopeWidth = dy / tangent;
|
||||
if (dx > slopeWidth)
|
||||
{
|
||||
return delta.X > 0d ? new Point(target.X - slopeWidth, source.Y) : new Point(source.X - slopeWidth, target.Y);
|
||||
}
|
||||
|
||||
double slopeHeight = dx * tangent;
|
||||
if (dy > slopeHeight)
|
||||
{
|
||||
if (delta.Y > 0d)
|
||||
{
|
||||
// handle top left
|
||||
return delta.X < 0d ? new Point(source.X, target.Y - slopeHeight) : new Point(target.X, source.Y + slopeHeight);
|
||||
}
|
||||
|
||||
// handle bottom left
|
||||
if (delta.X < 0d)
|
||||
{
|
||||
return new Point(source.X, target.Y + slopeHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return new Point(target.X, source.Y - slopeHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Nodify/Connections/Connection.cs
Normal file
112
Nodify/Connections/Connection.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a cubic bezier curve.
|
||||
/// </summary>
|
||||
public class Connection : BaseConnection
|
||||
{
|
||||
static Connection()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Connection), new FrameworkPropertyMetadata(typeof(Connection)));
|
||||
NodifyEditor.CuttingConnectionTypes.Add(typeof(Connection));
|
||||
}
|
||||
|
||||
private const double _baseOffset = 100d;
|
||||
private const double _offsetGrowthRate = 25d;
|
||||
|
||||
protected override ((Point ArrowStartSource, Point ArrowStartTarget), (Point ArrowEndSource, Point ArrowEndTarget)) DrawLineGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetBezierControlPoints(source, target);
|
||||
|
||||
context.BeginFigure(source, false, false);
|
||||
context.LineTo(p0, true, true);
|
||||
context.BezierTo(p1, p2, p3, true, true);
|
||||
context.LineTo(target, true, true);
|
||||
|
||||
return ((target, source), (source, target));
|
||||
}
|
||||
|
||||
protected override void DrawDirectionalArrowsGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetBezierControlPoints(source, target);
|
||||
|
||||
double spacing = 1d / (DirectionalArrowsCount + 1);
|
||||
for (int i = 1; i <= DirectionalArrowsCount; i++)
|
||||
{
|
||||
double t = (spacing * i + DirectionalArrowsOffset).WrapToRange(0d, 1d);
|
||||
var to = InterpolateCubicBezier(p0, p1, p2, p3, t);
|
||||
var direction = GetBezierTangent(p0, p1, p2, p3, t);
|
||||
|
||||
DrawDirectionalArrowheadGeometry(context, direction, to);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Point GetTextPosition(FormattedText text, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetBezierControlPoints(source, target);
|
||||
var textCenter = new Vector(text.Width / 2, text.Height / 2);
|
||||
return InterpolateCubicBezier(p0, p1, p2, p3, 0.5) - textCenter;
|
||||
}
|
||||
|
||||
private (Point P0, Point P1, Point P2, Point P3) GetBezierControlPoints(Point source, Point target)
|
||||
{
|
||||
double direction = Direction == ConnectionDirection.Forward ? 1d : -1d;
|
||||
var spacing = new Vector(Spacing * direction, 0d);
|
||||
var spacingVertical = new Vector(spacing.Y, spacing.X);
|
||||
|
||||
Point startPoint = source + (SourceOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
Point endPoint = target - (TargetOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
|
||||
Vector delta = target - source;
|
||||
double height = Math.Abs(delta.Y);
|
||||
double width = Math.Abs(delta.X);
|
||||
|
||||
// Smooth curve when distance is lower than base offset
|
||||
double smooth = Math.Min(_baseOffset, height);
|
||||
// Calculate offset based on distance
|
||||
double offset = Math.Max(smooth, width / 2d);
|
||||
// Grow slowly with distance
|
||||
offset = Math.Min(_baseOffset + Math.Sqrt(width * _offsetGrowthRate), offset);
|
||||
|
||||
var controlPoint = new Vector(offset * direction, 0d);
|
||||
var controlPointVertical = new Vector(controlPoint.Y, controlPoint.X);
|
||||
|
||||
// Avoid sharp bend if orientation different (when close to each other)
|
||||
if (TargetOrientation != SourceOrientation)
|
||||
{
|
||||
controlPoint *= 0.5;
|
||||
}
|
||||
|
||||
Point p0 = startPoint;
|
||||
Point p1 = startPoint + (SourceOrientation == Orientation.Vertical ? controlPointVertical : controlPoint);
|
||||
Point p2 = endPoint - (TargetOrientation == Orientation.Vertical ? controlPointVertical : controlPoint);
|
||||
Point p3 = endPoint;
|
||||
|
||||
return (p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
private static Vector GetBezierTangent(Point P0, Point P1, Point P2, Point P3, double t)
|
||||
{
|
||||
// Calculate the derivatives of the Bezier curve equation and negate the result
|
||||
return -(-3 * (1 - t) * (1 - t) * (Vector)P0 +
|
||||
(3 * (1 - t) * (1 - t) * (Vector)P1 - 6 * t * (1 - t) * (Vector)P1) +
|
||||
(6 * t * (1 - t) * (Vector)P2 - 3 * t * t * (Vector)P2) +
|
||||
3 * t * t * (Vector)P3);
|
||||
}
|
||||
|
||||
protected static Point InterpolateCubicBezier(Point P0, Point P1, Point P2, Point P3, double t)
|
||||
{
|
||||
// B = (1 − t)^3 * P0 + 3 * t * (1 − t)^2 * P1 + 3 * t^2 * (1 − t) * P2 + t^3 * P3
|
||||
return (Point)
|
||||
((Vector)P0 * (1 - t) * (1 - t) * (1 - t)
|
||||
+ (Vector)P1 * 3 * t * (1 - t) * (1 - t)
|
||||
+ (Vector)P2 * 3 * t * t * (1 - t)
|
||||
+ (Vector)P3 * t * t * t);
|
||||
}
|
||||
}
|
||||
}
|
||||
200
Nodify/Connections/ConnectionContainer.cs
Normal file
200
Nodify/Connections/ConnectionContainer.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using Nodify.Interactivity;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
public class ConnectionContainer : ContentPresenter, IKeyboardFocusTarget<ConnectionContainer>
|
||||
{
|
||||
#region Dependency properties
|
||||
|
||||
public static readonly DependencyProperty IsSelectableProperty = DependencyProperty.Register(nameof(IsSelectable), typeof(bool), typeof(ConnectionContainer), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
public static readonly DependencyProperty IsSelectedProperty = System.Windows.Controls.Primitives.Selector.IsSelectedProperty.AddOwner(typeof(ConnectionContainer), new FrameworkPropertyMetadata(BoxValue.False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsSelectedChanged));
|
||||
|
||||
private static void OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var elem = (ConnectionContainer)d;
|
||||
bool result = elem.IsSelectable && (bool)e.NewValue;
|
||||
elem.IsSelected = result;
|
||||
elem.OnSelectedChanged(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether this <see cref="ConnectionContainer"/> can be selected.
|
||||
/// </summary>
|
||||
public bool IsSelectable
|
||||
{
|
||||
get => BaseConnection.GetIsSelectable(Connection ?? this);
|
||||
set => BaseConnection.SetIsSelectable(Connection ?? this, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether this <see cref="ConnectionContainer"/> is selected.
|
||||
/// Can only be set if <see cref="IsSelectable"/> is true.
|
||||
/// </summary>
|
||||
public bool IsSelected
|
||||
{
|
||||
get => (bool)GetValue(IsSelectedProperty);
|
||||
set => SetValue(IsSelectedProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Routed events
|
||||
|
||||
public static readonly RoutedEvent SelectedEvent = System.Windows.Controls.Primitives.Selector.SelectedEvent.AddOwner(typeof(ConnectionContainer));
|
||||
public static readonly RoutedEvent UnselectedEvent = System.Windows.Controls.Primitives.Selector.UnselectedEvent.AddOwner(typeof(ConnectionContainer));
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when this <see cref="ConnectionContainer"/> is selected.
|
||||
/// </summary>
|
||||
public event RoutedEventHandler Selected
|
||||
{
|
||||
add => AddHandler(SelectedEvent, value);
|
||||
remove => RemoveHandler(SelectedEvent, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when this <see cref="ConnectionContainer"/> is unselected.
|
||||
/// </summary>
|
||||
public event RoutedEventHandler Unselected
|
||||
{
|
||||
add => AddHandler(UnselectedEvent, value);
|
||||
remove => RemoveHandler(UnselectedEvent, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private FrameworkElement? _connection;
|
||||
private SelectionType? _selectionType;
|
||||
|
||||
public Rect Bounds => ConnectionFocusTarget.Bounds;
|
||||
ConnectionContainer IKeyboardFocusTarget<ConnectionContainer>.Element => this;
|
||||
|
||||
private IKeyboardFocusTarget<FrameworkElement> ConnectionFocusTarget => Connection as IKeyboardFocusTarget<FrameworkElement>
|
||||
?? throw new NotSupportedException($"Custom connections must implement {nameof(IKeyboardFocusTarget<FrameworkElement>)} for keyboard navigation. Or disable keyboard navigation for the connections layer.");
|
||||
|
||||
public FrameworkElement? Connection => _connection ??= BaseConnection.PrioritizeBaseConnectionForSelection
|
||||
? this.GetChildOfType<BaseConnection>() ?? this.GetChildOfType<FrameworkElement>()
|
||||
: this.GetChildOfType<FrameworkElement>();
|
||||
|
||||
public ConnectionsMultiSelector Selector { get; }
|
||||
|
||||
static ConnectionContainer()
|
||||
{
|
||||
FocusableProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(BoxValue.True));
|
||||
FocusVisualStyleProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(new Style()));
|
||||
|
||||
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
|
||||
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ConnectionContainer), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
|
||||
}
|
||||
|
||||
public ConnectionContainer(ConnectionsMultiSelector selector)
|
||||
{
|
||||
Selector = selector;
|
||||
}
|
||||
|
||||
protected override void OnVisualParentChanged(DependencyObject oldParent)
|
||||
{
|
||||
if (VisualTreeHelper.GetParent(this) == null && IsKeyboardFocusWithin)
|
||||
{
|
||||
base.OnVisualParentChanged(oldParent);
|
||||
|
||||
Selector.Editor?.Focus();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnVisualParentChanged(oldParent);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnIsKeyboardFocusedChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (Connection is BaseConnection baseConnection)
|
||||
{
|
||||
baseConnection.UpdateFocusVisual();
|
||||
}
|
||||
else
|
||||
{
|
||||
Connection?.InvalidateVisual();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the <see cref="SelectedEvent"/> or <see cref="UnselectedEvent"/> based on <paramref name="newValue"/>.
|
||||
/// Called when the <see cref="IsSelected"/> value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">True if selected, false otherwise.</param>
|
||||
private void OnSelectedChanged(bool newValue)
|
||||
{
|
||||
BaseConnection.SetIsSelected(Connection, newValue);
|
||||
|
||||
RaiseEvent(new RoutedEventArgs(newValue ? SelectedEvent : UnselectedEvent, this));
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
|
||||
if (IsSelectable && gestures.Selection.Select.Matches(e.Source, e))
|
||||
{
|
||||
_selectionType = gestures.Selection.GetSelectionType(e);
|
||||
}
|
||||
// Replaces the current selection when right-clicking on an element that has a context menu and is not selected.
|
||||
// Applies only when the select gesture is not right click.
|
||||
else if (e.ChangedButton == MouseButton.Right && Connection?.ContextMenu != null)
|
||||
{
|
||||
_selectionType = IsSelected ? SelectionType.Append : SelectionType.Replace;
|
||||
}
|
||||
|
||||
if (_selectionType.HasValue)
|
||||
{
|
||||
Focus();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
if (_selectionType.HasValue)
|
||||
{
|
||||
// Determine whether the current selection should remain intact or be replaced by the clicked item.
|
||||
// If the right mouse button is pressed on an already selected item, and the item either has an
|
||||
// explicit context menu, the selection remains unchanged.
|
||||
// This ensures that the context menu applies to the entire selection rather than only the clicked item.
|
||||
bool allowContextMenu = e.ChangedButton == MouseButton.Right && IsSelected && Connection?.ContextMenu != null;
|
||||
if (!allowContextMenu)
|
||||
{
|
||||
Select(_selectionType.Value);
|
||||
}
|
||||
|
||||
_selectionType = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the selection state of the current item based on the specified selection type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of selection to perform.</param>
|
||||
public void Select(SelectionType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case SelectionType.Append:
|
||||
IsSelected = true;
|
||||
break;
|
||||
case SelectionType.Remove:
|
||||
IsSelected = false;
|
||||
break;
|
||||
case SelectionType.Invert:
|
||||
IsSelected = !IsSelected;
|
||||
break;
|
||||
case SelectionType.Replace:
|
||||
Selector.Select(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
291
Nodify/Connections/ConnectionsMultiSelector.cs
Normal file
291
Nodify/Connections/ConnectionsMultiSelector.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
using Nodify.Interactivity;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
public class ConnectionsMultiSelector : MultiSelector, IKeyboardNavigationLayer
|
||||
{
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty SelectedItemsProperty = NodifyEditor.SelectedItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(default(IList), OnSelectedItemsSourceChanged));
|
||||
public static readonly DependencyProperty CanSelectMultipleItemsProperty = NodifyEditor.CanSelectMultipleItemsProperty.AddOwner(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.True, OnCanSelectMultipleItemsChanged, CoerceCanSelectMultipleItems));
|
||||
|
||||
private static void OnCanSelectMultipleItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((ConnectionsMultiSelector)d).CanSelectMultipleItemsBase = (bool)e.NewValue;
|
||||
|
||||
private static object CoerceCanSelectMultipleItems(DependencyObject d, object baseValue)
|
||||
=> ((ConnectionsMultiSelector)d).CanSelectMultipleItemsBase = (bool)baseValue;
|
||||
|
||||
private static void OnSelectedItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
=> ((ConnectionsMultiSelector)d).OnSelectedItemsSourceChanged((IList)e.OldValue, (IList)e.NewValue);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the selected connections in the <see cref="NodifyEditor"/>.
|
||||
/// </summary>
|
||||
public new IList? SelectedItems
|
||||
{
|
||||
get => (IList?)GetValue(SelectedItemsProperty);
|
||||
set => SetValue(SelectedItemsProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether multiple connections can be selected.
|
||||
/// </summary>
|
||||
public new bool CanSelectMultipleItems
|
||||
{
|
||||
get => (bool)GetValue(CanSelectMultipleItemsProperty);
|
||||
set => SetValue(CanSelectMultipleItemsProperty, value);
|
||||
}
|
||||
|
||||
private bool CanSelectMultipleItemsBase
|
||||
{
|
||||
get => base.CanSelectMultipleItems;
|
||||
set => base.CanSelectMultipleItems = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="ConnectionsMultiSelector"/>.
|
||||
/// </summary>
|
||||
public NodifyEditor? Editor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all <see cref="ConnectionContainer"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>Cache the result before using it to avoid extra allocations.</remarks>
|
||||
protected internal IReadOnlyCollection<ConnectionContainer> ConnectionContainers
|
||||
{
|
||||
get
|
||||
{
|
||||
ItemCollection items = Items;
|
||||
var containers = new List<ConnectionContainer>(items.Count);
|
||||
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
containers.Add((ConnectionContainer)ItemContainerGenerator.ContainerFromIndex(i));
|
||||
}
|
||||
|
||||
return containers;
|
||||
}
|
||||
}
|
||||
|
||||
static ConnectionsMultiSelector()
|
||||
{
|
||||
FocusableProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
|
||||
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
|
||||
KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
|
||||
KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ConnectionsMultiSelector), new FrameworkPropertyMetadata(KeyboardNavigationMode.None));
|
||||
}
|
||||
|
||||
public ConnectionsMultiSelector()
|
||||
{
|
||||
_focusNavigator = new StatefulFocusNavigator<ConnectionContainer>(OnElementFocused);
|
||||
}
|
||||
|
||||
protected override DependencyObject GetContainerForItemOverride()
|
||||
=> new ConnectionContainer(this);
|
||||
|
||||
protected override bool IsItemItsOwnContainerOverride(object item)
|
||||
=> item is ConnectionContainer;
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
Editor = this.GetParentOfType<NodifyEditor>();
|
||||
|
||||
if (NodifyEditor.AutoRegisterConnectionsLayer)
|
||||
{
|
||||
Editor?.RegisterNavigationLayer(this);
|
||||
}
|
||||
}
|
||||
|
||||
#region Keyboard Navigation
|
||||
|
||||
public KeyboardNavigationLayerId Id { get; } = KeyboardNavigationLayerId.Connections;
|
||||
public IKeyboardFocusTarget<UIElement>? LastFocusedElement => _focusNavigator.LastFocusedElement;
|
||||
|
||||
private readonly StatefulFocusNavigator<ConnectionContainer> _focusNavigator;
|
||||
|
||||
public bool TryMoveFocus(TraversalRequest request)
|
||||
{
|
||||
return _focusNavigator.TryMoveFocus(request, TryFindContainerToFocus);
|
||||
}
|
||||
|
||||
public bool TryRestoreFocus()
|
||||
{
|
||||
return _focusNavigator.TryRestoreFocus();
|
||||
}
|
||||
|
||||
private bool TryFindContainerToFocus(ConnectionContainer? currentElement, TraversalRequest request, out ConnectionContainer? containerToFocus)
|
||||
{
|
||||
containerToFocus = null;
|
||||
|
||||
if (currentElement is ConnectionContainer focusedContainer)
|
||||
{
|
||||
containerToFocus = FindNextFocusTarget(focusedContainer, request);
|
||||
}
|
||||
else if (currentElement is UIElement elem && elem.GetParentOfType<ConnectionContainer>() is ConnectionContainer parentContainer)
|
||||
{
|
||||
containerToFocus = parentContainer;
|
||||
}
|
||||
else if (Items.Count > 0 && Editor != null)
|
||||
{
|
||||
var viewport = new Rect(Editor.ViewportLocation, Editor.ViewportSize);
|
||||
var containers = ConnectionContainers;
|
||||
containerToFocus = containers.FirstOrDefault(container => viewport.IntersectsWith(((IKeyboardFocusTarget<ConnectionContainer>)container).Bounds))
|
||||
?? containers.First();
|
||||
}
|
||||
|
||||
return containerToFocus != null;
|
||||
}
|
||||
|
||||
protected virtual ConnectionContainer? FindNextFocusTarget(ConnectionContainer currentContainer, TraversalRequest request)
|
||||
{
|
||||
var focusNavigator = new DirectionalFocusNavigator<ConnectionContainer>(ConnectionContainers);
|
||||
var result = focusNavigator.FindNextFocusTarget(currentContainer, request);
|
||||
|
||||
return result?.Element;
|
||||
}
|
||||
|
||||
protected virtual void OnElementFocused(IKeyboardFocusTarget<ConnectionContainer> target)
|
||||
{
|
||||
if (NodifyEditor.AutoPanOnNodeFocus)
|
||||
{
|
||||
Editor?.BringIntoView(target.Bounds, NodifyEditor.BringIntoViewEdgeOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void IKeyboardNavigationLayer.OnActivated()
|
||||
{
|
||||
TryRestoreFocus();
|
||||
}
|
||||
|
||||
void IKeyboardNavigationLayer.OnDeactivated()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Select(ConnectionContainer container)
|
||||
{
|
||||
BeginUpdateSelectedItems();
|
||||
var selected = base.SelectedItems;
|
||||
selected.Clear();
|
||||
selected.Add(container.DataContext);
|
||||
|
||||
#if NETCOREAPP3_0_OR_GREATER
|
||||
// For some reason the ConnectionContainer.IsSelected property change is not triggered, which prevents the visual update of the child connection.
|
||||
// To address this, we manually set the IsSelected property before it is automatically set to true by EndUpdateSelectedItems.
|
||||
// Note: This approach will cause bindings to update out of order.
|
||||
// It is recommended to handle undo/redo operations using the SelectionChanged event in this case.
|
||||
container.IsSelected = true;
|
||||
#endif
|
||||
|
||||
EndUpdateSelectedItems();
|
||||
|
||||
Editor?.UnselectAll();
|
||||
}
|
||||
|
||||
#region Selection Handlers
|
||||
|
||||
private void OnSelectedItemsSourceChanged(IList oldValue, IList newValue)
|
||||
{
|
||||
if (oldValue is INotifyCollectionChanged oc)
|
||||
{
|
||||
oc.CollectionChanged -= OnSelectedItemsChanged;
|
||||
}
|
||||
|
||||
if (newValue is INotifyCollectionChanged nc)
|
||||
{
|
||||
nc.CollectionChanged += OnSelectedItemsChanged;
|
||||
}
|
||||
|
||||
IList selectedItems = base.SelectedItems;
|
||||
|
||||
BeginUpdateSelectedItems();
|
||||
selectedItems.Clear();
|
||||
if (newValue != null)
|
||||
{
|
||||
for (var i = 0; i < newValue.Count; i++)
|
||||
{
|
||||
selectedItems.Add(newValue[i]);
|
||||
}
|
||||
}
|
||||
EndUpdateSelectedItems();
|
||||
}
|
||||
|
||||
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (!CanSelectMultipleItems)
|
||||
return;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
base.SelectedItems.Clear();
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
IList? newItems = e.NewItems;
|
||||
if (newItems != null)
|
||||
{
|
||||
IList selectedItems = base.SelectedItems;
|
||||
for (var i = 0; i < newItems.Count; i++)
|
||||
{
|
||||
selectedItems.Add(newItems[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
IList? oldItems = e.OldItems;
|
||||
if (oldItems != null)
|
||||
{
|
||||
IList selectedItems = base.SelectedItems;
|
||||
for (var i = 0; i < oldItems.Count; i++)
|
||||
{
|
||||
selectedItems.Remove(oldItems[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
|
||||
{
|
||||
base.OnSelectionChanged(e);
|
||||
|
||||
IList? selected = SelectedItems;
|
||||
if (selected != null)
|
||||
{
|
||||
IList added = e.AddedItems;
|
||||
for (var i = 0; i < added.Count; i++)
|
||||
{
|
||||
// Ensure no duplicates are added
|
||||
if (!selected.Contains(added[i]))
|
||||
{
|
||||
selected.Add(added[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IList removed = e.RemovedItems;
|
||||
for (var i = 0; i < removed.Count; i++)
|
||||
{
|
||||
selected.Remove(removed[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
38
Nodify/Connections/Events/ConnectionEventArgs.cs
Normal file
38
Nodify/Connections/Events/ConnectionEventArgs.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the method that will handle <see cref="BaseConnection"/> related routed events.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object where the event handler is attached.</param>
|
||||
/// <param name="e">The event data.</param>
|
||||
public delegate void ConnectionEventHandler(object sender, ConnectionEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for <see cref="BaseConnection"/> related routed events.
|
||||
/// </summary>
|
||||
public class ConnectionEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConnectionEventArgs"/> class using the specified <see cref="Connection"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="FrameworkElement.DataContext"/> of a related <see cref="BaseConnection"/>.</param>
|
||||
public ConnectionEventArgs(object connection)
|
||||
=> Connection = connection;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location where the connection should be split.
|
||||
/// </summary>
|
||||
public Point SplitLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FrameworkElement.DataContext"/> of the <see cref="BaseConnection"/> associated with this event.
|
||||
/// </summary>
|
||||
public object Connection { get; }
|
||||
|
||||
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
|
||||
=> ((ConnectionEventHandler)genericHandler)(genericTarget, this);
|
||||
}
|
||||
}
|
||||
173
Nodify/Connections/LineConnection.cs
Normal file
173
Nodify/Connections/LineConnection.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a line that has an arrow indicating its <see cref="BaseConnection.Direction"/>.
|
||||
/// </summary>
|
||||
public class LineConnection : BaseConnection
|
||||
{
|
||||
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(double), typeof(LineConnection), new FrameworkPropertyMetadata(BoxValue.Double5, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
/// <summary>
|
||||
/// The radius of the corners between the line segments.
|
||||
/// </summary>
|
||||
public double CornerRadius
|
||||
{
|
||||
get => (double)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
static LineConnection()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(LineConnection), new FrameworkPropertyMetadata(typeof(LineConnection)));
|
||||
NodifyEditor.CuttingConnectionTypes.Add(typeof(LineConnection));
|
||||
}
|
||||
|
||||
protected override ((Point ArrowStartSource, Point ArrowStartTarget), (Point ArrowEndSource, Point ArrowEndTarget)) DrawLineGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1) = GetLinePoints(source, target);
|
||||
|
||||
context.BeginFigure(source, false, false);
|
||||
if (CornerRadius > 0 && Spacing > 0)
|
||||
{
|
||||
AddSmoothCorner(context, source, p0, p1, CornerRadius);
|
||||
AddSmoothCorner(context, p0, p1, target, CornerRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.LineTo(p0, true, true);
|
||||
context.LineTo(p1, true, true);
|
||||
}
|
||||
context.LineTo(target, true, true);
|
||||
|
||||
return ((target, source), (source, target));
|
||||
}
|
||||
|
||||
protected override void DrawDefaultArrowhead(StreamGeometryContext context, Point source, Point target, ConnectionDirection arrowDirection = ConnectionDirection.Forward, Orientation orientation = Orientation.Horizontal)
|
||||
{
|
||||
if (Spacing < 1d)
|
||||
{
|
||||
Vector delta = source - target;
|
||||
double headWidth = ArrowSize.Width;
|
||||
double headHeight = ArrowSize.Height / 2;
|
||||
|
||||
double angle = Math.Atan2(delta.Y, delta.X);
|
||||
double sinT = Math.Sin(angle);
|
||||
double cosT = Math.Cos(angle);
|
||||
|
||||
var from = new Point(target.X + (headWidth * cosT - headHeight * sinT), target.Y + (headWidth * sinT + headHeight * cosT));
|
||||
var to = new Point(target.X + (headWidth * cosT + headHeight * sinT), target.Y - (headHeight * cosT - headWidth * sinT));
|
||||
|
||||
context.BeginFigure(target, true, true);
|
||||
context.LineTo(from, true, true);
|
||||
context.LineTo(to, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.DrawDefaultArrowhead(context, source, target, arrowDirection, orientation);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawDirectionalArrowsGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1) = GetLinePoints(source, target);
|
||||
var direction = p0 - p1;
|
||||
|
||||
double spacing = 1d / (DirectionalArrowsCount + 1);
|
||||
for (int i = 1; i <= DirectionalArrowsCount; i++)
|
||||
{
|
||||
double t = (spacing * i + DirectionalArrowsOffset).WrapToRange(0d, 1d);
|
||||
var to = InterpolateLineSegment(p0, p1, t);
|
||||
|
||||
DrawDirectionalArrowheadGeometry(context, direction, to);
|
||||
}
|
||||
}
|
||||
|
||||
private (Point P0, Point P1) GetLinePoints(Point source, Point target)
|
||||
{
|
||||
double direction = Direction == ConnectionDirection.Forward ? 1d : -1d;
|
||||
var spacing = new Vector(Spacing * direction, 0d);
|
||||
var spacingVertical = new Vector(spacing.Y, spacing.X);
|
||||
|
||||
var p0 = source + (SourceOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
var p1 = target - (TargetOrientation == Orientation.Vertical ? spacingVertical : spacing);
|
||||
|
||||
return (p0, p1);
|
||||
}
|
||||
|
||||
protected static Point InterpolateLineSegment(Point p0, Point p1, double t)
|
||||
{
|
||||
return (Point)((1 - t) * (Vector)p0 + t * (Vector)p1);
|
||||
}
|
||||
|
||||
protected static ((Point SegmentStart, Point SegmentEnd), Point InterpolatedPoint) InterpolateLine(Point p0, Point p1, Point p2, Point p3, double t)
|
||||
{
|
||||
double length1 = (p1 - p0).Length;
|
||||
double length2 = (p2 - p1).Length;
|
||||
double length3 = (p3 - p2).Length;
|
||||
double totalLength = length1 + length2 + length3;
|
||||
|
||||
double ratio1 = length1 / totalLength;
|
||||
double ratio2 = length2 / totalLength;
|
||||
double ratio3 = length3 / totalLength;
|
||||
|
||||
// Interpolate within the appropriate segment based on t
|
||||
if (t <= ratio1)
|
||||
{
|
||||
return ((p0, p1), InterpolateLineSegment(p0, p1, t / ratio1));
|
||||
}
|
||||
else if (t <= ratio1 + ratio2)
|
||||
{
|
||||
return ((p1, p2), InterpolateLineSegment(p1, p2, (t - ratio1) / ratio2));
|
||||
}
|
||||
|
||||
return ((p2, p3), InterpolateLineSegment(p2, p3, (t - ratio1 - ratio2) / ratio3));
|
||||
}
|
||||
|
||||
protected static ((Point SegmentStart, Point SegmentEnd), Point InterpolatedPoint) InterpolateLine(Point p0, Point p1, Point p2, double t)
|
||||
{
|
||||
double length1 = (p1 - p0).Length;
|
||||
double length2 = (p2 - p1).Length;
|
||||
double totalLength = length1 + length2;
|
||||
|
||||
double ratio1 = length1 / totalLength;
|
||||
double ratio2 = length2 / totalLength;
|
||||
|
||||
// Interpolate within the appropriate segment based on t
|
||||
if (t <= ratio1)
|
||||
{
|
||||
return ((p0, p1), InterpolateLineSegment(p0, p1, t / ratio1));
|
||||
}
|
||||
|
||||
return ((p1, p2), InterpolateLineSegment(p1, p2, (t - ratio1) / ratio2));
|
||||
}
|
||||
|
||||
protected static void AddSmoothCorner(StreamGeometryContext context, Point start, Point corner, Point end, double radius)
|
||||
{
|
||||
double distAB = (corner - start).LengthSquared;
|
||||
double distBC = (end - corner).LengthSquared;
|
||||
|
||||
double bendSize = Math.Sqrt(Math.Min(distAB, distBC)) / 2;
|
||||
radius = Math.Min(bendSize, radius);
|
||||
|
||||
Vector directionToCorner = corner - start;
|
||||
Vector directionFromCorner = end - corner;
|
||||
|
||||
if (directionToCorner.LengthSquared != 0)
|
||||
directionToCorner.Normalize();
|
||||
|
||||
if (directionFromCorner.LengthSquared != 0)
|
||||
directionFromCorner.Normalize();
|
||||
|
||||
Point curveStart = corner - directionToCorner * radius;
|
||||
Point curveEnd = corner + directionFromCorner * radius;
|
||||
|
||||
context.LineTo(curveStart, true, true);
|
||||
context.QuadraticBezierTo(corner, curveEnd, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Nodify/Connections/States/ConnectionState.cs
Normal file
11
Nodify/Connections/States/ConnectionState.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectionState
|
||||
{
|
||||
internal static void RegisterDefaultHandlers()
|
||||
{
|
||||
InputProcessor.Shared<BaseConnection>.RegisterHandlerFactory(elem => new Disconnect(elem));
|
||||
InputProcessor.Shared<BaseConnection>.RegisterHandlerFactory(elem => new Split(elem));
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Nodify/Connections/States/Disconnect.cs
Normal file
41
Nodify/Connections/States/Disconnect.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a state in which a connection can be disconnected from its connectors based on specific gestures.
|
||||
/// </summary>
|
||||
public class Disconnect : InputElementState<BaseConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Disconnect"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="BaseConnection"/> element associated with this state.</param>
|
||||
public Disconnect(BaseConnection connection) : base(connection)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
|
||||
if (gestures.Disconnect.Matches(e.Source, e))
|
||||
{
|
||||
Element.Focus();
|
||||
e.Handled = true; // prevent interacting with the editor
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
|
||||
if (gestures.Disconnect.Matches(e.Source, e))
|
||||
{
|
||||
Element.Remove();
|
||||
e.Handled = true; // prevent opening context menu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Nodify/Connections/States/Split.cs
Normal file
33
Nodify/Connections/States/Split.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Nodify.Interactivity
|
||||
{
|
||||
public static partial class ConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a state in which a connection can be split.
|
||||
/// </summary>
|
||||
public class Split : InputElementState<BaseConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Split"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="BaseConnection"/> element associated with this state.</param>
|
||||
public Split(BaseConnection connection) : base(connection)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ConnectionGestures gestures = EditorGestures.Mappings.Connection;
|
||||
if (gestures.Split.Matches(e.Source, e))
|
||||
{
|
||||
Element.Focus();
|
||||
Element.SplitAtLocation(e.GetPosition(Element));
|
||||
|
||||
e.Handled = true; // prevent interacting with the editor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
273
Nodify/Connections/StepConnection.cs
Normal file
273
Nodify/Connections/StepConnection.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
public enum ConnectorPosition
|
||||
{
|
||||
Top,
|
||||
Left,
|
||||
Bottom,
|
||||
Right
|
||||
}
|
||||
|
||||
public class StepConnection : LineConnection
|
||||
{
|
||||
public static readonly DependencyProperty SourcePositionProperty = DependencyProperty.Register(nameof(SourcePosition), typeof(ConnectorPosition), typeof(StepConnection), new FrameworkPropertyMetadata(ConnectorPosition.Right, FrameworkPropertyMetadataOptions.AffectsRender, OnConnectorPositionChanged));
|
||||
public static readonly DependencyProperty TargetPositionProperty = DependencyProperty.Register(nameof(TargetPosition), typeof(ConnectorPosition), typeof(StepConnection), new FrameworkPropertyMetadata(ConnectorPosition.Left, FrameworkPropertyMetadataOptions.AffectsRender, OnConnectorPositionChanged));
|
||||
|
||||
private static void OnConnectorPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var connection = (StepConnection)d;
|
||||
connection.CoerceValue(DirectionProperty);
|
||||
connection.CoerceValue(SourceOrientationProperty);
|
||||
connection.CoerceValue(TargetOrientationProperty);
|
||||
}
|
||||
|
||||
static StepConnection()
|
||||
{
|
||||
SourceOrientationProperty.OverrideMetadata(typeof(StepConnection), new FrameworkPropertyMetadata(Orientation.Horizontal, null, CoerceSourceOrientation));
|
||||
TargetOrientationProperty.OverrideMetadata(typeof(StepConnection), new FrameworkPropertyMetadata(Orientation.Horizontal, null, CoerceTargetOrientation));
|
||||
DirectionProperty.OverrideMetadata(typeof(StepConnection), new FrameworkPropertyMetadata(ConnectionDirection.Forward, null, CoerceConnectionDirection));
|
||||
NodifyEditor.CuttingConnectionTypes.Add(typeof(StepConnection));
|
||||
}
|
||||
|
||||
private static object CoerceSourceOrientation(DependencyObject d, object baseValue)
|
||||
{
|
||||
var connection = (StepConnection)d;
|
||||
return connection.SourcePosition == ConnectorPosition.Left || connection.SourcePosition == ConnectorPosition.Right
|
||||
? Orientation.Horizontal
|
||||
: Orientation.Vertical;
|
||||
}
|
||||
|
||||
private static object CoerceTargetOrientation(DependencyObject d, object baseValue)
|
||||
{
|
||||
var connection = (StepConnection)d;
|
||||
return connection.TargetPosition == ConnectorPosition.Left || connection.TargetPosition == ConnectorPosition.Right
|
||||
? Orientation.Horizontal
|
||||
: Orientation.Vertical;
|
||||
}
|
||||
|
||||
private static object CoerceConnectionDirection(DependencyObject d, object baseValue)
|
||||
{
|
||||
var connection = (StepConnection)d;
|
||||
return connection.TargetPosition == ConnectorPosition.Left || connection.TargetPosition == ConnectorPosition.Top
|
||||
? ConnectionDirection.Forward
|
||||
: ConnectionDirection.Backward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position of the source connector.
|
||||
/// </summary>
|
||||
public ConnectorPosition SourcePosition
|
||||
{
|
||||
get => (ConnectorPosition)GetValue(SourcePositionProperty);
|
||||
set => SetValue(SourcePositionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position of the target connector.
|
||||
/// </summary>
|
||||
public ConnectorPosition TargetPosition
|
||||
{
|
||||
get => (ConnectorPosition)GetValue(TargetPositionProperty);
|
||||
set => SetValue(TargetPositionProperty, value);
|
||||
}
|
||||
|
||||
protected override ((Point ArrowStartSource, Point ArrowStartTarget), (Point ArrowEndSource, Point ArrowEndTarget)) DrawLineGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetLinePoints(source, target);
|
||||
|
||||
if (CornerRadius > 0)
|
||||
{
|
||||
DrawSmoothLine(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawDefaultLine(context);
|
||||
}
|
||||
|
||||
if (Spacing < 1d)
|
||||
{
|
||||
return ((p1, source), (p2, target));
|
||||
}
|
||||
|
||||
return ((target, source), (source, target));
|
||||
|
||||
void DrawDefaultLine(StreamGeometryContext context)
|
||||
{
|
||||
context.BeginFigure(source, false, false);
|
||||
context.LineTo(p0, true, true);
|
||||
context.LineTo(p1, true, true);
|
||||
context.LineTo(p2, true, true);
|
||||
context.LineTo(p3, true, true);
|
||||
context.LineTo(target, true, true);
|
||||
}
|
||||
|
||||
void DrawSmoothLine(StreamGeometryContext context)
|
||||
{
|
||||
context.BeginFigure(source, false, false);
|
||||
AddSmoothCorner(context, source, p0, p1, CornerRadius);
|
||||
|
||||
if (p1 == p2)
|
||||
{
|
||||
// skip p1 or p2 because they overlap
|
||||
AddSmoothCorner(context, p0, p1, p3, CornerRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSmoothCorner(context, p0, p1, p2, CornerRadius);
|
||||
AddSmoothCorner(context, p1, p2, p3, CornerRadius);
|
||||
}
|
||||
|
||||
AddSmoothCorner(context, p2, p3, target, CornerRadius);
|
||||
context.LineTo(target, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Point GetTextPosition(FormattedText text, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetLinePoints(source, target);
|
||||
|
||||
Vector delta1 = p1 - p0;
|
||||
Vector delta2 = p2 - p1;
|
||||
Vector delta3 = p3 - p2;
|
||||
|
||||
var max = GetMax(delta1, GetMax(delta2, delta3));
|
||||
|
||||
if (max == delta1)
|
||||
{
|
||||
return new Point((p0.X + p1.X - text.Width) / 2, (p0.Y + p1.Y - text.Height) / 2);
|
||||
}
|
||||
else if (max == delta2)
|
||||
{
|
||||
return new Point((p2.X + p1.X - text.Width) / 2, (p2.Y + p1.Y - text.Height) / 2);
|
||||
}
|
||||
|
||||
return new Point((p3.X + p2.X - text.Width) / 2, (p3.Y + p2.Y - text.Height) / 2);
|
||||
|
||||
static Vector GetMax(in Vector a, in Vector b)
|
||||
=> a.LengthSquared > b.LengthSquared ? a : b;
|
||||
}
|
||||
|
||||
protected override void DrawDirectionalArrowsGeometry(StreamGeometryContext context, Point source, Point target)
|
||||
{
|
||||
var (p0, p1, p2, p3) = GetLinePoints(source, target);
|
||||
|
||||
double spacing = 1d / (DirectionalArrowsCount + 1);
|
||||
for (int i = 1; i <= DirectionalArrowsCount; i++)
|
||||
{
|
||||
double t = (spacing * i + DirectionalArrowsOffset).WrapToRange(0d, 1d);
|
||||
var (segment, to) = InterpolateLine(p0, p1, p2, p3, t);
|
||||
|
||||
var direction = segment.SegmentStart - segment.SegmentEnd;
|
||||
DrawDirectionalArrowheadGeometry(context, direction, to);
|
||||
}
|
||||
}
|
||||
|
||||
private (Point P0, Point P1, Point P2, Point P3) GetLinePoints(Point source, Point target)
|
||||
{
|
||||
var sourceDir = GetConnectorDirection(SourcePosition);
|
||||
var targetDir = GetConnectorDirection(TargetPosition);
|
||||
|
||||
Point startPoint = source + new Vector(Spacing * sourceDir.X, Spacing * sourceDir.Y);
|
||||
Point endPoint = target + new Vector(Spacing * targetDir.X, Spacing * targetDir.Y);
|
||||
|
||||
var connectionDir = GetConnectionDirection(startPoint, SourcePosition, endPoint);
|
||||
bool horizontalConnection = connectionDir.X != 0;
|
||||
|
||||
if (IsOppositePosition(SourcePosition, TargetPosition))
|
||||
{
|
||||
var (p1, p2) = GetOppositePositionPoints();
|
||||
return (startPoint, p1, p2, endPoint);
|
||||
}
|
||||
|
||||
// same: left to left / top to top etc
|
||||
if (SourcePosition == TargetPosition)
|
||||
{
|
||||
var p = GetSamePositionPoint();
|
||||
return (startPoint, p, p, endPoint);
|
||||
}
|
||||
|
||||
// mixed: right to bottom / left to top etc
|
||||
bool isSameDir = horizontalConnection ? sourceDir.X == targetDir.Y : sourceDir.Y == targetDir.X;
|
||||
bool startGreaterThanEnd = horizontalConnection ? startPoint.Y > endPoint.Y : startPoint.X > endPoint.X;
|
||||
|
||||
bool positiveDir = horizontalConnection ? sourceDir.X == 1 : sourceDir.Y == 1;
|
||||
bool shouldFlip = positiveDir
|
||||
? isSameDir ? !startGreaterThanEnd : startGreaterThanEnd
|
||||
: isSameDir ? startGreaterThanEnd : !startGreaterThanEnd;
|
||||
|
||||
if (shouldFlip)
|
||||
{
|
||||
var sourceTarget = new Point(startPoint.X, endPoint.Y);
|
||||
var targetSource = new Point(endPoint.X, startPoint.Y);
|
||||
|
||||
var pf = horizontalConnection ? sourceTarget : targetSource;
|
||||
return (startPoint, pf, pf, endPoint);
|
||||
}
|
||||
|
||||
var pp = GetSamePositionPoint();
|
||||
return (startPoint, pp, pp, endPoint);
|
||||
|
||||
(Point P1, Point P2) GetOppositePositionPoints()
|
||||
{
|
||||
var center = startPoint + (endPoint - startPoint) / 2;
|
||||
|
||||
(Point P1, Point P2) verticalSplit = (new Point(center.X, startPoint.Y), new Point(center.X, endPoint.Y));
|
||||
(Point P1, Point P2) horizontalSplit = (new Point(startPoint.X, center.Y), new Point(endPoint.X, center.Y));
|
||||
|
||||
if (horizontalConnection)
|
||||
{
|
||||
// left to right / right to left
|
||||
return sourceDir.X == connectionDir.X ? verticalSplit : horizontalSplit;
|
||||
}
|
||||
|
||||
// top to bottom / bottom to top
|
||||
return sourceDir.Y == connectionDir.Y ? horizontalSplit : verticalSplit;
|
||||
}
|
||||
|
||||
Point GetSamePositionPoint()
|
||||
{
|
||||
var sourceTarget = new Point(startPoint.X, endPoint.Y);
|
||||
var targetSource = new Point(endPoint.X, startPoint.Y);
|
||||
|
||||
if (horizontalConnection)
|
||||
{
|
||||
// left to left / right to right
|
||||
return sourceDir.X == connectionDir.X ? targetSource : sourceTarget;
|
||||
}
|
||||
|
||||
// top to top / bottom to bottom
|
||||
return sourceDir.Y == connectionDir.Y ? sourceTarget : targetSource;
|
||||
}
|
||||
|
||||
static Point GetConnectionDirection(in Point source, ConnectorPosition sourcePosition, in Point target)
|
||||
{
|
||||
return sourcePosition == ConnectorPosition.Left || sourcePosition == ConnectorPosition.Right
|
||||
? new Point(Math.Sign(target.X - source.X), 0)
|
||||
: new Point(0, Math.Sign(target.Y - source.Y));
|
||||
}
|
||||
|
||||
static Point GetConnectorDirection(ConnectorPosition position)
|
||||
=> position switch
|
||||
{
|
||||
ConnectorPosition.Top => new Point(0, -1),
|
||||
ConnectorPosition.Left => new Point(-1, 0),
|
||||
ConnectorPosition.Bottom => new Point(0, 1),
|
||||
ConnectorPosition.Right => new Point(1, 0),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
static bool IsOppositePosition(ConnectorPosition sourcePosition, ConnectorPosition targetPosition)
|
||||
{
|
||||
return sourcePosition == ConnectorPosition.Left && targetPosition == ConnectorPosition.Right
|
||||
|| sourcePosition == ConnectorPosition.Right && targetPosition == ConnectorPosition.Left
|
||||
|| sourcePosition == ConnectorPosition.Top && targetPosition == ConnectorPosition.Bottom
|
||||
|| sourcePosition == ConnectorPosition.Bottom && targetPosition == ConnectorPosition.Top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user