|
@@ -1,3 +1,4 @@
|
|
|
+using ChunkyImageLib.DataHolders;
|
|
|
using System;
|
|
|
using System.ComponentModel;
|
|
|
using System.Windows;
|
|
@@ -25,14 +26,28 @@ namespace PixiEditor.Zoombox
|
|
|
public static readonly DependencyProperty UseTouchGesturesProperty =
|
|
|
DependencyProperty.Register(nameof(UseTouchGestures), typeof(bool), typeof(Zoombox));
|
|
|
|
|
|
+
|
|
|
+ public static readonly DependencyProperty ScaleProperty =
|
|
|
+ DependencyProperty.Register(nameof(Scale), typeof(double), typeof(Zoombox), new(1.0, OnPropertyChange));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty CenterProperty =
|
|
|
+ DependencyProperty.Register(nameof(Center), typeof(Vector2d), typeof(Zoombox), new(new Vector2d(0, 0), OnPropertyChange));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty DimensionsProperty =
|
|
|
+ DependencyProperty.Register(nameof(Dimensions), typeof(Vector2d), typeof(Zoombox));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty AngleProperty =
|
|
|
+ DependencyProperty.Register(nameof(Angle), typeof(double), typeof(Zoombox), new(0.0, OnPropertyChange));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty FlipXProperty =
|
|
|
+ DependencyProperty.Register(nameof(FlipX), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty FlipYProperty =
|
|
|
+ DependencyProperty.Register(nameof(FlipY), typeof(bool), typeof(Zoombox), new(false, OnPropertyChange));
|
|
|
+
|
|
|
public static readonly RoutedEvent ViewportMovedEvent = EventManager.RegisterRoutedEvent(
|
|
|
nameof(ViewportMoved), RoutingStrategy.Bubble, typeof(EventHandler<ViewportRoutedEventArgs>), typeof(Zoombox));
|
|
|
|
|
|
- private const double zoomFactor = 1.09050773267; //2^(1/8)
|
|
|
- private const double maxZoom = 50;
|
|
|
- private double minZoom = -28;
|
|
|
-
|
|
|
- private double[] roundZoomValues = new double[] { .01, .02, .03, .04, .05, .06, .07, .08, .1, .13, .17, .2, .25, .33, .5, .67, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64 };
|
|
|
public object? AdditionalContent
|
|
|
{
|
|
|
get => GetValue(AdditionalContentProperty);
|
|
@@ -56,60 +71,92 @@ namespace PixiEditor.Zoombox
|
|
|
set => SetValue(UseTouchGesturesProperty, value);
|
|
|
}
|
|
|
|
|
|
+ public bool FlipX
|
|
|
+ {
|
|
|
+ get => (bool)GetValue(FlipXProperty);
|
|
|
+ set => SetValue(FlipXProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool FlipY
|
|
|
+ {
|
|
|
+ get => (bool)GetValue(FlipYProperty);
|
|
|
+ set => SetValue(FlipYProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public double Scale
|
|
|
+ {
|
|
|
+ get => (double)GetValue(ScaleProperty);
|
|
|
+ set => SetValue(ScaleProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public double Angle
|
|
|
+ {
|
|
|
+ get => (double)GetValue(AngleProperty);
|
|
|
+ set => SetValue(AngleProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Vector2d Center
|
|
|
+ {
|
|
|
+ get => (Vector2d)GetValue(CenterProperty);
|
|
|
+ set => SetValue(CenterProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Vector2d Dimensions
|
|
|
+ {
|
|
|
+ get => (Vector2d)GetValue(DimensionsProperty);
|
|
|
+ set => SetValue(DimensionsProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
public event EventHandler<ViewportRoutedEventArgs> ViewportMoved
|
|
|
{
|
|
|
add => AddHandler(ViewportMovedEvent, value);
|
|
|
remove => RemoveHandler(ViewportMovedEvent, value);
|
|
|
}
|
|
|
|
|
|
- public double Zoom => Math.Pow(zoomFactor, zoomPower);
|
|
|
+ public double CanvasX => ToScreenSpace(new(0, 0)).X;
|
|
|
+ public double CanvasY => ToScreenSpace(new(0, 0)).Y;
|
|
|
|
|
|
- private Point spaceOriginPos;
|
|
|
- internal Point SpaceOriginPos
|
|
|
+ public double ScaleTransformXY => Scale;
|
|
|
+ public double FlipTransformX => FlipX ? -1 : 1;
|
|
|
+ public double FlipTransformY => FlipY ? -1 : 1;
|
|
|
+ public double RotateTransformAngle => Angle * 180 / Math.PI;
|
|
|
+ internal const double MaxScale = 70;
|
|
|
+ internal double MinScale
|
|
|
{
|
|
|
- get => spaceOriginPos;
|
|
|
- set
|
|
|
+ get
|
|
|
{
|
|
|
- spaceOriginPos = value;
|
|
|
- Canvas.SetLeft(mainGrid, spaceOriginPos.X);
|
|
|
- Canvas.SetTop(mainGrid, spaceOriginPos.Y);
|
|
|
- RaiseViewportEvent();
|
|
|
+ double fraction = Math.Max(
|
|
|
+ mainCanvas.ActualWidth / mainGrid.ActualWidth,
|
|
|
+ mainCanvas.ActualHeight / mainGrid.ActualHeight);
|
|
|
+ return Math.Min(fraction / 8, 0.1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private double zoomPower;
|
|
|
- internal double ZoomPowerClamped
|
|
|
+ internal const double ScaleFactor = 1.09050773267; //2^(1/8)
|
|
|
+
|
|
|
+ private double[] roundZoomValues = new double[] { .01, .02, .03, .04, .05, .06, .07, .08, .1, .13, .17, .2, .25, .33, .5, .67, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64 };
|
|
|
+
|
|
|
+ internal Vector2d ToScreenSpace(Vector2d p)
|
|
|
{
|
|
|
- get => zoomPower;
|
|
|
- set
|
|
|
- {
|
|
|
- value = Math.Clamp(value, minZoom, maxZoom);
|
|
|
- if (value == zoomPower)
|
|
|
- return;
|
|
|
- zoomPower = value;
|
|
|
- var mult = Zoom;
|
|
|
- scaleTransform.ScaleX = mult;
|
|
|
- scaleTransform.ScaleY = mult;
|
|
|
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Zoom)));
|
|
|
- RaiseViewportEvent();
|
|
|
- }
|
|
|
+ Vector2d delta = p - Center;
|
|
|
+ delta = delta.Rotate(Angle) * Scale;
|
|
|
+ if (FlipX)
|
|
|
+ delta.X = -delta.X;
|
|
|
+ if (FlipY)
|
|
|
+ delta.Y = -delta.Y;
|
|
|
+ delta += new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
|
|
|
+ return delta;
|
|
|
}
|
|
|
- private double ZoomPowerTopCapped
|
|
|
+
|
|
|
+ internal Vector2d ToZoomboxSpace(Vector2d mousePos)
|
|
|
{
|
|
|
- get => zoomPower;
|
|
|
- set
|
|
|
- {
|
|
|
- if (value > maxZoom)
|
|
|
- value = maxZoom;
|
|
|
- if (value == zoomPower)
|
|
|
- return;
|
|
|
- zoomPower = value;
|
|
|
- var mult = Zoom;
|
|
|
- scaleTransform.ScaleX = mult;
|
|
|
- scaleTransform.ScaleY = mult;
|
|
|
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Zoom)));
|
|
|
- RaiseViewportEvent();
|
|
|
- }
|
|
|
+ Vector2d delta = mousePos - new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2);
|
|
|
+ if (FlipX)
|
|
|
+ delta.X = -delta.X;
|
|
|
+ if (FlipY)
|
|
|
+ delta.Y = -delta.Y;
|
|
|
+ delta = (delta / Scale).Rotate(-Angle);
|
|
|
+ return delta + Center;
|
|
|
}
|
|
|
|
|
|
private IDragOperation? activeDragOperation = null;
|
|
@@ -133,62 +180,55 @@ namespace PixiEditor.Zoombox
|
|
|
|
|
|
private void RaiseViewportEvent()
|
|
|
{
|
|
|
- Point center = ToZoomboxSpace(new(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2));
|
|
|
- Point topLeft = ToZoomboxSpace(new(0, 0));
|
|
|
- Point bottomRight = ToZoomboxSpace(new(mainCanvas.ActualWidth, mainCanvas.ActualHeight));
|
|
|
-
|
|
|
RaiseEvent(new ViewportRoutedEventArgs(
|
|
|
ViewportMovedEvent,
|
|
|
- new(center.X, center.Y),
|
|
|
- new(bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y),
|
|
|
+ Center,
|
|
|
+ Dimensions,
|
|
|
new(mainCanvas.ActualWidth, mainCanvas.ActualHeight),
|
|
|
- 0));
|
|
|
+ Angle));
|
|
|
}
|
|
|
|
|
|
- public void CenterContent() => CenterContent(new Size(mainGrid.ActualWidth, mainGrid.ActualHeight));
|
|
|
+ public void CenterContent() => CenterContent(new(mainGrid.ActualWidth, mainGrid.ActualHeight));
|
|
|
|
|
|
- public void CenterContent(Size newSize)
|
|
|
+ public void CenterContent(Vector2d newSize)
|
|
|
{
|
|
|
+
|
|
|
const double marginFactor = 1.1;
|
|
|
double scaleFactor = Math.Max(
|
|
|
- newSize.Width * marginFactor / mainCanvas.ActualWidth,
|
|
|
- newSize.Height * marginFactor / mainCanvas.ActualHeight);
|
|
|
- ZoomPowerTopCapped = -Math.Log(scaleFactor, zoomFactor);
|
|
|
- SpaceOriginPos = new Point(
|
|
|
- mainCanvas.ActualWidth / 2 - newSize.Width * Zoom / 2,
|
|
|
- mainCanvas.ActualHeight / 2 - newSize.Height * Zoom / 2);
|
|
|
+ newSize.X * marginFactor / mainCanvas.ActualWidth,
|
|
|
+ newSize.Y * marginFactor / mainCanvas.ActualHeight);
|
|
|
+
|
|
|
+ Angle = 0;
|
|
|
+ FlipX = false;
|
|
|
+ FlipY = false;
|
|
|
+ Scale = scaleFactor;
|
|
|
+ Center = newSize / 2;
|
|
|
}
|
|
|
|
|
|
- public void ZoomIntoCenter(double delta, bool round)
|
|
|
+ public void ZoomIntoCenter(double delta)
|
|
|
{
|
|
|
- ZoomInto(new Point(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2), delta, round);
|
|
|
+ ZoomInto(new Vector2d(mainCanvas.ActualWidth / 2, mainCanvas.ActualHeight / 2), delta);
|
|
|
}
|
|
|
|
|
|
- public void ZoomInto(Point mousePos, double delta, bool round = false)
|
|
|
+ public void ZoomInto(Vector2d mousePos, double delta)
|
|
|
{
|
|
|
if (delta == 0)
|
|
|
return;
|
|
|
var oldZoomboxMousePos = ToZoomboxSpace(mousePos);
|
|
|
|
|
|
- if (round)
|
|
|
- {
|
|
|
- int curIndex = GetClosestRoundZoomValueIndex(Zoom);
|
|
|
- if (curIndex == 0 && delta < 0 || curIndex == roundZoomValues.Length - 1 && delta > 0)
|
|
|
- return;
|
|
|
- int nextIndex = delta < 0 ? curIndex - 1 : curIndex + 1;
|
|
|
- double newZoom = roundZoomValues[nextIndex];
|
|
|
- ZoomPowerClamped = Math.Log(newZoom, zoomFactor);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- ZoomPowerClamped += delta;
|
|
|
- }
|
|
|
+ int curIndex = GetClosestRoundZoomValueIndex(Scale);
|
|
|
+ int nextIndex = curIndex;
|
|
|
+ if (!(curIndex == 0 && delta < 0 || curIndex == roundZoomValues.Length - 1 && delta > 0))
|
|
|
+ nextIndex = delta < 0 ? curIndex - 1 : curIndex + 1;
|
|
|
+ double newScale = roundZoomValues[nextIndex];
|
|
|
|
|
|
- if (Math.Abs(ZoomPowerClamped) < 1) ZoomPowerClamped = 0;
|
|
|
+ if (Math.Abs(newScale - 1) < 0.1) newScale = 1;
|
|
|
+ newScale = Math.Clamp(newScale, MinScale, MaxScale);
|
|
|
+ Scale = newScale;
|
|
|
|
|
|
- var shiftedMousePos = ToScreenSpace(oldZoomboxMousePos);
|
|
|
- var deltaMousePos = mousePos - shiftedMousePos;
|
|
|
- SpaceOriginPos = SpaceOriginPos + deltaMousePos;
|
|
|
+ var newZoomboxMousePos = ToZoomboxSpace(mousePos);
|
|
|
+ var deltaCenter = oldZoomboxMousePos - newZoomboxMousePos;
|
|
|
+ Center += deltaCenter;
|
|
|
}
|
|
|
|
|
|
private int GetClosestRoundZoomValueIndex(double value)
|
|
@@ -207,32 +247,6 @@ namespace PixiEditor.Zoombox
|
|
|
return index;
|
|
|
}
|
|
|
|
|
|
- private void RecalculateMinZoomLevel(object sender, SizeChangedEventArgs args)
|
|
|
- {
|
|
|
- double fraction = Math.Max(
|
|
|
- mainCanvas.ActualWidth / mainGrid.ActualWidth,
|
|
|
- mainCanvas.ActualHeight / mainGrid.ActualHeight);
|
|
|
- minZoom = Math.Min(0, Math.Log(fraction / 8, zoomFactor));
|
|
|
- }
|
|
|
-
|
|
|
- internal Point ToScreenSpace(Point p)
|
|
|
- {
|
|
|
- double zoom = Zoom;
|
|
|
- p.X *= zoom;
|
|
|
- p.Y *= zoom;
|
|
|
- p += (Vector)SpaceOriginPos;
|
|
|
- return p;
|
|
|
- }
|
|
|
-
|
|
|
- internal Point ToZoomboxSpace(Point mousePos)
|
|
|
- {
|
|
|
- double zoom = Zoom;
|
|
|
- mousePos -= (Vector)SpaceOriginPos;
|
|
|
- mousePos.X /= zoom;
|
|
|
- mousePos.Y /= zoom;
|
|
|
- return mousePos;
|
|
|
- }
|
|
|
-
|
|
|
private void OnMouseDown(object sender, MouseButtonEventArgs e)
|
|
|
{
|
|
|
if (e.ChangedButton == MouseButton.Right)
|
|
@@ -253,6 +267,8 @@ namespace PixiEditor.Zoombox
|
|
|
activeDragOperation = new MoveDragOperation(this);
|
|
|
else if (ZoomMode == ZoomboxMode.Zoom)
|
|
|
activeDragOperation = new ZoomDragOperation(this);
|
|
|
+ else if (ZoomMode == ZoomboxMode.Rotate)
|
|
|
+ activeDragOperation = new RotateDragOperation(this);
|
|
|
else
|
|
|
throw new InvalidOperationException("Unknown zoombox mode");
|
|
|
|
|
@@ -271,7 +287,7 @@ namespace PixiEditor.Zoombox
|
|
|
else
|
|
|
{
|
|
|
if (ZoomMode == ZoomboxMode.Zoom && e.ChangedButton == MouseButton.Left)
|
|
|
- ZoomInto(e.GetPosition(mainCanvas), ZoomOutOnClick ? -1 : 1, true);
|
|
|
+ ZoomInto(ToVector2d(e.GetPosition(mainCanvas)), ZoomOutOnClick ? -1 : 1);
|
|
|
}
|
|
|
activeMouseDownEventArgs = null;
|
|
|
}
|
|
@@ -292,17 +308,59 @@ namespace PixiEditor.Zoombox
|
|
|
{
|
|
|
for (int i = 0; i < Math.Abs(e.Delta / 100); i++)
|
|
|
{
|
|
|
- ZoomInto(e.GetPosition(mainCanvas), e.Delta / 100, true);
|
|
|
+ ZoomInto(ToVector2d(e.GetPosition(mainCanvas)), e.Delta / 100);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
|
|
|
{
|
|
|
- if (e.Handled = UseTouchGestures)
|
|
|
+ if (UseTouchGestures)
|
|
|
{
|
|
|
- ZoomInto(e.ManipulationOrigin, e.DeltaManipulation.Expansion.X / 5.0);
|
|
|
- SpaceOriginPos += e.DeltaManipulation.Translation;
|
|
|
+ e.Handled = true;
|
|
|
+ double newScale = Math.Clamp(Scale * e.DeltaManipulation.Scale.X, MinScale, MaxScale);
|
|
|
+ double newAngle = Angle + e.DeltaManipulation.Rotation;
|
|
|
+ Vector2d screenTranslation = new(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);
|
|
|
+ Vector2d screenOrigin = new(e.ManipulationOrigin.X, e.ManipulationOrigin.Y);
|
|
|
+
|
|
|
+ Vector2d originalPos = ToZoomboxSpace(screenOrigin);
|
|
|
+ Angle = newAngle;
|
|
|
+ Scale = newScale;
|
|
|
+ Vector2d newPos = ToZoomboxSpace(screenOrigin);
|
|
|
+ Vector2d centerTranslation = originalPos - newPos;
|
|
|
+ Center += centerTranslation;
|
|
|
+
|
|
|
+ Vector2d translatedZoomboxPos = ToZoomboxSpace(screenOrigin + screenTranslation);
|
|
|
+ Center -= translatedZoomboxPos - originalPos;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ internal static Vector2d ToVector2d(Point point) => new Vector2d(point.X, point.Y);
|
|
|
+
|
|
|
+ private static void OnPropertyChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
|
|
+ {
|
|
|
+ var zoombox = (Zoombox)obj;
|
|
|
+
|
|
|
+ Vector2d topLeft = zoombox.ToZoomboxSpace(new(0, 0)).Rotate(zoombox.Angle);
|
|
|
+ Vector2d bottomRight = zoombox.ToZoomboxSpace(new(zoombox.mainCanvas.ActualWidth, zoombox.mainCanvas.ActualHeight)).Rotate(zoombox.Angle);
|
|
|
+
|
|
|
+ zoombox.Dimensions = (bottomRight - topLeft).Abs();
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.ScaleTransformXY)));
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.RotateTransformAngle)));
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.FlipTransformX)));
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.FlipTransformY)));
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasX)));
|
|
|
+ zoombox.PropertyChanged?.Invoke(zoombox, new(nameof(zoombox.CanvasY)));
|
|
|
+ zoombox.RaiseViewportEvent();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnMainCanvasSizeChanged(object sender, SizeChangedEventArgs e)
|
|
|
+ {
|
|
|
+ RaiseViewportEvent();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
|
|
|
+ {
|
|
|
+ RaiseViewportEvent();
|
|
|
+ }
|
|
|
}
|
|
|
}
|