using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace PamhagenSysCtrl.Helpers.Pipeline { internal sealed class FlowArrows { public const double SPEED = 20; public const double SPACING = 15; public const double LENGTH = 7; public const double WIDTH = 7; private static readonly Stopwatch Clock = Stopwatch.StartNew(); private readonly Canvas _layer; private readonly Point[] _path; private Point[] _points = []; private double _length; private double _pathOffset; private bool _isActive; public double Length => GetLength(_path[0], _path[1]) + GetLength(_path[1], _path[2]); private FlowArrows(Canvas canvas, Point[] path) { _path = path; _layer = new Canvas() { IsHitTestVisible = false, Visibility = Visibility.Collapsed, }; _layer.Unloaded += (_, _) => Stop(); canvas.Children.Add(_layer); } public static FlowArrows Create(Canvas canvas, Line first, Line second) { return new FlowArrows(canvas, [ new Point(first.X1, first.Y1), new Point(first.X2, first.Y2), new Point(second.X2, second.Y2), ]); } public static FlowArrows Create(Canvas canvas, Polyline path) { return new FlowArrows(canvas, [.. path.Points]); } public void Update((bool TowardEnd, double PathOffset)? flow) { if (flow == null) { Stop(); return; } _points = flow.Value.TowardEnd ? _path : [.. _path.Reverse()]; _length = GetLength(_points[0], _points[1]) + GetLength(_points[1], _points[2]); _pathOffset = flow.Value.PathOffset; if (!_isActive) { CompositionTarget.Rendering += OnRendering; _isActive = true; } _layer.Visibility = Visibility.Visible; Render(); } private void OnRendering(object? sender, EventArgs args) { Render(); } private void Render() { var distance = ((Clock.Elapsed.TotalSeconds * SPEED - _pathOffset) % SPACING + SPACING) % SPACING; var arrowIndex = 0; while (distance <= _length) { var arrow = GetArrow(arrowIndex++); SetPosition(arrow, distance); arrow.Visibility = Visibility.Visible; distance += SPACING; } while (arrowIndex < _layer.Children.Count) { _layer.Children[arrowIndex++].Visibility = Visibility.Collapsed; } } private Polygon GetArrow(int index) { if (index < _layer.Children.Count) { return (Polygon)_layer.Children[index]; } var arrow = new Polygon() { Fill = new SolidColorBrush(Color.FromArgb(0x60, 0, 0, 0)), IsHitTestVisible = false, Points = [ new(-LENGTH / 2, -WIDTH / 2), new(0, -WIDTH / 2), new(LENGTH / 2, 0), new(0, WIDTH / 2), new(-LENGTH / 2, WIDTH / 2), new(0, 0), ], RenderTransform = new MatrixTransform(Matrix.Identity), }; _layer.Children.Add(arrow); return arrow; } private void SetPosition(Polygon arrow, double distance) { const double blendDistance = WIDTH / 2; double firstLength = GetLength(_points[0], _points[1]); double blendStartDistance = firstLength - blendDistance; double blendEndDistance = firstLength + blendDistance; Vector v1 = Normalize(_points[0], _points[1]); Vector v2 = Normalize(_points[1], _points[2]); Point blendStart = _points[1] - v1 * blendDistance; Point blendEnd = _points[1] + v2 * blendDistance; Point position; Vector direction; if (distance < blendStartDistance) { position = _points[0] + v1 * distance; direction = v1; } else if (distance <= blendEndDistance) { double t = (distance - blendStartDistance) / (blendEndDistance - blendStartDistance); double u = 1.0 - t; position = new Point( u * u * blendStart.X + 2 * u * t * _points[1].X + t * t * blendEnd.X, u * u * blendStart.Y + 2 * u * t * _points[1].Y + t * t * blendEnd.Y); direction = new Vector( 2 * u * (_points[1].X - blendStart.X) + 2 * t * (blendEnd.X - _points[1].X), 2 * u * (_points[1].Y - blendStart.Y) + 2 * t * (blendEnd.Y - _points[1].Y)); direction.Normalize(); } else { double d = distance - firstLength; position = _points[1] + v2 * d; direction = v2; } double ux = direction.X; double uy = direction.Y; (arrow.RenderTransform as MatrixTransform)?.Matrix = new Matrix(ux, uy, -uy, ux, position.X, position.Y); } private void Stop() { if (_isActive) { CompositionTarget.Rendering -= OnRendering; _isActive = false; } _layer.Visibility = Visibility.Collapsed; } private static double GetLength(Point start, Point end) { var dx = end.X - start.X; var dy = end.Y - start.Y; return Math.Sqrt(dx * dx + dy * dy); } private static Vector Normalize(Point p1, Point p2) { Vector v = p2 - p1; if (v.Length > 0) v.Normalize(); return v; } } }