Files
pamhagen-sysctrl/PamhagenSysCtrl/Helpers/Pipeline/FlowArrow.cs
T

156 lines
5.2 KiB
C#

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);
}
}
}