1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:33:30 +08:00

Merge branch 'master' into localisation-proof-of-concept

This commit is contained in:
Dean Herbert 2021-05-24 17:10:56 +09:00
commit 65c3c0d53f
12 changed files with 404 additions and 134 deletions

View File

@ -5,12 +5,14 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@ -32,6 +34,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
}
[Test]
public void TestSpinningSamplePitchShift()
{
AddStep("Add spinner", () => SetContents(() => testSingle(5, true, 4000)));
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
}
[TestCase(false)]
[TestCase(true)]
public void TestLongSpinner(bool autoplay)
@ -93,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.Update();
if (auto)
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2));
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
@ -12,7 +11,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using Vector2 = osuTK.Vector2;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
@ -173,12 +172,12 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
}
}
@ -225,26 +224,10 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
// move the selection before scaling if dragging from top or left anchors.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = getSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
{
var newPosition = h.Position;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
newPosition.X = selectionQuad.TopLeft.X + xOffset + (h.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
newPosition.Y = selectionQuad.TopLeft.Y + yOffset + (h.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
h.Position = newPosition;
}
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
@ -340,28 +323,5 @@ namespace osu.Game.Rulesets.Osu.Edit
private OsuHitObject[] selectedMovableObjects => SelectedItems.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
}
}

View File

@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
/// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
/// </summary>
@ -106,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample);
}
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void OnFree()
{
base.OnFree();

View File

@ -0,0 +1,183 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneUpdateableBeatmapSetCover : OsuTestScene
{
[Test]
public void TestLocal([Values] BeatmapSetCoverType coverType)
{
AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddUntilStep("wait for load", () => this.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
}
[Test]
public void TestUnloadAndReload()
{
OsuScrollContainer scroll = null;
List<UpdateableBeatmapSetCover> covers = new List<UpdateableBeatmapSetCover>();
AddStep("setup covers", () =>
{
BeatmapSetInfo setInfo = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
FillFlowContainer fillFlow;
Child = scroll = new OsuScrollContainer
{
Size = new Vector2(500f),
Child = fillFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Padding = new MarginPadding { Bottom = 550 }
}
};
var coverTypes = Enum.GetValues(typeof(BeatmapSetCoverType))
.Cast<BeatmapSetCoverType>()
.ToList();
for (int i = 0; i < 25; i++)
{
var coverType = coverTypes[i % coverTypes.Count];
var cover = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = setInfo,
Height = 100,
Masking = true,
};
if (coverType == BeatmapSetCoverType.Cover)
cover.Width = 500;
else if (coverType == BeatmapSetCoverType.Card)
cover.Width = 400;
else if (coverType == BeatmapSetCoverType.List)
cover.Size = new Vector2(100, 50);
fillFlow.Add(cover);
covers.Add(cover);
}
});
var loadedCovers = covers.Where(c => c.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any());
AddStep("scroll to end", () => scroll.ScrollToEnd());
AddUntilStep("all unloaded", () => !loadedCovers.Any());
}
[Test]
public void TestSetNullBeatmapWhileLoading()
{
TestUpdateableBeatmapSetCover updateableCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddStep("change model", () => updateableCover.BeatmapSet = null);
AddWaitStep("wait some", 5);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
}
[Test]
public void TestCoverChangeOnNewBeatmap()
{
TestUpdateableBeatmapSetCover updateableCover = null;
BeatmapSetCover initialCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0)
{
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
RelativeSizeAxes = Axes.Both,
Masking = true,
Alpha = 0.4f
});
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Any());
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<BeatmapSetCover>().Single());
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Except(new[] { initialCover }).Any());
}
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
}
};
private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover
{
private readonly int loadDelay;
public TestUpdateableBeatmapSetCover(int loadDelay = 10000)
{
this.loadDelay = loadDelay;
}
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{
if (model == null)
return null;
return new TestBeatmapSetCover(model, loadDelay)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
};
}
}
private class TestBeatmapSetCover : BeatmapSetCover
{
private readonly int loadDelay;
public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay)
: base(set)
{
this.loadDelay = loadDelay;
}
[BackgroundDependencyLoader]
private void load()
{
Thread.Sleep(loadDelay);
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -8,78 +9,52 @@ using osu.Game.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
public class UpdateableBeatmapSetCover : Container
public class UpdateableBeatmapSetCover : ModelBackedDrawable<BeatmapSetInfo>
{
private Drawable displayedCover;
private BeatmapSetInfo beatmapSet;
private readonly BeatmapSetCoverType coverType;
public BeatmapSetInfo BeatmapSet
{
get => beatmapSet;
set
{
if (value == beatmapSet) return;
beatmapSet = value;
if (IsLoaded)
updateCover();
}
get => Model;
set => Model = value;
}
private BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover;
public BeatmapSetCoverType CoverType
public new bool Masking
{
get => coverType;
set
{
if (value == coverType) return;
coverType = value;
if (IsLoaded)
updateCover();
}
get => base.Masking;
set => base.Masking = value;
}
public UpdateableBeatmapSetCover()
public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
{
Child = new Box
this.coverType = coverType;
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateCover();
}
protected override double LoadDelay => 500;
private void updateCover()
{
displayedCover?.FadeOut(400);
displayedCover?.Expire();
displayedCover = null;
protected override double TransformDuration => 400;
if (beatmapSet != null)
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{
if (model == null)
return null;
return new BeatmapSetCover(model, coverType)
{
Add(displayedCover = new DelayedLoadUnloadWrapper(() =>
{
var cover = new BeatmapSetCover(beatmapSet, coverType)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
};
cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
return cover;
}));
}
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
};
}
}
}

View File

@ -90,7 +90,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = beatmapCover = new UpdateableBeatmapSetCover
Child = beatmapCover = new TopSearchBeatmapSetCover
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
@ -184,5 +184,10 @@ namespace osu.Game.Overlays.BeatmapListing
return true;
}
}
private class TopSearchBeatmapSetCover : UpdateableBeatmapSetCover
{
protected override bool TransformImmediately => true;
}
}
}

View File

@ -41,12 +41,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
AddRangeInternal(new Drawable[]
{
new UpdateableBeatmapSetCover
new UpdateableBeatmapSetCover(BeatmapSetCoverType.List)
{
RelativeSizeAxes = Axes.Y,
Width = cover_width,
BeatmapSet = beatmap.BeatmapSet,
CoverType = BeatmapSetCoverType.List,
},
new Container
{

View File

@ -172,7 +172,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
base.AddInternal(Samples = new PausableSkinnableSound());
CurrentSkin = skinSource;
CurrentSkin.SourceChanged += onSkinSourceChanged;
CurrentSkin.SourceChanged += skinSourceChanged;
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
skinChanged();
}
protected override void LoadComplete()
@ -495,7 +501,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected ISkinSource CurrentSkin { get; private set; }
private void onSkinSourceChanged() => Scheduler.AddOnce(() =>
private void skinSourceChanged() => Scheduler.AddOnce(skinChanged);
private void skinChanged()
{
UpdateComboColour();
@ -503,7 +511,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (IsLoaded)
updateState(State.Value, true);
});
}
protected void UpdateComboColour()
{
@ -747,7 +755,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (HitObject != null)
HitObject.DefaultsApplied -= onDefaultsApplied;
CurrentSkin.SourceChanged -= onSkinSourceChanged;
CurrentSkin.SourceChanged -= skinSourceChanged;
}
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -353,6 +354,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Helper Methods
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
@ -375,6 +399,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
return position;
}
/// <summary>
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
/// will return the scaled position in screen space coordinates.
/// </summary>
protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
{
// adjust the direction of scale depending on which side the user is dragging.
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
// guard against no-ops and NaN.
if (scale.X != 0 && selectionQuad.Width > 0)
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
if (scale.Y != 0 && selectionQuad.Height > 0)
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
return position;
}
/// <summary>
/// Returns a quad surrounding the provided points.
/// </summary>

View File

@ -12,7 +12,7 @@ namespace osu.Game.Screens.Edit
{
private readonly Bindable<float> waveformOpacity;
private readonly Dictionary<float, ToggleMenuItem> menuItemLookup = new Dictionary<float, ToggleMenuItem>();
private readonly Dictionary<float, TernaryStateRadioMenuItem> menuItemLookup = new Dictionary<float, TernaryStateRadioMenuItem>();
public WaveformOpacityMenuItem(Bindable<float> waveformOpacity)
: base("Waveform opacity")
@ -29,13 +29,13 @@ namespace osu.Game.Screens.Edit
waveformOpacity.BindValueChanged(opacity =>
{
foreach (var kvp in menuItemLookup)
kvp.Value.State.Value = kvp.Key == opacity.NewValue;
kvp.Value.State.Value = kvp.Key == opacity.NewValue ? TernaryState.True : TernaryState.False;
}, true);
}
private ToggleMenuItem createMenuItem(float opacity)
private TernaryStateRadioMenuItem createMenuItem(float opacity)
{
var item = new ToggleMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity));
var item = new TernaryStateRadioMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity));
menuItemLookup[opacity] = item;
return item;
}

View File

@ -176,10 +176,7 @@ namespace osu.Game.Screens.Play
foreach (var element in mainComponents.Components.Cast<Drawable>())
{
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
if (!element.RelativeSizeAxes.HasFlagFast(Axes.X))
continue;
if (element.Anchor.HasFlagFast(Anchor.TopRight))
if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
{
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay)
@ -189,7 +186,8 @@ namespace osu.Game.Screens.Play
if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
lowestTopScreenSpace = bottomRight;
}
else if (element.Anchor.HasFlagFast(Anchor.y2))
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
{
var topLeft = element.ScreenSpaceDrawQuad.TopLeft;
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)

View File

@ -7,7 +7,9 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -23,34 +25,112 @@ namespace osu.Game.Skinning.Editor
public override bool HandleRotation(float angle)
{
// TODO: this doesn't correctly account for origin/anchor specs being different in a multi-selection.
foreach (var c in SelectedBlueprints)
((Drawable)c.Item).Rotation += angle;
if (SelectedBlueprints.Count == 1)
{
// for single items, rotate around the origin rather than the selection centre.
((Drawable)SelectedBlueprints.First().Item).Rotation += angle;
}
else
{
var selectionQuad = getSelectionQuad();
return base.HandleRotation(angle);
foreach (var b in SelectedBlueprints)
{
var drawableItem = (Drawable)b.Item;
var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle);
updateDrawablePosition(drawableItem, rotatedPosition);
drawableItem.Rotation += angle;
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
public override bool HandleScale(Vector2 scale, Anchor anchor)
{
// convert scale to screen space
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
adjustScaleFromAnchor(ref scale, anchor);
foreach (var c in SelectedBlueprints)
// TODO: this is temporary and will be fixed with a separate refactor of selection transform logic.
((Drawable)c.Item).Scale += scale * 0.02f;
// the selection quad is always upright, so use an AABB rect to make mutating the values easier.
var selectionRect = getSelectionQuad().AABBFloat;
// copy to mutate, as we will need to compare to the original later on.
var adjustedRect = selectionRect;
// first, remove any scale axis we are not interested in.
if (anchor.HasFlagFast(Anchor.x1)) scale.X = 0;
if (anchor.HasFlagFast(Anchor.y1)) scale.Y = 0;
bool shouldAspectLock =
// for now aspect lock scale adjustments that occur at corners..
(!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
// ..or if any of the selection have been rotated.
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|| SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation, 0));
if (shouldAspectLock)
{
if (anchor.HasFlagFast(Anchor.x1))
// if dragging from the horizontal centre, only a vertical component is available.
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
else
// in all other cases (arbitrarily) use the horizontal component for aspect lock.
scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
}
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
adjustedRect.Width += scale.X;
adjustedRect.Height += scale.Y;
// scale adjust applied to each individual item should match that of the quad itself.
var scaledDelta = new Vector2(
adjustedRect.Width / selectionRect.Width,
adjustedRect.Height / selectionRect.Height
);
foreach (var b in SelectedBlueprints)
{
var drawableItem = (Drawable)b.Item;
// each drawable's relative position should be maintained in the scaled quad.
var screenPosition = b.ScreenSpaceSelectionPoint;
var relativePositionInOriginal =
new Vector2(
(screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
(screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
);
var newPositionInAdjusted = new Vector2(
adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X,
adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y
);
updateDrawablePosition(drawableItem, newPositionInAdjusted);
drawableItem.Scale *= scaledDelta;
}
return true;
}
public override bool HandleFlip(Direction direction)
{
var selectionQuad = GetSurroundingQuad(SelectedBlueprints.Select(b => b.ScreenSpaceSelectionPoint));
var selectionQuad = getSelectionQuad();
foreach (var b in SelectedBlueprints)
{
var drawableItem = (Drawable)b.Item;
drawableItem.Position =
drawableItem.Parent.ToLocalSpace(GetFlippedPosition(direction, selectionQuad, b.ScreenSpaceSelectionPoint)) - drawableItem.AnchorPosition;
var flippedPosition = GetFlippedPosition(direction, selectionQuad, b.ScreenSpaceSelectionPoint);
updateDrawablePosition(drawableItem, flippedPosition);
drawableItem.Scale *= new Vector2(
direction == Direction.Horizontal ? -1 : 1,
@ -125,6 +205,12 @@ namespace osu.Game.Skinning.Editor
}
}
private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition)
{
drawable.Position =
drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
}
private void applyOrigin(Anchor anchor)
{
foreach (var item in SelectedItems)
@ -137,6 +223,13 @@ namespace osu.Game.Skinning.Editor
}
}
/// <summary>
/// A screen-space quad surrounding all selected drawables, accounting for their full displayed size.
/// </summary>
/// <returns></returns>
private Quad getSelectionQuad() =>
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
private void applyAnchor(Anchor anchor)
{
foreach (var item in SelectedItems)
@ -158,13 +251,6 @@ namespace osu.Game.Skinning.Editor
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
// for now aspect lock scale adjustments that occur at corners.
if (!reference.HasFlagFast(Anchor.x1) && !reference.HasFlagFast(Anchor.y1))
{
// TODO: temporary implementation - only dragging the corner handles across the X axis changes size.
scale.Y = scale.X;
}
}
}
}