scale along rotated axis

This commit is contained in:
OliBomby 2024-07-03 16:23:19 +02:00
parent 5f8512896e
commit d0715c5f12
6 changed files with 97 additions and 31 deletions

View File

@ -86,7 +86,7 @@ public override void Begin()
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
@ -94,6 +94,7 @@ public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAx
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
scale = clampScaleToAdjustAxis(scale, adjustAxis);
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
@ -102,15 +103,15 @@ public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAx
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation);
}
else
{
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
}
}
@ -134,14 +135,34 @@ public override void Commit()
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
{
switch (adjustAxis)
{
case Axes.Y:
scale.X = 1;
break;
case Axes.X:
scale.Y = 1;
break;
case Axes.None:
scale = Vector2.One;
break;
}
return scale;
}
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0)
{
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation);
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
}
@ -176,11 +197,13 @@ private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPos
/// </summary>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
/// <param name="axisRotation">The rotation of the axes in degrees</param>
/// <returns>The clamped scale vector</returns>
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
if (objectsInScale == null)
if (objectsInScale == null || adjustAxis == Axes.None)
return scale;
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
@ -188,24 +211,63 @@ public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
origin = slider.Position;
scale = clampScaleToAdjustAxis(scale, adjustAxis);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
var selectionQuad = OriginalSurroundingQuad.Value;
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.X, Axes.X);
scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.Y, Axes.Y);
scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.X);
scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.Y);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
Vector2 clampToBound(Vector2 s, Vector2 p, float bound, Axes axis)
{
float px = p.X - actualOrigin.X;
float py = p.Y - actualOrigin.Y;
float c = axis == Axes.X ? bound - actualOrigin.X : bound - actualOrigin.Y;
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
float a, b;
if (axis == Axes.X)
{
a = cos * cos * px - sin * cos * py;
b = sin * sin * px + sin * cos * py;
}
else
{
a = -sin * cos * px + sin * sin * py;
b = sin * cos * px + cos * cos * py;
}
switch (adjustAxis)
{
case Axes.X:
if (Precision.AlmostEquals(a, 0) || (c - b) / a < 0)
break;
s.X = MathF.Min(scale.X, (c - b) / a);
break;
case Axes.Y:
if (Precision.AlmostEquals(b, 0) || (c - a) / b < 0)
break;
s.Y = MathF.Min(scale.Y, (c - a) / b);
break;
case Axes.Both:
if (Precision.AlmostEquals(a + b, 0) || c / (a * s.X + b * s.Y) < 0)
break;
s = Vector2.ComponentMin(s, s * c / (a * s.X + b * s.Y));
break;
}
return s;
}
}
private void moveSelectionInBounds()

View File

@ -130,8 +130,8 @@ protected override void LoadComplete()
scaleInfo.BindValueChanged(scale =>
{
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), gridToolbox.GridLinesRotation.Value);
});
}
@ -164,7 +164,7 @@ private void updateMaxScale()
return;
const float max_scale = 10;
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), gridToolbox.GridLinesRotation.Value);
if (!scaleInfo.Value.XAxis)
scale.X = max_scale;
@ -183,6 +183,8 @@ private void setOrigin(ScaleOrigin origin)
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null;
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
private void setAxis(bool x, bool y)
{
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };

View File

@ -134,7 +134,7 @@ public override void Begin()
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");

View File

@ -73,7 +73,7 @@ public override void Begin()
isFlippedY = false;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");

View File

@ -52,10 +52,11 @@ public partial class SelectionScaleHandler : Component
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
Begin();
Update(scale, origin, adjustAxis);
Update(scale, origin, adjustAxis, axisRotation);
Commit();
}
@ -91,7 +92,8 @@ public virtual void Begin()
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
/// <param name="axisRotation">The rotation of the axes in degrees.</param>
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
{
}

View File

@ -104,9 +104,9 @@ public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad se
/// Given a scale multiplier, an origin, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position)
public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0)
{
return origin + (position - origin) * scale;
return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation);
}
/// <summary>