using System.Diagnostics; using System.Linq; namespace NotesDataAnalyst { public partial class GraphPage : ContentPage { private Point _initialPanPosition; private PointF _initialNodePosition; private PointF _initialTouchPosition; private SizeF _initialNodeSize; private readonly List _nodes = new(); private readonly List _connections = new(); private GraphNode _selectedNode; private bool _isScrollingBlocked = false; private double _scale = 1.0; private double _initialScale = 1.0; private bool _isNodeBeingMoved = false; private const double SmoothFactor = 0.85; // Коэффициент сглаживания private Point _lastTranslation; public GraphPage() { InitializeComponent(); var graphDrawable = new GraphDrawable(_nodes, _connections); canvasView.Drawable = graphDrawable; var panGesture = new PanGestureRecognizer(); panGesture.PanUpdated += OnPanUpdated; canvasView.GestureRecognizers.Add(panGesture); var tapGesture = new TapGestureRecognizer(); tapGesture.Tapped += OnCanvasTapped; canvasView.GestureRecognizers.Add(tapGesture); // Добавляем Pinch только один раз if (!canvasView.GestureRecognizers.OfType().Any()) { var pinchGesture = new PinchGestureRecognizer(); pinchGesture.PinchUpdated += OnPinchUpdated; canvasView.GestureRecognizers.Add(pinchGesture); } } // Обработчик жеста перемещения (Pan) для перемещения канвы или ноды private void OnPanUpdated(object sender, PanUpdatedEventArgs e) { if (_selectedNode != null) { // Начало перетаскивания if (e.StatusType == GestureStatus.Started) { // Запоминаем начальную позицию касания и ноды _initialTouchPosition = new Point(e.TotalX, e.TotalY); _initialNodePosition = _selectedNode.Position; } // Перемещение else if (e.StatusType == GestureStatus.Running) { var deltaX = e.TotalX - _initialTouchPosition.X; // Разница с начальной точкой var deltaY = e.TotalY - _initialTouchPosition.Y; // Перемещаем ноду относительно её начальной позиции _selectedNode.Position = new Point( _initialNodePosition.X + deltaX, _initialNodePosition.Y + deltaY ); canvasView.Invalidate(); // Перерисовываем канву } // Завершение перетаскивания else if (e.StatusType == GestureStatus.Completed) { // Можно добавить логику на завершение перетаскивания, например, включить прокрутку //scrollView.IsEnabled = true; } } else // Перемещение канвы при снятии выделения с ноды { if (e.StatusType == GestureStatus.Started) { _initialPanPosition = new Point(canvasView.TranslationX - e.TotalX, canvasView.TranslationY - e.TotalY); } else if (e.StatusType == GestureStatus.Running) { var deltaX = e.TotalX; var deltaY = e.TotalY; // Применяем сглаживание к движению double smoothedX = _lastTranslation.X + SmoothFactor * (deltaX - _lastTranslation.X); double smoothedY = _lastTranslation.Y + SmoothFactor * (deltaY - _lastTranslation.Y); // Обновляем положение канвы canvasView.TranslationX = _initialPanPosition.X + smoothedX; canvasView.TranslationY = _initialPanPosition.Y + smoothedY; // Сохраняем последнюю смягченную позицию для следующего цикла _lastTranslation = new Point(smoothedX, smoothedY); } else if (e.StatusType == GestureStatus.Completed) { _initialPanPosition = new Point(canvasView.TranslationX, canvasView.TranslationY); canvasView.Invalidate(); } } } private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) { if (e.Status == GestureStatus.Started) { // Сохраняем начальный масштаб _initialScale = _scale; } else if (e.Status == GestureStatus.Running) { // Вычисляем новый масштаб _scale = Math.Max(0.5, Math.Min(_initialScale * e.Scale, 3.0)); // Применяем масштаб к канве canvasView.Scale = _scale; } } private async void OnEditTextClicked(object sender, EventArgs e) { // Проверяем, выбрана ли нода if (_selectedNode == null) { await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK"); return; } // Показываем поле для редактирования текста и заполняем его текущим текстом ноды textEditor.IsVisible = true; textEditor.Text = _selectedNode.Text; // Ставим фокус на текстовое поле для ввода textEditor.Focus(); } // Обработчик завершения редактирования текста private void OnTextEditorCompleted(object sender, EventArgs e) { if (_selectedNode != null) { // Обновляем текст ноды с введенным значением _selectedNode.Text = textEditor.Text; // Скрываем поле для редактирования текста textEditor.IsVisible = false; // Перерисовываем канву для обновления отображения текста canvasView.Invalidate(); } } private async void OnResizeNodeClicked(object sender, EventArgs e) { // Проверяем, выбрана ли нода if (_selectedNode == null) { await DisplayAlert("Ошибка", "Сначала выберите ноду", "OK"); return; } // Открываем диалог для ввода новых размеров var result = await DisplayPromptAsync("Изменить размер", "Введите ширину и высоту", initialValue: $"{_selectedNode.Size.Width} x {_selectedNode.Size.Height}", keyboard: Keyboard.Numeric); if (string.IsNullOrEmpty(result)) return; // Разбиваем введенное значение на ширину и высоту var dimensions = result.Split('x'); if (dimensions.Length == 2 && double.TryParse(dimensions[0].Trim(), out double width) && double.TryParse(dimensions[1].Trim(), out double height)) { // Обновляем размеры ноды _selectedNode.Size = new Size(width, height); canvasView.Invalidate(); // Перерисовываем канву } else { await DisplayAlert("Ошибка", "Неверный формат. Введите значения в формате ширина x высота.", "OK"); } } private async void OnAddNodeClicked(object sender, EventArgs e) { var newNode = new GraphNode { Text = "Новая нода", Position = new Point(100, 200), Size = new Size(100, 50), }; _nodes.Add(newNode); canvasView.Invalidate(); } private void OnChildClicked(object sender, EventArgs e) { if (_selectedNode != null) { var childNode = new GraphNode { Text = "Child Node", Position = new Point(_selectedNode.Position.X + 100, _selectedNode.Position.Y + 70), Size = _selectedNode.Size, BackgroundColor = Colors.Gray, }; _nodes.Add(childNode); _connections.Add(new GraphConnection { FromNode = _selectedNode, ToNode = childNode }); canvasView.Invalidate(); _selectedNode = childNode; } else { Debug.WriteLine("Сначала выберите ноду"); } } private void OnCanvasTapped(object sender, TappedEventArgs e) { var tapPosition = e.GetPosition((View)sender); _selectedNode = null; if (tapPosition.HasValue) { var touchPoint = new PointF((float)tapPosition.Value.X, (float)tapPosition.Value.Y); _selectedNode = _nodes.FirstOrDefault(node => IsPointInsideNode(touchPoint, node)); foreach (var node in _nodes) node.IsActive = node == _selectedNode; canvasView.Invalidate(); } } private bool IsPointInsideNode(PointF point, GraphNode node) { return point.X >= node.Position.X && point.X <= node.Position.X + node.Size.Width && point.Y >= node.Position.Y && point.Y <= node.Position.Y + node.Size.Height; } } }