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 FlowArrow { private const double Speed = 45; private const double Spacing = 40; private const double HalfLength = 5; private const double HalfWidth = 3.5; 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 FlowArrow(Canvas canvas, Point[] path) { _path = path; _layer = new Canvas() { IsHitTestVisible = false, Visibility = Visibility.Collapsed, }; _layer.Unloaded += (_, _) => Stop(); canvas.Children.Add(_layer); } public static FlowArrow Create(Canvas canvas, Line first, Line second) { return new FlowArrow(canvas, [ new Point(first.X1, first.Y1), new Point(first.X2, first.Y2), new Point(second.X2, second.Y2), ]); } public static FlowArrow Create(Canvas canvas, Polyline path) { return new FlowArrow(canvas, [.. path.Points]); } public void Update((bool TowardEnd, double PathOffset)? flow) { if (flow == null) { Stop(); return; } var points = flow.Value.TowardEnd ? _path : [.. _path.Reverse()]; var length = GetLength(points[0], points[1]) + GetLength(points[1], points[2]); if (length < 16) { Stop(); return; } _points = points; _length = length; _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 = PositiveModulo(Clock.Elapsed.TotalSeconds * Speed - _pathOffset, 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 = Brushes.Black, IsHitTestVisible = false, Points = [ new Point(HalfLength, 0), new Point(-HalfLength, HalfWidth), new Point(-HalfLength, -HalfWidth), ], RenderTransform = new MatrixTransform(Matrix.Identity), }; _layer.Children.Add(arrow); return arrow; } private void SetPosition(Polygon arrow, double distance) { var firstLength = GetLength(_points[0], _points[1]); var secondLength = GetLength(_points[1], _points[2]); Point start; Point end; double segmentDistance; if ((firstLength > 0 && distance <= firstLength) || secondLength == 0) { start = _points[0]; end = _points[1]; segmentDistance = distance; } else { start = _points[1]; end = _points[2]; segmentDistance = distance - firstLength; } var segmentLength = GetLength(start, end); if (segmentLength == 0) return; var ux = (end.X - start.X) / segmentLength; var uy = (end.Y - start.Y) / segmentLength; var x = start.X + ux * segmentDistance; var y = start.Y + uy * segmentDistance; ((MatrixTransform)arrow.RenderTransform).Matrix = new Matrix( ux, uy, -uy, ux, x, y); } private void Stop() { if (_isActive) { CompositionTarget.Rendering -= OnRendering; _isActive = false; } _layer.Visibility = Visibility.Collapsed; } private static double PositiveModulo(double value, double modulus) { return (value % modulus + modulus) % modulus; } 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); } } }