From 30657f7881bb663e073151962943dd606e1c24cf Mon Sep 17 00:00:00 2001 From: Thiago Fonseca <19864159+ThFnsc@users.noreply.github.com> Date: Wed, 16 Apr 2025 08:54:47 -0300 Subject: [PATCH] Multi-touch support --- .../Behaviors/PanBehavior.cs | 69 +++++++++++++------ .../Behaviors/ZoomBehavior.cs | 19 +---- src/Blazor.Diagrams.Core/Diagram.cs | 36 ++++++++++ .../Events/MouseEventArgs.cs | 9 ++- src/Blazor.Diagrams.Core/Geometry/Point.cs | 23 +++++++ 5 files changed, 116 insertions(+), 40 deletions(-) diff --git a/src/Blazor.Diagrams.Core/Behaviors/PanBehavior.cs b/src/Blazor.Diagrams.Core/Behaviors/PanBehavior.cs index bb4d974b..32abc5a1 100644 --- a/src/Blazor.Diagrams.Core/Behaviors/PanBehavior.cs +++ b/src/Blazor.Diagrams.Core/Behaviors/PanBehavior.cs @@ -6,9 +6,8 @@ namespace Blazor.Diagrams.Core.Behaviors; public class PanBehavior : Behavior { - private Point? _initialPan; - private double _lastClientX; - private double _lastClientY; + private readonly Dictionary _activePointers = new(); + private CenterCircle? _lastPointerCircle; public PanBehavior(Diagram diagram) : base(diagram) { @@ -22,45 +21,75 @@ private void OnPointerDown(Model? model, PointerEventArgs e) if (e.Button != (int)MouseEventButton.Left) return; - Start(model, e.ClientX, e.ClientY, e.ShiftKey); + Start(model, e.Client, e.ShiftKey, e.PointerId); } - private void OnPointerMove(Model? model, PointerEventArgs e) => Move(e.ClientX, e.ClientY); + private void OnPointerMove(Model? model, PointerEventArgs e) => Move(e.Client, e.PointerId); - private void OnPointerUp(Model? model, PointerEventArgs e) => End(); + private void OnPointerUp(Model? model, PointerEventArgs e) => End(e.PointerId); - private void Start(Model? model, double clientX, double clientY, bool shiftKey) + private void Start(Model? model, Point client, bool shiftKey, long pointerId) { - if (!Diagram.Options.AllowPanning || model != null || shiftKey) + if (model != null || shiftKey) return; - _initialPan = Diagram.Pan; - _lastClientX = clientX; - _lastClientY = clientY; + _activePointers[pointerId] = client; + + PointersChanged(); } - private void Move(double clientX, double clientY) + private void Move(Point client, long pointerId) { - if (!Diagram.Options.AllowPanning || _initialPan == null) + if (_lastPointerCircle == null) + return; + + if (_activePointers.ContainsKey(pointerId) is false) return; - var deltaX = clientX - _lastClientX - (Diagram.Pan.X - _initialPan.X); - var deltaY = clientY - _lastClientY - (Diagram.Pan.Y - _initialPan.Y); - Diagram.UpdatePan(deltaX, deltaY); + _activePointers[pointerId] = client; + + var newPointerCircle = GetCurrentPointerCircle(); + + var zoomFactor = _lastPointerCircle.Value.Radius == 0 ? 1 : newPointerCircle.Radius / _lastPointerCircle.Value.Radius; + var deltaPan = newPointerCircle.Origin - _lastPointerCircle.Value.Origin; + + Diagram.Batch(() => + { + if (Diagram.Options.Zoom.Enabled) + Diagram.SetZoom(Diagram.Zoom * zoomFactor, zoomClientOrigin: newPointerCircle.Origin); + if (Diagram.Options.AllowPanning) + Diagram.UpdatePan(deltaPan.X, deltaPan.Y); + }); + + _lastPointerCircle = newPointerCircle; } - private void End() + private void End(long pointerId) { - if (!Diagram.Options.AllowPanning) - return; + _activePointers.Remove(pointerId); + + if (_activePointers.Count is 0) + _lastPointerCircle = null; + else + PointersChanged(); + } - _initialPan = null; + private CenterCircle GetCurrentPointerCircle() + { + var centroid = Point.CalculateCentroid(_activePointers.Values) ?? Point.Zero; + var radius = _activePointers.Values.Average(p => p.DistanceTo(centroid)); + return new(centroid, radius); } + private void PointersChanged() => + _lastPointerCircle = GetCurrentPointerCircle(); + public override void Dispose() { Diagram.PointerDown -= OnPointerDown; Diagram.PointerMove -= OnPointerMove; Diagram.PointerUp -= OnPointerUp; } + + private record struct CenterCircle(Point Origin, double Radius); } diff --git a/src/Blazor.Diagrams.Core/Behaviors/ZoomBehavior.cs b/src/Blazor.Diagrams.Core/Behaviors/ZoomBehavior.cs index 3da507b7..cab0b297 100644 --- a/src/Blazor.Diagrams.Core/Behaviors/ZoomBehavior.cs +++ b/src/Blazor.Diagrams.Core/Behaviors/ZoomBehavior.cs @@ -28,24 +28,7 @@ private void Diagram_Wheel(WheelEventArgs e) if (newZoom < 0 || newZoom == Diagram.Zoom) return; - // Other algorithms (based only on the changes in the zoom) don't work for our case - // This solution is taken as is from react-diagrams (ZoomCanvasAction) - var clientWidth = Diagram.Container.Width; - var clientHeight = Diagram.Container.Height; - var widthDiff = clientWidth * newZoom - clientWidth * oldZoom; - var heightDiff = clientHeight * newZoom - clientHeight * oldZoom; - var clientX = e.ClientX - Diagram.Container.Left; - var clientY = e.ClientY - Diagram.Container.Top; - var xFactor = (clientX - Diagram.Pan.X) / oldZoom / clientWidth; - var yFactor = (clientY - Diagram.Pan.Y) / oldZoom / clientHeight; - var newPanX = Diagram.Pan.X - widthDiff * xFactor; - var newPanY = Diagram.Pan.Y - heightDiff * yFactor; - - Diagram.Batch(() => - { - Diagram.SetPan(newPanX, newPanY); - Diagram.SetZoom(newZoom); - }); + Diagram.SetZoom(newZoom, zoomClientOrigin: e.Client); } public override void Dispose() diff --git a/src/Blazor.Diagrams.Core/Diagram.cs b/src/Blazor.Diagrams.Core/Diagram.cs index 713fbc75..8c3a5c61 100644 --- a/src/Blazor.Diagrams.Core/Diagram.cs +++ b/src/Blazor.Diagrams.Core/Diagram.cs @@ -251,6 +251,42 @@ public void SetZoom(double newZoom) Refresh(); } + /// + /// Changes the zoom level with a given origin + /// + /// New zoom + /// The origin of the zoom. Where it will expand/contract from + public void SetZoom(double newZoom, Point zoomClientOrigin) + { + if(Container is null) + { + SetZoom(newZoom); + return; + } + + newZoom = Math.Clamp(newZoom, Options.Zoom.Minimum, Options.Zoom.Maximum); + var oldZoom = Zoom; + + // Other algorithms (based only on the changes in the zoom) don't work for our case + // This solution is taken as is from react-diagrams (ZoomCanvasAction) + var clientWidth = Container.Width; + var clientHeight = Container.Height; + var widthDiff = clientWidth * newZoom - clientWidth * oldZoom; + var heightDiff = clientHeight * newZoom - clientHeight * oldZoom; + var clientX = zoomClientOrigin.X - Container.Left; + var clientY = zoomClientOrigin.Y - Container.Top; + var xFactor = (clientX - Pan.X) / oldZoom / clientWidth; + var yFactor = (clientY - Pan.Y) / oldZoom / clientHeight; + var newPanX = Pan.X - widthDiff * xFactor; + var newPanY = Pan.Y - heightDiff * yFactor; + + Batch(() => + { + SetPan(newPanX, newPanY); + SetZoom(newZoom); + }); + } + public void SetContainer(Rectangle newRect) { if (newRect.Equals(Container)) diff --git a/src/Blazor.Diagrams.Core/Events/MouseEventArgs.cs b/src/Blazor.Diagrams.Core/Events/MouseEventArgs.cs index 94c4cba5..a84c10a8 100644 --- a/src/Blazor.Diagrams.Core/Events/MouseEventArgs.cs +++ b/src/Blazor.Diagrams.Core/Events/MouseEventArgs.cs @@ -1,3 +1,8 @@ -namespace Blazor.Diagrams.Core.Events; +using Blazor.Diagrams.Core.Geometry; -public record MouseEventArgs(double ClientX, double ClientY, long Button, long Buttons, bool CtrlKey, bool ShiftKey, bool AltKey); +namespace Blazor.Diagrams.Core.Events; + +public record MouseEventArgs(double ClientX, double ClientY, long Button, long Buttons, bool CtrlKey, bool ShiftKey, bool AltKey) +{ + public Point Client => new(ClientX, ClientY); +} diff --git a/src/Blazor.Diagrams.Core/Geometry/Point.cs b/src/Blazor.Diagrams.Core/Geometry/Point.cs index 7bb02b51..e388b226 100644 --- a/src/Blazor.Diagrams.Core/Geometry/Point.cs +++ b/src/Blazor.Diagrams.Core/Geometry/Point.cs @@ -59,4 +59,27 @@ public void Deconstruct(out double x, out double y) x = X; y = Y; } + + /// + /// Calculates the centroid of a set of points. + /// + /// The collection of points + /// A instance with the centroid coordinates or if there were no points. + public static Point? CalculateCentroid(IEnumerable points) + { + double sumX = 0, sumY = 0; + var count = 0; + + foreach (var point in points) + { + sumX += point.X; + sumY += point.Y; + count++; + } + + if (count is 0) + return null; + + return new Point(sumX / count, sumY / count); + } }