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; /// /// 节点画布页面 ViewModel /// public class NodeCanvasPageViewModel : RoutableViewModel { private readonly INodeCanvasService _nodeCanvasService; private readonly ILogger? _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 Nodes => _nodeCanvasService.Nodes; public ObservableCollection Connections => _nodeCanvasService.Connections; private ObservableCollection _nodeTemplates = new(); public ObservableCollection NodeTemplates { get => _nodeTemplates; set => this.RaiseAndSetIfChanged(ref _nodeTemplates, value); } public IReadOnlyList ConnectorCountOptions => _connectorCountOptions; /// /// 选中的节点 /// 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 _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); } } /// /// 正在连接的源连接点 /// public ConnectionPoint? ConnectingSourcePoint { get => _connectingSourcePoint; set => this.RaiseAndSetIfChanged(ref _connectingSourcePoint, value); } /// /// 是否正在连接模式 /// public bool IsConnecting { get => _isConnecting; set => this.RaiseAndSetIfChanged(ref _isConnecting, value); } /// /// 画布偏移X /// public double CanvasOffsetX { get => _canvasOffsetX; set => this.RaiseAndSetIfChanged(ref _canvasOffsetX, value); } /// /// 画布偏移Y /// public double CanvasOffsetY { get => _canvasOffsetY; set => this.RaiseAndSetIfChanged(ref _canvasOffsetY, value); } /// /// 画布缩放 /// public double CanvasZoom { get => _canvasZoom; set => this.RaiseAndSetIfChanged(ref _canvasZoom, value); } // 命令 public ReactiveCommand ClearCanvasCommand { get; } public ReactiveCommand DeleteNodeCommand { get; } public ReactiveCommand StartConnectionCommand { get; } public ReactiveCommand CompleteConnectionCommand { get; } public ReactiveCommand DeleteConnectionCommand { get; } public ReactiveCommand<(double x, double y), Unit> AddNodeCommand { get; } public ReactiveCommand<(NodeTemplate template, double x, double y), Unit> AddNodeFromTemplateCommand { get; } public ReactiveCommand AddNodeFromTemplateAtCenterCommand { get; } public ReactiveCommand AddNodeToCanvasCommand { get; } public ReactiveCommand ZoomInCommand { get; } public ReactiveCommand ZoomOutCommand { get; } public ReactiveCommand ResetZoomCommand { get; } public ReactiveCommand SetZoomCommand { get; } public NodeCanvasPageViewModel( IScreen screen, INodeCanvasService nodeCanvasService, ILogger? logger = null) : base(screen, "node-canvas") { _nodeCanvasService = nodeCanvasService; _logger = logger; // 初始化节点模板 InitializeNodeTemplates(); // 初始化命令 ClearCanvasCommand = ReactiveCommand.Create(ClearCanvas); DeleteNodeCommand = ReactiveCommand.Create(DeleteNode); StartConnectionCommand = ReactiveCommand.Create(StartConnection); CompleteConnectionCommand = ReactiveCommand.Create(CompleteConnection); DeleteConnectionCommand = ReactiveCommand.Create(DeleteConnection); AddNodeCommand = ReactiveCommand.Create<(double x, double y)>(AddNode); AddNodeFromTemplateCommand = ReactiveCommand.Create<(NodeTemplate template, double x, double y)>(AddNodeFromTemplate); AddNodeFromTemplateAtCenterCommand = ReactiveCommand.Create(template => AddNodeFromTemplateAtCenter(template)); AddNodeToCanvasCommand = ReactiveCommand.Create(AddNodeToCanvas); ZoomInCommand = ReactiveCommand.Create(ZoomIn); ZoomOutCommand = ReactiveCommand.Create(ZoomOut); ResetZoomCommand = ReactiveCommand.Create(ResetZoom); SetZoomCommand = ReactiveCommand.Create(SetZoom); // 监听选中节点变化 this.WhenAnyValue(x => x.SelectedNode) .Subscribe(node => { // 取消其他节点的选中状态 foreach (var n in Nodes) { n.IsSelected = n == node; } }); _logger?.LogInformation("NodeCanvasPageViewModel 已创建"); } /// /// 初始化节点模板 /// 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 }); } /// /// 添加节点到画布 /// 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(); } } } /// /// 删除节点 /// private void DeleteNode(Node node) { if (node == null) return; _nodeCanvasService.RemoveNode(node); if (SelectedNode == node) { SelectedNode = null; } _logger?.LogDebug("删除节点: {NodeId}", node.Id); } /// /// 开始连接 /// private void StartConnection(ConnectionPoint connectionPoint) { if (connectionPoint == null || connectionPoint.Type != ConnectionPointType.Output) return; ConnectingSourcePoint = connectionPoint; IsConnecting = true; _logger?.LogDebug("开始连接,源连接点: {PointId}", connectionPoint.Id); } /// /// 完成连接 /// 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(); } /// /// 取消连接 /// public void CancelConnection() { ConnectingSourcePoint = null; IsConnecting = false; } /// /// 删除连接 /// private void DeleteConnection(Connection connection) { if (connection == null) return; _nodeCanvasService.RemoveConnection(connection); _logger?.LogDebug("删除连接: {ConnectionId}", connection.Id); } /// /// 清空画布 /// private void ClearCanvas() { _nodeCanvasService.Clear(); SelectedNode = null; _logger?.LogDebug("清空画布"); } /// /// 更新节点位置 /// public void UpdateNodePosition(Node node, double x, double y) { _nodeCanvasService.UpdateNodePosition(node, x, y); } /// /// 从模板添加节点到画布 /// 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, "添加节点时发生错误"); } } /// /// 从模板添加节点到画布中心 /// 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); } /// /// 添加节点到画布(简化版本,默认位置) /// 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, "添加节点时发生错误"); } } /// /// 放大 /// private void ZoomIn() { var newZoom = Math.Min(_canvasZoom * 1.2, 3.0); // 最大3倍 CanvasZoom = newZoom; _logger?.LogDebug("放大到: {Zoom}", newZoom); } /// /// 缩小 /// private void ZoomOut() { var newZoom = Math.Max(_canvasZoom / 1.2, 0.1); // 最小0.1倍 CanvasZoom = newZoom; _logger?.LogDebug("缩小到: {Zoom}", newZoom); } /// /// 重置缩放 /// private void ResetZoom() { CanvasZoom = 1.0; _logger?.LogDebug("重置缩放到: 1.0"); } /// /// 设置缩放 /// 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); } }