You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

738 lines
24 KiB

using AuroraDesk.Core.Entities;
using AuroraDesk.Core.Interfaces;
using AuroraDesk.Presentation.ViewModels.Base;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Linq;
using Microsoft.Extensions.Logging;
namespace AuroraDesk.Presentation.ViewModels.Pages;
/// <summary>
/// 节点画布页面 ViewModel
/// </summary>
public class NodeCanvasPageViewModel : RoutableViewModel
{
private readonly INodeCanvasService _nodeCanvasService;
private readonly ILogger<NodeCanvasPageViewModel>? _logger;
private Node? _selectedNode;
private IDisposable? _selectedNodeSubscription;
private ConnectionPoint? _connectingSourcePoint;
private bool _isConnecting;
private double _canvasOffsetX;
private double _canvasOffsetY;
private double _canvasZoom = 1.0;
private int _selectedLeftConnectorCount;
private int _selectedRightConnectorCount;
public ObservableCollection<Node> Nodes => _nodeCanvasService.Nodes;
public ObservableCollection<Connection> Connections => _nodeCanvasService.Connections;
private ObservableCollection<NodeTemplate> _nodeTemplates = new();
public ObservableCollection<NodeTemplate> NodeTemplates
{
get => _nodeTemplates;
set => this.RaiseAndSetIfChanged(ref _nodeTemplates, value);
}
public IReadOnlyList<int> ConnectorCountOptions => _connectorCountOptions;
/// <summary>
/// 选中的节点
/// </summary>
private const double DefaultConnectorSize = 10d;
private const int MinConnectorCount = 0;
private const int MaxConnectorCount = 4;
private const int DefaultConnectorCount = 3;
private const string DefaultLeftConnectorColor = "#2D9CDB";
private const string DefaultRightConnectorColor = "#EB5757";
private readonly IReadOnlyList<int> _connectorCountOptions = Enumerable.Range(MinConnectorCount, MaxConnectorCount - MinConnectorCount + 1).ToList();
public Node? SelectedNode
{
get => _selectedNode;
set
{
if (ReferenceEquals(_selectedNode, value))
{
return;
}
this.RaiseAndSetIfChanged(ref _selectedNode, value);
_selectedNodeSubscription?.Dispose();
_selectedNodeSubscription = null;
RefreshSelectedConnectorCounts();
if (value != null)
{
var disposables = new CompositeDisposable();
NotifyCollectionChangedEventHandler? handler = null;
handler = (_, __) =>
{
RefreshSelectedConnectorCounts();
this.RaisePropertyChanged(nameof(SelectedLeftConnectorCount));
this.RaisePropertyChanged(nameof(SelectedRightConnectorCount));
this.RaisePropertyChanged(nameof(SelectedConnectorMode));
};
value.ConnectionPoints.CollectionChanged += handler;
disposables.Add(Disposable.Create(() => value.ConnectionPoints.CollectionChanged -= handler));
_selectedNodeSubscription = disposables;
}
this.RaisePropertyChanged(nameof(SelectedConnectorMode));
this.RaisePropertyChanged(nameof(SelectedLeftConnectorCount));
this.RaisePropertyChanged(nameof(SelectedRightConnectorCount));
}
}
public ConnectorAttachmentMode SelectedConnectorMode
{
get
{
if (SelectedNode == null)
{
return ConnectorAttachmentMode.None;
}
var hasLeft = _selectedLeftConnectorCount > 0;
var hasRight = _selectedRightConnectorCount > 0;
return (hasLeft, hasRight) switch
{
(false, false) => ConnectorAttachmentMode.None,
(true, false) => ConnectorAttachmentMode.LeftOnly,
(false, true) => ConnectorAttachmentMode.RightOnly,
_ => ConnectorAttachmentMode.Both
};
}
set
{
if (SelectedNode == null)
{
return;
}
if (SelectedConnectorMode == value)
{
return;
}
switch (value)
{
case ConnectorAttachmentMode.None:
SetConnectorCount(SelectedNode, ConnectionPointType.Input, 0);
SetConnectorCount(SelectedNode, ConnectionPointType.Output, 0);
break;
case ConnectorAttachmentMode.LeftOnly:
if (SelectedLeftConnectorCount == 0)
{
SetConnectorCount(SelectedNode, ConnectionPointType.Input, 1);
}
SetConnectorCount(SelectedNode, ConnectionPointType.Output, 0);
break;
case ConnectorAttachmentMode.RightOnly:
SetConnectorCount(SelectedNode, ConnectionPointType.Input, 0);
if (SelectedRightConnectorCount == 0)
{
SetConnectorCount(SelectedNode, ConnectionPointType.Output, 1);
}
break;
case ConnectorAttachmentMode.Both:
if (SelectedLeftConnectorCount == 0)
{
SetConnectorCount(SelectedNode, ConnectionPointType.Input, 1);
}
if (SelectedRightConnectorCount == 0)
{
SetConnectorCount(SelectedNode, ConnectionPointType.Output, 1);
}
break;
default:
return;
}
this.RaisePropertyChanged(nameof(SelectedConnectorMode));
}
}
public int SelectedLeftConnectorCount
{
get => _selectedLeftConnectorCount;
set
{
if (SelectedNode == null)
{
return;
}
SetConnectorCount(SelectedNode, ConnectionPointType.Input, value);
}
}
public int SelectedRightConnectorCount
{
get => _selectedRightConnectorCount;
set
{
if (SelectedNode == null)
{
return;
}
SetConnectorCount(SelectedNode, ConnectionPointType.Output, value);
}
}
/// <summary>
/// 正在连接的源连接点
/// </summary>
public ConnectionPoint? ConnectingSourcePoint
{
get => _connectingSourcePoint;
set => this.RaiseAndSetIfChanged(ref _connectingSourcePoint, value);
}
/// <summary>
/// 是否正在连接模式
/// </summary>
public bool IsConnecting
{
get => _isConnecting;
set => this.RaiseAndSetIfChanged(ref _isConnecting, value);
}
/// <summary>
/// 画布偏移X
/// </summary>
public double CanvasOffsetX
{
get => _canvasOffsetX;
set => this.RaiseAndSetIfChanged(ref _canvasOffsetX, value);
}
/// <summary>
/// 画布偏移Y
/// </summary>
public double CanvasOffsetY
{
get => _canvasOffsetY;
set => this.RaiseAndSetIfChanged(ref _canvasOffsetY, value);
}
/// <summary>
/// 画布缩放
/// </summary>
public double CanvasZoom
{
get => _canvasZoom;
set => this.RaiseAndSetIfChanged(ref _canvasZoom, value);
}
// 命令
public ReactiveCommand<Unit, Unit> ClearCanvasCommand { get; }
public ReactiveCommand<Node, Unit> DeleteNodeCommand { get; }
public ReactiveCommand<ConnectionPoint, Unit> StartConnectionCommand { get; }
public ReactiveCommand<ConnectionPoint, Unit> CompleteConnectionCommand { get; }
public ReactiveCommand<Connection, Unit> DeleteConnectionCommand { get; }
public ReactiveCommand<(double x, double y), Unit> AddNodeCommand { get; }
public ReactiveCommand<(NodeTemplate template, double x, double y), Unit> AddNodeFromTemplateCommand { get; }
public ReactiveCommand<NodeTemplate, Unit> AddNodeFromTemplateAtCenterCommand { get; }
public ReactiveCommand<NodeTemplate, Unit> AddNodeToCanvasCommand { get; }
public ReactiveCommand<Unit, Unit> ZoomInCommand { get; }
public ReactiveCommand<Unit, Unit> ZoomOutCommand { get; }
public ReactiveCommand<Unit, Unit> ResetZoomCommand { get; }
public ReactiveCommand<double, Unit> SetZoomCommand { get; }
public NodeCanvasPageViewModel(
IScreen screen,
INodeCanvasService nodeCanvasService,
ILogger<NodeCanvasPageViewModel>? logger = null) : base(screen, "node-canvas")
{
_nodeCanvasService = nodeCanvasService;
_logger = logger;
// 初始化节点模板
InitializeNodeTemplates();
// 初始化命令
ClearCanvasCommand = ReactiveCommand.Create(ClearCanvas);
DeleteNodeCommand = ReactiveCommand.Create<Node>(DeleteNode);
StartConnectionCommand = ReactiveCommand.Create<ConnectionPoint>(StartConnection);
CompleteConnectionCommand = ReactiveCommand.Create<ConnectionPoint>(CompleteConnection);
DeleteConnectionCommand = ReactiveCommand.Create<Connection>(DeleteConnection);
AddNodeCommand = ReactiveCommand.Create<(double x, double y)>(AddNode);
AddNodeFromTemplateCommand = ReactiveCommand.Create<(NodeTemplate template, double x, double y)>(AddNodeFromTemplate);
AddNodeFromTemplateAtCenterCommand = ReactiveCommand.Create<NodeTemplate>(template => AddNodeFromTemplateAtCenter(template));
AddNodeToCanvasCommand = ReactiveCommand.Create<NodeTemplate>(AddNodeToCanvas);
ZoomInCommand = ReactiveCommand.Create(ZoomIn);
ZoomOutCommand = ReactiveCommand.Create(ZoomOut);
ResetZoomCommand = ReactiveCommand.Create(ResetZoom);
SetZoomCommand = ReactiveCommand.Create<double>(SetZoom);
// 监听选中节点变化
this.WhenAnyValue(x => x.SelectedNode)
.Subscribe(node =>
{
// 取消其他节点的选中状态
foreach (var n in Nodes)
{
n.IsSelected = n == node;
}
});
_logger?.LogInformation("NodeCanvasPageViewModel 已创建");
}
/// <summary>
/// 初始化节点模板
/// </summary>
private void InitializeNodeTemplates()
{
NodeTemplates.Clear();
NodeTemplates.Add(new NodeTemplate
{
Name = "rectangle-vertical",
DisplayName = "竖向矩形",
Description = "高>宽的矩形组件",
Content = string.Empty,
InputCount = 3,
OutputCount = 3,
Width = 90,
Height = 160,
LeftConnectorSize = DefaultConnectorSize,
RightConnectorSize = DefaultConnectorSize,
LeftConnectorColor = "#2D9CDB",
RightConnectorColor = "#EB5757",
LeftConnectorPlacement = ConnectorPlacementMode.Outside,
RightConnectorPlacement = ConnectorPlacementMode.Outside
});
NodeTemplates.Add(new NodeTemplate
{
Name = "rectangle-horizontal",
DisplayName = "横向矩形",
Description = "宽>高的矩形组件",
Content = string.Empty,
InputCount = 3,
OutputCount = 3,
Width = 200,
Height = 90,
LeftConnectorSize = DefaultConnectorSize,
RightConnectorSize = DefaultConnectorSize,
LeftConnectorColor = "#2D9CDB",
RightConnectorColor = "#EB5757",
LeftConnectorPlacement = ConnectorPlacementMode.Outside,
RightConnectorPlacement = ConnectorPlacementMode.Outside
});
}
/// <summary>
/// 添加节点到画布
/// </summary>
private void AddNode((double x, double y) position)
{
// 确保位置在合理范围内(至少为正数)
var x = Math.Max(0, position.x);
var y = Math.Max(0, position.y);
var node = new Node
{
X = x,
Y = y,
Title = $"矩形 {Nodes.Count + 1}",
Content = string.Empty,
Width = 200,
Height = 90,
LeftConnectorSize = DefaultConnectorSize,
RightConnectorSize = DefaultConnectorSize,
LeftConnectorColor = "#2D9CDB",
RightConnectorColor = "#EB5757",
LeftConnectorPlacement = ConnectorPlacementMode.Outside,
RightConnectorPlacement = ConnectorPlacementMode.Outside
};
// 默认提供输入和输出连接点
for (int i = 0; i < DefaultConnectorCount; i++)
{
node.ConnectionPoints.Add(new ConnectionPoint
{
Label = string.Empty,
Type = ConnectionPointType.Input,
Index = i,
Node = node,
Diameter = node.LeftConnectorSize,
Color = node.LeftConnectorColor,
Placement = node.LeftConnectorPlacement
});
}
for (int i = 0; i < DefaultConnectorCount; i++)
{
node.ConnectionPoints.Add(new ConnectionPoint
{
Label = (i + 1).ToString(),
Type = ConnectionPointType.Output,
Index = i,
Node = node,
Diameter = node.RightConnectorSize,
Color = node.RightConnectorColor,
Placement = node.RightConnectorPlacement
});
}
_nodeCanvasService.AddNode(node);
SelectedNode = node;
_logger?.LogDebug("添加节点: {NodeId} 在位置 ({X}, {Y})", node.Id, position.x, position.y);
}
private void SetConnectorCount(Node node, ConnectionPointType type, int desiredCount)
{
UpdateConnectorCount(node, type, desiredCount);
RefreshSelectedConnectorCounts();
if (type == ConnectionPointType.Input)
{
this.RaisePropertyChanged(nameof(SelectedLeftConnectorCount));
}
else
{
this.RaisePropertyChanged(nameof(SelectedRightConnectorCount));
}
this.RaisePropertyChanged(nameof(SelectedConnectorMode));
}
private void RefreshSelectedConnectorCounts()
{
if (SelectedNode == null)
{
_selectedLeftConnectorCount = 0;
_selectedRightConnectorCount = 0;
return;
}
_selectedLeftConnectorCount = SelectedNode.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Input);
_selectedRightConnectorCount = SelectedNode.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Output);
}
private void UpdateConnectorCount(Node node, ConnectionPointType type, int desiredCount)
{
if (node == null)
{
return;
}
desiredCount = Math.Clamp(desiredCount, MinConnectorCount, MaxConnectorCount);
var existing = node.ConnectionPoints
.Where(p => p.Type == type)
.OrderBy(p => p.Index)
.ToList();
if (existing.Count > desiredCount)
{
for (int i = existing.Count - 1; i >= desiredCount; i--)
{
node.ConnectionPoints.Remove(existing[i]);
}
}
else if (existing.Count < desiredCount)
{
for (int i = existing.Count; i < desiredCount; i++)
{
node.ConnectionPoints.Add(CreateConnectionPoint(node, type, i));
}
}
RenumberConnectionPoints(node, type);
}
private ConnectionPoint CreateConnectionPoint(Node node, ConnectionPointType type, int index)
{
var existingSameSide = node.ConnectionPoints
.Where(p => p.Type == type)
.OrderBy(p => p.Index)
.LastOrDefault();
var defaultDiameter = existingSameSide?.Diameter ?? (type == ConnectionPointType.Input ? node.LeftConnectorSize : node.RightConnectorSize);
if (defaultDiameter <= 0)
{
defaultDiameter = DefaultConnectorSize;
}
var defaultColor = existingSameSide?.Color ?? (type == ConnectionPointType.Input ? node.LeftConnectorColor : node.RightConnectorColor);
if (string.IsNullOrWhiteSpace(defaultColor))
{
defaultColor = type == ConnectionPointType.Input ? DefaultLeftConnectorColor : DefaultRightConnectorColor;
}
var defaultPlacement = existingSameSide?.Placement ?? (type == ConnectionPointType.Input ? node.LeftConnectorPlacement : node.RightConnectorPlacement);
return new ConnectionPoint
{
Type = type,
Index = index,
Label = type == ConnectionPointType.Output ? (index + 1).ToString() : string.Empty,
Node = node,
Diameter = defaultDiameter,
Color = defaultColor,
Placement = defaultPlacement
};
}
private void RenumberConnectionPoints(Node node, ConnectionPointType type)
{
var points = node.ConnectionPoints
.Where(p => p.Type == type)
.OrderBy(p => p.Index)
.ToList();
for (int i = 0; i < points.Count; i++)
{
points[i].Index = i;
if (type == ConnectionPointType.Output)
{
points[i].Label = (i + 1).ToString();
}
}
}
/// <summary>
/// 删除节点
/// </summary>
private void DeleteNode(Node node)
{
if (node == null) return;
_nodeCanvasService.RemoveNode(node);
if (SelectedNode == node)
{
SelectedNode = null;
}
_logger?.LogDebug("删除节点: {NodeId}", node.Id);
}
/// <summary>
/// 开始连接
/// </summary>
private void StartConnection(ConnectionPoint connectionPoint)
{
if (connectionPoint == null || connectionPoint.Type != ConnectionPointType.Output)
return;
ConnectingSourcePoint = connectionPoint;
IsConnecting = true;
_logger?.LogDebug("开始连接,源连接点: {PointId}", connectionPoint.Id);
}
/// <summary>
/// 完成连接
/// </summary>
private void CompleteConnection(ConnectionPoint targetPoint)
{
if (targetPoint == null || ConnectingSourcePoint == null)
{
CancelConnection();
return;
}
if (targetPoint.Type != ConnectionPointType.Input)
{
CancelConnection();
return;
}
var success = _nodeCanvasService.CreateConnection(ConnectingSourcePoint, targetPoint);
if (success)
{
_logger?.LogDebug("连接成功: {SourceId} -> {TargetId}",
ConnectingSourcePoint.Id, targetPoint.Id);
}
else
{
_logger?.LogWarning("连接失败: {SourceId} -> {TargetId}",
ConnectingSourcePoint.Id, targetPoint.Id);
}
CancelConnection();
}
/// <summary>
/// 取消连接
/// </summary>
public void CancelConnection()
{
ConnectingSourcePoint = null;
IsConnecting = false;
}
/// <summary>
/// 删除连接
/// </summary>
private void DeleteConnection(Connection connection)
{
if (connection == null) return;
_nodeCanvasService.RemoveConnection(connection);
_logger?.LogDebug("删除连接: {ConnectionId}", connection.Id);
}
/// <summary>
/// 清空画布
/// </summary>
private void ClearCanvas()
{
_nodeCanvasService.Clear();
SelectedNode = null;
_logger?.LogDebug("清空画布");
}
/// <summary>
/// 更新节点位置
/// </summary>
public void UpdateNodePosition(Node node, double x, double y)
{
_nodeCanvasService.UpdateNodePosition(node, x, y);
}
/// <summary>
/// 从模板添加节点到画布
/// </summary>
private void AddNodeFromTemplate((NodeTemplate template, double x, double y) args)
{
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeFromTemplate 被调用");
System.Diagnostics.Debug.WriteLine($"[ViewModel] 模板: {args.template.Name}, 位置: ({args.x}, {args.y})");
try
{
var node = args.template.CreateNode(args.x, args.y);
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点创建成功: {node.Title}, ID: {node.Id}");
_nodeCanvasService.AddNode(node);
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点已添加到服务,当前节点数量: {Nodes.Count}");
SelectedNode = node;
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点已设置为选中状态");
_logger?.LogDebug("从模板添加节点: {TemplateName} 在位置 ({X}, {Y})",
args.template.Name, args.x, args.y);
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeFromTemplate 完成");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeFromTemplate 异常: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[ViewModel] 堆栈: {ex.StackTrace}");
_logger?.LogError(ex, "添加节点时发生错误");
}
}
/// <summary>
/// 从模板添加节点到画布中心
/// </summary>
private void AddNodeFromTemplateAtCenter(NodeTemplate template)
{
// 默认位置:画布中心区域
double centerX = 400;
double centerY = 300;
var node = template.CreateNode(centerX, centerY);
_nodeCanvasService.AddNode(node);
SelectedNode = node;
_logger?.LogDebug("从模板添加节点到中心: {TemplateName} 在位置 ({X}, {Y})",
template.Name, centerX, centerY);
}
/// <summary>
/// 添加节点到画布(简化版本,默认位置)
/// </summary>
private void AddNodeToCanvas(NodeTemplate template)
{
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeToCanvas 被调用,模板: {template?.Name}");
if (template == null)
{
System.Diagnostics.Debug.WriteLine($"[ViewModel] 错误: template为null");
return;
}
try
{
// 默认位置:画布中心区域
double centerX = 400;
double centerY = 300;
var node = template.CreateNode(centerX, centerY);
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点创建成功: {node.Title}, ID: {node.Id}, 位置: ({centerX}, {centerY})");
_nodeCanvasService.AddNode(node);
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点已添加到服务,当前节点数量: {Nodes.Count}");
SelectedNode = node;
System.Diagnostics.Debug.WriteLine($"[ViewModel] 节点已设置为选中状态");
_logger?.LogDebug("添加节点到画布: {TemplateName} 在位置 ({X}, {Y})",
template.Name, centerX, centerY);
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeToCanvas 完成");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ViewModel] AddNodeToCanvas 异常: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[ViewModel] 堆栈: {ex.StackTrace}");
_logger?.LogError(ex, "添加节点时发生错误");
}
}
/// <summary>
/// 放大
/// </summary>
private void ZoomIn()
{
var newZoom = Math.Min(_canvasZoom * 1.2, 3.0); // 最大3倍
CanvasZoom = newZoom;
_logger?.LogDebug("放大到: {Zoom}", newZoom);
}
/// <summary>
/// 缩小
/// </summary>
private void ZoomOut()
{
var newZoom = Math.Max(_canvasZoom / 1.2, 0.1); // 最小0.1倍
CanvasZoom = newZoom;
_logger?.LogDebug("缩小到: {Zoom}", newZoom);
}
/// <summary>
/// 重置缩放
/// </summary>
private void ResetZoom()
{
CanvasZoom = 1.0;
_logger?.LogDebug("重置缩放到: 1.0");
}
/// <summary>
/// 设置缩放
/// </summary>
private void SetZoom(double zoom)
{
var newZoom = Math.Max(0.1, Math.Min(zoom, 3.0)); // 限制在0.1到3.0之间
CanvasZoom = newZoom;
_logger?.LogDebug("设置缩放到: {Zoom}", newZoom);
}
}