Skip to content

Multi-touch support #513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions src/Blazor.Diagrams.Core/Behaviors/PanBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<long, Point> _activePointers = new();
private CenterCircle? _lastPointerCircle;

public PanBehavior(Diagram diagram) : base(diagram)
{
Expand All @@ -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);
}
19 changes: 1 addition & 18 deletions src/Blazor.Diagrams.Core/Behaviors/ZoomBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions src/Blazor.Diagrams.Core/Diagram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,42 @@ public void SetZoom(double newZoom)
Refresh();
}

/// <summary>
/// Changes the zoom level with a given origin
/// </summary>
/// <param name="newZoom">New zoom</param>
/// <param name="zoomClientOrigin">The origin of the zoom. Where it will expand/contract from</param>
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))
Expand Down
9 changes: 7 additions & 2 deletions src/Blazor.Diagrams.Core/Events/MouseEventArgs.cs
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 23 additions & 0 deletions src/Blazor.Diagrams.Core/Geometry/Point.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,27 @@ public void Deconstruct(out double x, out double y)
x = X;
y = Y;
}

/// <summary>
/// Calculates the centroid of a set of points.
/// </summary>
/// <param name="points">The collection of points</param>
/// <returns>A <see cref="Point"/> instance with the centroid coordinates or <see langword="null"/> if there were no points.</returns>
public static Point? CalculateCentroid(IEnumerable<Point> 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);
}
}