diff --git a/AuroraDesk.Core/Entities/ConnectionPoint.cs b/AuroraDesk.Core/Entities/ConnectionPoint.cs
index 07723b9..afaf5ed 100644
--- a/AuroraDesk.Core/Entities/ConnectionPoint.cs
+++ b/AuroraDesk.Core/Entities/ConnectionPoint.cs
@@ -8,10 +8,16 @@ namespace AuroraDesk.Core.Entities;
///
public class ConnectionPoint : ReactiveObject
{
+ private const double MinDiameter = 0;
+ private const double MaxDiameter = 400;
+
private string _label = string.Empty;
private ConnectionPointType _type = ConnectionPointType.Output;
private int _index;
private bool _isConnected;
+ private double _diameter = 10;
+ private string _color = "#3498DB";
+ private ConnectorPlacementMode _placement = ConnectorPlacementMode.Inside;
///
/// 连接点唯一标识符
@@ -54,6 +60,37 @@ public class ConnectionPoint : ReactiveObject
set => this.RaiseAndSetIfChanged(ref _isConnected, value);
}
+ ///
+ /// 附件圆直径
+ ///
+ public double Diameter
+ {
+ get => _diameter;
+ set
+ {
+ var clamped = Math.Clamp(value, MinDiameter, MaxDiameter);
+ this.RaiseAndSetIfChanged(ref _diameter, clamped);
+ }
+ }
+
+ ///
+ /// 附件圆颜色(Hex)
+ ///
+ public string Color
+ {
+ get => _color;
+ set => this.RaiseAndSetIfChanged(ref _color, value);
+ }
+
+ ///
+ /// 附件圆定位(内侧/外侧)
+ ///
+ public ConnectorPlacementMode Placement
+ {
+ get => _placement;
+ set => this.RaiseAndSetIfChanged(ref _placement, value);
+ }
+
///
/// 所属节点
///
diff --git a/AuroraDesk.Core/Entities/ConnectorAttachmentMode.cs b/AuroraDesk.Core/Entities/ConnectorAttachmentMode.cs
new file mode 100644
index 0000000..9e8da81
--- /dev/null
+++ b/AuroraDesk.Core/Entities/ConnectorAttachmentMode.cs
@@ -0,0 +1,28 @@
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 节点附件圆显示模式
+///
+public enum ConnectorAttachmentMode
+{
+ ///
+ /// 不显示附件圆
+ ///
+ None,
+
+ ///
+ /// 仅显示左侧附件圆
+ ///
+ LeftOnly,
+
+ ///
+ /// 仅显示右侧附件圆
+ ///
+ RightOnly,
+
+ ///
+ /// 左右两侧都显示附件圆
+ ///
+ Both
+}
+
diff --git a/AuroraDesk.Core/Entities/ConnectorPlacementMode.cs b/AuroraDesk.Core/Entities/ConnectorPlacementMode.cs
new file mode 100644
index 0000000..42908eb
--- /dev/null
+++ b/AuroraDesk.Core/Entities/ConnectorPlacementMode.cs
@@ -0,0 +1,18 @@
+namespace AuroraDesk.Core.Entities;
+
+///
+/// 附件圆定位模式(控制在节点边框内侧或外侧渲染)
+///
+public enum ConnectorPlacementMode
+{
+ ///
+ /// 在节点边框内侧对齐
+ ///
+ Inside,
+
+ ///
+ /// 在节点边框外侧对齐
+ ///
+ Outside
+}
+
diff --git a/AuroraDesk.Core/Entities/Node.cs b/AuroraDesk.Core/Entities/Node.cs
index 1d31003..7e8203f 100644
--- a/AuroraDesk.Core/Entities/Node.cs
+++ b/AuroraDesk.Core/Entities/Node.cs
@@ -1,6 +1,7 @@
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
namespace AuroraDesk.Core.Entities;
@@ -9,6 +10,11 @@ namespace AuroraDesk.Core.Entities;
///
public class Node : ReactiveObject
{
+ private const double MinDimension = 20;
+ private const double MaxDimension = 2000;
+ private const double MinConnectorSize = 0;
+ private const double MaxConnectorSize = 400;
+
private double _x;
private double _y;
private double _width = 120;
@@ -16,7 +22,23 @@ public class Node : ReactiveObject
private string _title = string.Empty;
private string _content = string.Empty;
private bool _isSelected;
+ private readonly NotifyCollectionChangedEventHandler _connectionPointsChangedHandler;
private ObservableCollection _connectionPoints = new();
+ private double _leftConnectorSize = 10;
+ private double _rightConnectorSize = 10;
+ private string _leftConnectorColor = "#3498DB";
+ private string _rightConnectorColor = "#FF6B6B";
+ private ConnectorPlacementMode _leftConnectorPlacement = ConnectorPlacementMode.Inside;
+ private ConnectorPlacementMode _rightConnectorPlacement = ConnectorPlacementMode.Inside;
+
+ ///
+ /// 构造函数
+ ///
+ public Node()
+ {
+ _connectionPointsChangedHandler = (_, _) => RaiseConnectionPointsChanged();
+ _connectionPoints.CollectionChanged += _connectionPointsChangedHandler;
+ }
///
/// 节点唯一标识符
@@ -47,7 +69,11 @@ public class Node : ReactiveObject
public double Width
{
get => _width;
- set => this.RaiseAndSetIfChanged(ref _width, value);
+ set
+ {
+ var clamped = Math.Clamp(value, MinDimension, MaxDimension);
+ this.RaiseAndSetIfChanged(ref _width, clamped);
+ }
}
///
@@ -56,7 +82,11 @@ public class Node : ReactiveObject
public double Height
{
get => _height;
- set => this.RaiseAndSetIfChanged(ref _height, value);
+ set
+ {
+ var clamped = Math.Clamp(value, MinDimension, MaxDimension);
+ this.RaiseAndSetIfChanged(ref _height, clamped);
+ }
}
///
@@ -92,7 +122,90 @@ public class Node : ReactiveObject
public ObservableCollection ConnectionPoints
{
get => _connectionPoints;
- set => this.RaiseAndSetIfChanged(ref _connectionPoints, value);
+ set
+ {
+ if (ReferenceEquals(_connectionPoints, value))
+ {
+ return;
+ }
+
+ if (_connectionPoints != null)
+ {
+ _connectionPoints.CollectionChanged -= _connectionPointsChangedHandler;
+ }
+
+ var newCollection = value ?? new ObservableCollection();
+ newCollection.CollectionChanged += _connectionPointsChangedHandler;
+
+ this.RaiseAndSetIfChanged(ref _connectionPoints, newCollection);
+ RaiseConnectionPointsChanged();
+ }
+ }
+
+ ///
+ /// 左侧附件圆尺寸
+ ///
+ public double LeftConnectorSize
+ {
+ get => _leftConnectorSize;
+ set
+ {
+ var clamped = Math.Clamp(value, MinConnectorSize, MaxConnectorSize);
+ this.RaiseAndSetIfChanged(ref _leftConnectorSize, clamped);
+ }
+ }
+
+ ///
+ /// 右侧附件圆尺寸
+ ///
+ public double RightConnectorSize
+ {
+ get => _rightConnectorSize;
+ set
+ {
+ var clamped = Math.Clamp(value, MinConnectorSize, MaxConnectorSize);
+ this.RaiseAndSetIfChanged(ref _rightConnectorSize, clamped);
+ }
+ }
+
+ ///
+ /// 左侧附件圆颜色(Hex)
+ ///
+ public string LeftConnectorColor
+ {
+ get => _leftConnectorColor;
+ set => this.RaiseAndSetIfChanged(ref _leftConnectorColor, value);
+ }
+
+ ///
+ /// 右侧附件圆颜色(Hex)
+ ///
+ public string RightConnectorColor
+ {
+ get => _rightConnectorColor;
+ set => this.RaiseAndSetIfChanged(ref _rightConnectorColor, value);
+ }
+
+ ///
+ /// 左侧附件圆定位方式
+ ///
+ public ConnectorPlacementMode LeftConnectorPlacement
+ {
+ get => _leftConnectorPlacement;
+ set => this.RaiseAndSetIfChanged(ref _leftConnectorPlacement, value);
+ }
+
+ ///
+ /// 右侧附件圆定位方式
+ ///
+ public ConnectorPlacementMode RightConnectorPlacement
+ {
+ get => _rightConnectorPlacement;
+ set => this.RaiseAndSetIfChanged(ref _rightConnectorPlacement, value);
+ }
+ private void RaiseConnectionPointsChanged()
+ {
+ this.RaisePropertyChanged(nameof(ConnectionPoints));
}
}
diff --git a/AuroraDesk.Core/Entities/NodeTemplate.cs b/AuroraDesk.Core/Entities/NodeTemplate.cs
index fc582bd..6e5d18c 100644
--- a/AuroraDesk.Core/Entities/NodeTemplate.cs
+++ b/AuroraDesk.Core/Entities/NodeTemplate.cs
@@ -12,10 +12,16 @@ public class NodeTemplate : ReactiveObject
private string _displayName = string.Empty;
private string _description = string.Empty;
private string _content = string.Empty;
- private int _inputCount = 1;
+ private int _inputCount = 3;
private int _outputCount = 3;
private double _width = 120;
private double _height = 80;
+ private double _leftConnectorSize = 10;
+ private double _rightConnectorSize = 10;
+ private ConnectorPlacementMode _leftConnectorPlacement = ConnectorPlacementMode.Inside;
+ private ConnectorPlacementMode _rightConnectorPlacement = ConnectorPlacementMode.Inside;
+ private string _leftConnectorColor = "#3498DB";
+ private string _rightConnectorColor = "#FF6B6B";
///
/// 模板唯一标识符
@@ -64,7 +70,11 @@ public class NodeTemplate : ReactiveObject
public int InputCount
{
get => _inputCount;
- set => this.RaiseAndSetIfChanged(ref _inputCount, value);
+ set
+ {
+ var clamped = Math.Max(3, value);
+ this.RaiseAndSetIfChanged(ref _inputCount, clamped);
+ }
}
///
@@ -73,7 +83,11 @@ public class NodeTemplate : ReactiveObject
public int OutputCount
{
get => _outputCount;
- set => this.RaiseAndSetIfChanged(ref _outputCount, value);
+ set
+ {
+ var clamped = Math.Max(3, value);
+ this.RaiseAndSetIfChanged(ref _outputCount, clamped);
+ }
}
///
@@ -94,6 +108,60 @@ public class NodeTemplate : ReactiveObject
set => this.RaiseAndSetIfChanged(ref _height, value);
}
+ ///
+ /// 左侧附件圆尺寸
+ ///
+ public double LeftConnectorSize
+ {
+ get => _leftConnectorSize;
+ set => this.RaiseAndSetIfChanged(ref _leftConnectorSize, value);
+ }
+
+ ///
+ /// 右侧附件圆尺寸
+ ///
+ public double RightConnectorSize
+ {
+ get => _rightConnectorSize;
+ set => this.RaiseAndSetIfChanged(ref _rightConnectorSize, value);
+ }
+
+ ///
+ /// 左侧附件圆颜色(Hex)
+ ///
+ public string LeftConnectorColor
+ {
+ get => _leftConnectorColor;
+ set => this.RaiseAndSetIfChanged(ref _leftConnectorColor, value);
+ }
+
+ ///
+ /// 右侧附件圆颜色(Hex)
+ ///
+ public string RightConnectorColor
+ {
+ get => _rightConnectorColor;
+ set => this.RaiseAndSetIfChanged(ref _rightConnectorColor, value);
+ }
+
+ ///
+ /// 左侧附件圆定位
+ ///
+ public ConnectorPlacementMode LeftConnectorPlacement
+ {
+ get => _leftConnectorPlacement;
+ set => this.RaiseAndSetIfChanged(ref _leftConnectorPlacement, value);
+ }
+
+ ///
+ /// 右侧附件圆定位
+ ///
+ public ConnectorPlacementMode RightConnectorPlacement
+ {
+ get => _rightConnectorPlacement;
+ set => this.RaiseAndSetIfChanged(ref _rightConnectorPlacement, value);
+ }
+
///
/// 根据模板创建节点实例
///
@@ -106,7 +174,13 @@ public class NodeTemplate : ReactiveObject
Title = DisplayName,
Content = Content,
Width = Width,
- Height = Height
+ Height = Height,
+ LeftConnectorSize = LeftConnectorSize,
+ RightConnectorSize = RightConnectorSize,
+ LeftConnectorColor = LeftConnectorColor,
+ RightConnectorColor = RightConnectorColor,
+ LeftConnectorPlacement = LeftConnectorPlacement,
+ RightConnectorPlacement = RightConnectorPlacement
};
// 创建输入连接点(左侧)
@@ -117,7 +191,10 @@ public class NodeTemplate : ReactiveObject
Label = "",
Type = ConnectionPointType.Input,
Index = i,
- Node = node
+ Node = node,
+ Diameter = LeftConnectorSize,
+ Color = LeftConnectorColor,
+ Placement = LeftConnectorPlacement
});
}
@@ -129,7 +206,10 @@ public class NodeTemplate : ReactiveObject
Label = (i + 1).ToString(),
Type = ConnectionPointType.Output,
Index = i,
- Node = node
+ Node = node,
+ Diameter = RightConnectorSize,
+ Color = RightConnectorColor,
+ Placement = RightConnectorPlacement
});
}
diff --git a/AuroraDesk.Presentation/Controls/ConnectorColumnPanel.cs b/AuroraDesk.Presentation/Controls/ConnectorColumnPanel.cs
new file mode 100644
index 0000000..7dbf64d
--- /dev/null
+++ b/AuroraDesk.Presentation/Controls/ConnectorColumnPanel.cs
@@ -0,0 +1,61 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace AuroraDesk.Presentation.Controls;
+
+///
+/// 垂直均匀分布子元素的面板,用于渲染节点左右侧连接点。
+///
+public class ConnectorColumnPanel : Panel
+{
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var maxWidth = 0d;
+ var totalHeight = 0d;
+
+ foreach (var child in Children)
+ {
+ child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ var desired = child.DesiredSize;
+ maxWidth = Math.Max(maxWidth, desired.Width);
+ totalHeight += desired.Height;
+ }
+
+ var height = double.IsInfinity(availableSize.Height) ? totalHeight : availableSize.Height;
+ return new Size(maxWidth, height);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var count = Children.Count;
+ if (count == 0)
+ {
+ return finalSize;
+ }
+
+ var totalHeight = 0d;
+ foreach (var child in Children)
+ {
+ totalHeight += child.DesiredSize.Height;
+ }
+
+ var availableHeight = finalSize.Height;
+ var spacing = count > 0
+ ? Math.Max(0, (availableHeight - totalHeight) / (count + 1))
+ : 0;
+
+ var currentY = spacing;
+ foreach (var child in Children)
+ {
+ var childHeight = child.DesiredSize.Height;
+ var childWidth = Math.Min(child.DesiredSize.Width, finalSize.Width);
+ var x = (finalSize.Width - childWidth) / 2;
+ child.Arrange(new Rect(x, currentY, childWidth, childHeight));
+ currentY += childHeight + spacing;
+ }
+
+ return finalSize;
+ }
+}
+
diff --git a/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
index c0ea34a..3d57ebd 100644
--- a/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
+++ b/AuroraDesk.Presentation/Converters/NodeCanvasConverters.cs
@@ -3,7 +3,7 @@ using Avalonia.Media;
using Avalonia;
using AuroraDesk.Core.Entities;
using System;
-using System.Collections.ObjectModel;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -16,10 +16,17 @@ public static class NodeCanvasConverters
{
public static readonly IValueConverter BooleanToBorderBrushConverter = new BooleanToBorderBrushConverter();
public static readonly IValueConverter BooleanToBorderThicknessConverter = new BooleanToBorderThicknessConverter();
- public static readonly IValueConverter FilterInputPointsConverter = new FilterInputPointsConverter();
- public static readonly IValueConverter FilterOutputPointsConverter = new FilterOutputPointsConverter();
public static readonly IValueConverter NodeToSelectionTextConverter = new NodeToSelectionTextConverter();
public static readonly IValueConverter IsNotNullConverter = new IsNotNullConverter();
+ public static readonly IValueConverter IsNullConverter = new IsNullConverter();
+ public static readonly IValueConverter DoubleToStringConverter = new DoubleToStringConverter();
+ public static readonly IValueConverter ColorHexToBrushConverter = new ColorHexToBrushConverter();
+ public static readonly IValueConverter ConnectorAttachmentModeToTextConverter = new ConnectorAttachmentModeToTextConverter();
+ public static readonly IValueConverter ConnectionPointsToInputsConverter = new ConnectionPointsToInputsConverter();
+ public static readonly IValueConverter ConnectionPointsToOutputsConverter = new ConnectionPointsToOutputsConverter();
+ public static readonly IValueConverter ConnectorPlacementToTextConverter = new ConnectorPlacementToTextConverter();
+ public static readonly IMultiValueConverter ConnectorPlacementMarginConverter = new ConnectorPlacementMarginConverter();
+ public static readonly IValueConverter IndexToDisplayTextConverter = new IndexToDisplayTextConverter();
}
///
@@ -74,15 +81,15 @@ public class BooleanToBorderThicknessConverter : IValueConverter
///
/// 过滤输入连接点转换器
///
-public class FilterInputPointsConverter : IValueConverter
+public class NodeToSelectionTextConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- if (value is ObservableCollection points)
+ if (value is Node node)
{
- return points.Where(p => p.Type == ConnectionPointType.Input).ToList();
+ return $"已选中1个对象 (节点)";
}
- return Enumerable.Empty();
+ return "未选中对象";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
@@ -92,17 +99,57 @@ public class FilterInputPointsConverter : IValueConverter
}
///
-/// 过滤输出连接点转换器
+/// 非空转换器(用于控制可见性)
///
-public class FilterOutputPointsConverter : IValueConverter
+public class IsNotNullConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- if (value is ObservableCollection points)
+ return value != null;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 空值转换器(用于控制可见性)
+///
+public class IsNullConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return value == null;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 将附件圆模式枚举转换为中文描述
+///
+public class ConnectorAttachmentModeToTextConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is ConnectorAttachmentMode mode)
{
- return points.Where(p => p.Type == ConnectionPointType.Output).ToList();
+ return mode switch
+ {
+ ConnectorAttachmentMode.None => "不显示",
+ ConnectorAttachmentMode.LeftOnly => "仅左侧",
+ ConnectorAttachmentMode.RightOnly => "仅右侧",
+ ConnectorAttachmentMode.Both => "左右两侧",
+ _ => mode.ToString()
+ };
}
- return Enumerable.Empty();
+
+ return string.Empty;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
@@ -112,17 +159,21 @@ public class FilterOutputPointsConverter : IValueConverter
}
///
-/// 节点到选择文本转换器
+/// 过滤节点输入连接点集合
///
-public class NodeToSelectionTextConverter : IValueConverter
+public class ConnectionPointsToInputsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- if (value is Node node)
+ if (value is IEnumerable points)
{
- return $"已选中1个对象 (节点)";
+ return points
+ .Where(p => p.Type == ConnectionPointType.Input)
+ .OrderBy(p => p.Index)
+ .ToList();
}
- return "未选中对象";
+
+ return Array.Empty();
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
@@ -132,13 +183,154 @@ public class NodeToSelectionTextConverter : IValueConverter
}
///
-/// 非空转换器(用于控制可见性)
+/// 过滤节点输出连接点集合
///
-public class IsNotNullConverter : IValueConverter
+public class ConnectionPointsToOutputsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
- return value != null;
+ if (value is IEnumerable points)
+ {
+ return points
+ .Where(p => p.Type == ConnectionPointType.Output)
+ .OrderBy(p => p.Index)
+ .ToList();
+ }
+
+ return Array.Empty();
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 将附件圆定位模式转换为显示文本
+///
+public class ConnectorPlacementToTextConverter : IValueConverter
+{
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is ConnectorPlacementMode placement)
+ {
+ return placement switch
+ {
+ ConnectorPlacementMode.Inside => "内侧",
+ ConnectorPlacementMode.Outside => "外侧",
+ _ => placement.ToString()
+ };
+ }
+
+ return string.Empty;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+///
+/// 根据附件圆定位和尺寸计算 ItemsControl 外边距
+///
+public class ConnectorPlacementMarginConverter : IMultiValueConverter
+{
+ public object? Convert(IList