diff --git a/PamhagenSysCtrl/Helpers/PamhagenGraph.cs b/PamhagenSysCtrl/Helpers/PamhagenGraph.cs index c1f6f96..c767109 100644 --- a/PamhagenSysCtrl/Helpers/PamhagenGraph.cs +++ b/PamhagenSysCtrl/Helpers/PamhagenGraph.cs @@ -433,9 +433,12 @@ namespace PamhagenSysCtrl.Helpers { public void ColorPipesAdjacientToPath(Path path, Brush? color1, Brush? color2) { var color = color1; + var flowPathOffset = 0.0; HashSet visited = []; foreach (var (edge, node) in path.Hops) { edge.Highlight = color1; + edge.Flow = color1 == PamhagenBrushes.Green ? (edge.End == node, flowPathOffset) : null; + if (edge.Flow != null) flowPathOffset += edge.FlowLength; visited.Add(edge); if (node is Pump) color = color2; TraverseSubgraph(node, visited, edgeAction: (e) => e.Highlight = color); diff --git a/PamhagenSysCtrl/Helpers/Pipeline/FlowArrow.cs b/PamhagenSysCtrl/Helpers/Pipeline/FlowArrow.cs new file mode 100644 index 0000000..4d1ed07 --- /dev/null +++ b/PamhagenSysCtrl/Helpers/Pipeline/FlowArrow.cs @@ -0,0 +1,155 @@ +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); + } + } +} diff --git a/PamhagenSysCtrl/Helpers/Pipeline/IEdge.cs b/PamhagenSysCtrl/Helpers/Pipeline/IEdge.cs index 538cf70..bff0ea3 100644 --- a/PamhagenSysCtrl/Helpers/Pipeline/IEdge.cs +++ b/PamhagenSysCtrl/Helpers/Pipeline/IEdge.cs @@ -9,6 +9,8 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { public bool IsTwoWay { get; } public bool Orientation { get; } public Brush? Highlight { get; set; } + public (bool TowardEnd, double PathOffset)? Flow { get; set; } + public double FlowLength { get; } public void Draw(Canvas canvas); public void Update(); diff --git a/PamhagenSysCtrl/Helpers/Pipeline/Inlet.cs b/PamhagenSysCtrl/Helpers/Pipeline/Inlet.cs index da2561a..ba4c07c 100644 --- a/PamhagenSysCtrl/Helpers/Pipeline/Inlet.cs +++ b/PamhagenSysCtrl/Helpers/Pipeline/Inlet.cs @@ -11,11 +11,14 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { public bool IsTwoWay => false; public bool Orientation { get; set; } public Brush? Highlight { get; set; } + public (bool TowardEnd, double PathOffset)? Flow { get; set; } + public double FlowLength => _flowArrow?.Length ?? 0; private Line? _outer1; private Line? _outer2; private Line? _inner1; private Line? _inner2; + private FlowArrow? _flowArrow; public Inlet(INode start, ISink end, bool orientation = false) { Start = start; @@ -74,11 +77,13 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { canvas.Children.Add(_outer2); canvas.Children.Add(_inner1); canvas.Children.Add(_inner2); + _flowArrow = FlowArrow.Create(canvas, _inner1!, _inner2!); } public void Update() { _inner1?.Stroke = Highlight ?? Brushes.DarkGray; _inner2?.Stroke = Highlight ?? Brushes.DarkGray; + _flowArrow?.Update(Highlight == PamhagenBrushes.Green ? Flow : null); } } } diff --git a/PamhagenSysCtrl/Helpers/Pipeline/Outlet.cs b/PamhagenSysCtrl/Helpers/Pipeline/Outlet.cs index e51db1e..4450984 100644 --- a/PamhagenSysCtrl/Helpers/Pipeline/Outlet.cs +++ b/PamhagenSysCtrl/Helpers/Pipeline/Outlet.cs @@ -13,8 +13,11 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { public bool IsTwoWay => false; public bool Orientation { get; set; } public Brush? Highlight { get; set; } + public (bool TowardEnd, double PathOffset)? Flow { get; set; } + public double FlowLength => _flowArrow?.Length ?? 0; private Polygon? _hopper; + private FlowArrow? _flowArrow; private Line? _outer1; private Line? _outer2; private Line? _inner1; @@ -81,15 +84,17 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { }; canvas.Children.Add(_outer1); canvas.Children.Add(_outer2); - canvas.Children.Add(_hopper); canvas.Children.Add(_inner1); canvas.Children.Add(_inner2); + _flowArrow = FlowArrow.Create(canvas, _inner1!, _inner2!); + canvas.Children.Add(_hopper); } public void Update() { _inner1?.Stroke = Highlight ?? Brushes.DarkGray; _inner2?.Stroke = Highlight ?? Brushes.DarkGray; _hopper?.Fill = Highlight ?? Brushes.DarkGray; + _flowArrow?.Update(Highlight == PamhagenBrushes.Green ? Flow : null); } } } diff --git a/PamhagenSysCtrl/Helpers/Pipeline/Pipe.cs b/PamhagenSysCtrl/Helpers/Pipeline/Pipe.cs index eaa586f..20fffa9 100644 --- a/PamhagenSysCtrl/Helpers/Pipeline/Pipe.cs +++ b/PamhagenSysCtrl/Helpers/Pipeline/Pipe.cs @@ -10,9 +10,12 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { public bool IsTwoWay { get; set; } public bool Orientation { get; set; } public Brush? Highlight { get; set; } + public (bool TowardEnd, double PathOffset)? Flow { get; set; } + public double FlowLength => _flowArrow?.Length ?? 0; private Polyline? _outer; private Polyline? _inner; + private FlowArrow? _flowArrow; public Pipe(INode start, INode end, bool orientation = false, bool twoWay = false) { Start = start; @@ -46,10 +49,12 @@ namespace PamhagenSysCtrl.Helpers.Pipeline { }; canvas.Children.Add(_outer); canvas.Children.Add(_inner); + _flowArrow = FlowArrow.Create(canvas, _inner); } public void Update() { _inner?.Stroke = Highlight ?? Brushes.DarkGray; + _flowArrow?.Update(Highlight == PamhagenBrushes.Green ? Flow : null); } } } diff --git a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs index d4e6044..9c66998 100644 --- a/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs +++ b/PamhagenSysCtrl/Windows/PlantSchemeWindow.xaml.cs @@ -73,6 +73,7 @@ namespace PamhagenSysCtrl.Windows { private ISet UpdateScheme(Point pos, bool down) { foreach (var e in Graph.Edges) { e.Highlight = null; + e.Flow = null; } foreach (var p in Graph.SelectedPaths) { Graph.ColorPipesAdjacientToPath(p, PamhagenBrushes.Green, PamhagenBrushes.DimOrange); @@ -96,6 +97,7 @@ namespace PamhagenSysCtrl.Windows { if (_lastSource != null) { foreach (var edge in _lastSource.Outputs) { edge.Highlight = PamhagenBrushes.Green; + edge.Flow = (true, 0); } } Graph.Update(hover, down);