mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 16:27:26 +08:00
Merge branch 'master' into fix-preview-not-stopping
This commit is contained in:
commit
1e52c83804
@ -62,6 +62,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1029.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1106.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
230
osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
Normal file
230
osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs
Normal file
@ -0,0 +1,230 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public class TestSceneFollowPoints : OsuTestScene
|
||||
{
|
||||
private Container<DrawableOsuHitObject> hitObjectContainer;
|
||||
private FollowPointRenderer followPointRenderer;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both },
|
||||
followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestAddObject()
|
||||
{
|
||||
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveObject()
|
||||
{
|
||||
addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } });
|
||||
|
||||
removeObjectStep(() => getObject(0));
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddMultipleObjects()
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveEndObject()
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
removeObjectStep(() => getObject(4));
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveStartObject()
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
removeObjectStep(() => getObject(0));
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRemoveMiddleObject()
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
removeObjectStep(() => getObject(2));
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMoveObject()
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100));
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
[TestCase(0, 0)] // Start -> Start
|
||||
[TestCase(0, 2)] // Start -> Middle
|
||||
[TestCase(0, 5)] // Start -> End
|
||||
[TestCase(2, 0)] // Middle -> Start
|
||||
[TestCase(1, 3)] // Middle -> Middle (forwards)
|
||||
[TestCase(3, 1)] // Middle -> Middle (backwards)
|
||||
[TestCase(4, 0)] // End -> Start
|
||||
[TestCase(4, 2)] // End -> Middle
|
||||
[TestCase(4, 4)] // End -> End
|
||||
public void TestReorderObjects(int startIndex, int endIndex)
|
||||
{
|
||||
addMultipleObjectsStep();
|
||||
|
||||
reorderObjectStep(startIndex, endIndex);
|
||||
|
||||
assertGroups();
|
||||
}
|
||||
|
||||
private void addMultipleObjectsStep() => addObjectsStep(() => new OsuHitObject[]
|
||||
{
|
||||
new HitCircle { Position = new Vector2(100, 100) },
|
||||
new HitCircle { Position = new Vector2(200, 200) },
|
||||
new HitCircle { Position = new Vector2(300, 300) },
|
||||
new HitCircle { Position = new Vector2(400, 400) },
|
||||
new HitCircle { Position = new Vector2(500, 500) },
|
||||
});
|
||||
|
||||
private void addObjectsStep(Func<OsuHitObject[]> ctorFunc)
|
||||
{
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
var objects = ctorFunc();
|
||||
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
{
|
||||
objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1);
|
||||
objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
DrawableOsuHitObject drawableObject = null;
|
||||
|
||||
switch (objects[i])
|
||||
{
|
||||
case HitCircle circle:
|
||||
drawableObject = new DrawableHitCircle(circle);
|
||||
break;
|
||||
|
||||
case Slider slider:
|
||||
drawableObject = new DrawableSlider(slider);
|
||||
break;
|
||||
|
||||
case Spinner spinner:
|
||||
drawableObject = new DrawableSpinner(spinner);
|
||||
break;
|
||||
}
|
||||
|
||||
hitObjectContainer.Add(drawableObject);
|
||||
followPointRenderer.AddFollowPoints(drawableObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeObjectStep(Func<DrawableOsuHitObject> getFunc)
|
||||
{
|
||||
AddStep("remove hitobject", () =>
|
||||
{
|
||||
var drawableObject = getFunc?.Invoke();
|
||||
|
||||
hitObjectContainer.Remove(drawableObject);
|
||||
followPointRenderer.RemoveFollowPoints(drawableObject);
|
||||
});
|
||||
}
|
||||
|
||||
private void reorderObjectStep(int startIndex, int endIndex)
|
||||
{
|
||||
AddStep($"move object {startIndex} to {endIndex}", () =>
|
||||
{
|
||||
DrawableOsuHitObject toReorder = getObject(startIndex);
|
||||
|
||||
double targetTime;
|
||||
if (endIndex < hitObjectContainer.Count)
|
||||
targetTime = getObject(endIndex).HitObject.StartTime - 1;
|
||||
else
|
||||
targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1;
|
||||
|
||||
hitObjectContainer.Remove(toReorder);
|
||||
toReorder.HitObject.StartTime = targetTime;
|
||||
hitObjectContainer.Add(toReorder);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertGroups()
|
||||
{
|
||||
AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count);
|
||||
AddAssert("group endpoints are correct", () =>
|
||||
{
|
||||
for (int i = 0; i < hitObjectContainer.Count; i++)
|
||||
{
|
||||
DrawableOsuHitObject expectedStart = getObject(i);
|
||||
DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null;
|
||||
|
||||
if (getGroup(i).Start != expectedStart)
|
||||
throw new AssertionException($"Object {i} expected to be the start of group {i}.");
|
||||
|
||||
if (getGroup(i).End != expectedEnd)
|
||||
throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}.");
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index];
|
||||
|
||||
private FollowPointConnection getGroup(int index) => followPointRenderer.Connections[index];
|
||||
|
||||
private class TestHitObjectContainer : Container<DrawableOsuHitObject>
|
||||
{
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
var osuX = (DrawableOsuHitObject)x;
|
||||
var osuY = (DrawableOsuHitObject)y;
|
||||
|
||||
int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime);
|
||||
|
||||
if (compare == 0)
|
||||
return base.Compare(x, y);
|
||||
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -42,11 +42,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
private readonly SnapProvider snapProvider = new SnapProvider();
|
||||
|
||||
private readonly TestOsuDistanceSnapGrid grid;
|
||||
private TestOsuDistanceSnapGrid grid;
|
||||
|
||||
public TestSceneOsuDistanceSnapGrid()
|
||||
{
|
||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -58,14 +66,6 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
||||
};
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1;
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
});
|
||||
|
||||
[TestCase(1)]
|
||||
@ -102,6 +102,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertSnappedDistance((float)beat_length * 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLimitedDistance()
|
||||
{
|
||||
AddStep("create limited grid", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.SlateGray
|
||||
},
|
||||
grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
|
||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f)));
|
||||
assertSnappedDistance((float)beat_length * 2);
|
||||
}
|
||||
|
||||
private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () =>
|
||||
{
|
||||
Vector2 snappedPosition = grid.GetSnappedPosition(grid.ToLocalSpace(InputManager.CurrentState.Mouse.Position)).position;
|
||||
@ -152,8 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public new float DistanceSpacing => base.DistanceSpacing;
|
||||
|
||||
public TestOsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -164,9 +185,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
|
||||
|
||||
public float DurationToDistance(double referenceTime, double duration) => 0;
|
||||
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||
|
||||
public double DistanceToDuration(double referenceTime, float distance) => 0;
|
||||
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||
|
||||
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||
|
||||
|
@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
|
||||
{
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject)
|
||||
: base(hitObject, hitObject.StackedEndPosition)
|
||||
public OsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject)
|
||||
: base(hitObject, nextHitObject, hitObject.StackedEndPosition)
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
|
@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -60,25 +61,40 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
var objects = selectedHitObjects.ToList();
|
||||
|
||||
if (objects.Count == 0)
|
||||
return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
|
||||
|
||||
double minTime = objects.Min(h => h.StartTime);
|
||||
return createGrid(h => h.StartTime < minTime, objects.Count + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a grid from the last <see cref="HitObject"/> matching a predicate to a target <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="sourceSelector">A predicate that matches <see cref="HitObject"/>s where the grid can start from.
|
||||
/// Only the last <see cref="HitObject"/> matching the predicate is used.</param>
|
||||
/// <param name="targetOffset">An offset from the <see cref="HitObject"/> selected via <paramref name="sourceSelector"/> at which the grid should stop.</param>
|
||||
/// <returns>The <see cref="OsuDistanceSnapGrid"/> from a selected <see cref="HitObject"/> to a target <see cref="HitObject"/>.</returns>
|
||||
private OsuDistanceSnapGrid createGrid(Func<HitObject, bool> sourceSelector, int targetOffset = 1)
|
||||
{
|
||||
if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset));
|
||||
|
||||
int sourceIndex = -1;
|
||||
|
||||
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
|
||||
{
|
||||
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime <= EditorClock.CurrentTime);
|
||||
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
|
||||
break;
|
||||
|
||||
if (lastObject == null)
|
||||
return null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
sourceIndex = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
double minTime = objects.Min(h => h.StartTime);
|
||||
|
||||
var lastObject = EditorBeatmap.HitObjects.LastOrDefault(h => h.StartTime < minTime);
|
||||
if (sourceIndex == -1)
|
||||
return null;
|
||||
|
||||
if (lastObject == null)
|
||||
return null;
|
||||
OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
|
||||
OsuHitObject targetObject = sourceIndex + targetOffset < EditorBeatmap.HitObjects.Count ? EditorBeatmap.HitObjects[sourceIndex + targetOffset] : null;
|
||||
|
||||
return new OsuDistanceSnapGrid(lastObject);
|
||||
}
|
||||
return new OsuDistanceSnapGrid(sourceObject, targetObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
// 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 osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects hit objects visually, for example with follow points.
|
||||
/// </summary>
|
||||
public abstract class ConnectionRenderer<T> : LifetimeManagementContainer
|
||||
where T : HitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit objects to create connections for
|
||||
/// </summary>
|
||||
public abstract IEnumerable<T> HitObjects { get; set; }
|
||||
}
|
||||
}
|
@ -12,6 +12,9 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPoint : Container
|
||||
{
|
||||
private const float width = 8;
|
||||
|
@ -0,0 +1,140 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualises the <see cref="FollowPoint"/>s between two <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPointConnection : CompositeDrawable
|
||||
{
|
||||
// Todo: These shouldn't be constants
|
||||
private const int spacing = 32;
|
||||
private const double preempt = 800;
|
||||
|
||||
/// <summary>
|
||||
/// The start time of <see cref="Start"/>.
|
||||
/// </summary>
|
||||
public readonly Bindable<double> StartTime = new Bindable<double>();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.
|
||||
/// </summary>
|
||||
[NotNull]
|
||||
public readonly DrawableOsuHitObject Start;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="FollowPointConnection"/>.
|
||||
/// </summary>
|
||||
/// <param name="start">The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will exit from.</param>
|
||||
public FollowPointConnection([NotNull] DrawableOsuHitObject start)
|
||||
{
|
||||
Start = start;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
StartTime.BindTo(Start.HitObject.StartTimeBindable);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
bindEvents(Start);
|
||||
}
|
||||
|
||||
private DrawableOsuHitObject end;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableOsuHitObject"/> which <see cref="FollowPoint"/>s will enter.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public DrawableOsuHitObject End
|
||||
{
|
||||
get => end;
|
||||
set
|
||||
{
|
||||
end = value;
|
||||
|
||||
if (end != null)
|
||||
bindEvents(end);
|
||||
|
||||
if (IsLoaded)
|
||||
scheduleRefresh();
|
||||
else
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindEvents(DrawableOsuHitObject drawableObject)
|
||||
{
|
||||
drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh());
|
||||
drawableObject.HitObject.DefaultsApplied += scheduleRefresh;
|
||||
}
|
||||
|
||||
private void scheduleRefresh() => Scheduler.AddOnce(refresh);
|
||||
|
||||
private void refresh()
|
||||
{
|
||||
ClearInternal();
|
||||
|
||||
if (End == null)
|
||||
return;
|
||||
|
||||
OsuHitObject osuStart = Start.HitObject;
|
||||
OsuHitObject osuEnd = End.HitObject;
|
||||
|
||||
if (osuEnd.NewCombo)
|
||||
return;
|
||||
|
||||
if (osuStart is Spinner || osuEnd is Spinner)
|
||||
return;
|
||||
|
||||
Vector2 startPosition = osuStart.EndPosition;
|
||||
Vector2 endPosition = osuEnd.Position;
|
||||
double startTime = (osuStart as IHasEndTime)?.EndTime ?? osuStart.StartTime;
|
||||
double endTime = osuEnd.StartTime;
|
||||
|
||||
Vector2 distanceVector = endPosition - startPosition;
|
||||
int distance = (int)distanceVector.Length;
|
||||
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
|
||||
double duration = endTime - startTime;
|
||||
|
||||
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
|
||||
{
|
||||
float fraction = (float)d / distance;
|
||||
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
|
||||
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
|
||||
double fadeOutTime = startTime + fraction * duration;
|
||||
double fadeInTime = fadeOutTime - preempt;
|
||||
|
||||
FollowPoint fp;
|
||||
|
||||
AddInternal(fp = new FollowPoint
|
||||
{
|
||||
Position = pointStartPosition,
|
||||
Rotation = rotation,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(1.5f * osuEnd.Scale),
|
||||
});
|
||||
|
||||
using (fp.BeginAbsoluteSequence(fadeInTime))
|
||||
{
|
||||
fp.FadeIn(osuEnd.TimeFadeIn);
|
||||
fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out);
|
||||
fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out);
|
||||
fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn);
|
||||
}
|
||||
|
||||
fp.Expire(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +1,110 @@
|
||||
// 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 osuTK;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
{
|
||||
public class FollowPointRenderer : ConnectionRenderer<OsuHitObject>
|
||||
/// <summary>
|
||||
/// Visualises connections between <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPointRenderer : CompositeDrawable
|
||||
{
|
||||
private int pointDistance = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how much space there is between points.
|
||||
/// All the <see cref="FollowPointConnection"/>s contained by this <see cref="FollowPointRenderer"/>.
|
||||
/// </summary>
|
||||
public int PointDistance
|
||||
{
|
||||
get => pointDistance;
|
||||
set
|
||||
{
|
||||
if (pointDistance == value) return;
|
||||
internal IReadOnlyList<FollowPointConnection> Connections => connections;
|
||||
|
||||
pointDistance = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private int preEmpt = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Follow points to the next hitobject start appearing for this many milliseconds before an hitobject's end time.
|
||||
/// </summary>
|
||||
public int PreEmpt
|
||||
{
|
||||
get => preEmpt;
|
||||
set
|
||||
{
|
||||
if (preEmpt == value) return;
|
||||
|
||||
preEmpt = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> hitObjects;
|
||||
|
||||
public override IEnumerable<OsuHitObject> HitObjects
|
||||
{
|
||||
get => hitObjects;
|
||||
set
|
||||
{
|
||||
hitObjects = value;
|
||||
update();
|
||||
}
|
||||
}
|
||||
private readonly List<FollowPointConnection> connections = new List<FollowPointConnection>();
|
||||
|
||||
public override bool RemoveCompletedTransforms => false;
|
||||
|
||||
private void update()
|
||||
/// <summary>
|
||||
/// Adds the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
|
||||
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to add <see cref="FollowPoint"/>s for.</param>
|
||||
public void AddFollowPoints(DrawableOsuHitObject hitObject)
|
||||
=> addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g))));
|
||||
|
||||
/// <summary>
|
||||
/// Removes the <see cref="FollowPoint"/>s around a <see cref="DrawableOsuHitObject"/>.
|
||||
/// This includes <see cref="FollowPoint"/>s leading into <paramref name="hitObject"/>, and <see cref="FollowPoint"/>s exiting <paramref name="hitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableOsuHitObject"/> to remove <see cref="FollowPoint"/>s for.</param>
|
||||
public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="FollowPointConnection"/> to this <see cref="FollowPointRenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="FollowPointConnection"/> to add.</param>
|
||||
/// <returns>The index of <paramref name="connection"/> in <see cref="connections"/>.</returns>
|
||||
private void addConnection(FollowPointConnection connection)
|
||||
{
|
||||
ClearInternal();
|
||||
AddInternal(connection);
|
||||
|
||||
if (hitObjects == null)
|
||||
return;
|
||||
// Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections
|
||||
int index = connections.AddInPlace(connection, Comparer<FollowPointConnection>.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value)));
|
||||
|
||||
OsuHitObject prevHitObject = null;
|
||||
|
||||
foreach (var currHitObject in hitObjects)
|
||||
if (index < connections.Count - 1)
|
||||
{
|
||||
if (prevHitObject != null && !currHitObject.NewCombo && !(prevHitObject is Spinner) && !(currHitObject is Spinner))
|
||||
{
|
||||
Vector2 startPosition = prevHitObject.EndPosition;
|
||||
Vector2 endPosition = currHitObject.Position;
|
||||
double startTime = (prevHitObject as IHasEndTime)?.EndTime ?? prevHitObject.StartTime;
|
||||
double endTime = currHitObject.StartTime;
|
||||
// Update the connection's end point to the next connection's start point
|
||||
// h1 -> -> -> h2
|
||||
// connection nextGroup
|
||||
|
||||
Vector2 distanceVector = endPosition - startPosition;
|
||||
int distance = (int)distanceVector.Length;
|
||||
float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI));
|
||||
double duration = endTime - startTime;
|
||||
|
||||
for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance)
|
||||
{
|
||||
float fraction = (float)d / distance;
|
||||
Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector;
|
||||
Vector2 pointEndPosition = startPosition + fraction * distanceVector;
|
||||
double fadeOutTime = startTime + fraction * duration;
|
||||
double fadeInTime = fadeOutTime - PreEmpt;
|
||||
|
||||
FollowPoint fp;
|
||||
|
||||
AddInternal(fp = new FollowPoint
|
||||
{
|
||||
Position = pointStartPosition,
|
||||
Rotation = rotation,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(1.5f * currHitObject.Scale),
|
||||
});
|
||||
|
||||
using (fp.BeginAbsoluteSequence(fadeInTime))
|
||||
{
|
||||
fp.FadeIn(currHitObject.TimeFadeIn);
|
||||
fp.ScaleTo(currHitObject.Scale, currHitObject.TimeFadeIn, Easing.Out);
|
||||
|
||||
fp.MoveTo(pointEndPosition, currHitObject.TimeFadeIn, Easing.Out);
|
||||
|
||||
fp.Delay(fadeOutTime - fadeInTime).FadeOut(currHitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
fp.Expire(true);
|
||||
}
|
||||
}
|
||||
|
||||
prevHitObject = currHitObject;
|
||||
FollowPointConnection nextConnection = connections[index + 1];
|
||||
connection.End = nextConnection.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The end point may be non-null during re-ordering
|
||||
connection.End = null;
|
||||
}
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
// Update the previous connection's end point to the current connection's start point
|
||||
// h1 -> -> -> h2
|
||||
// prevGroup connection
|
||||
|
||||
FollowPointConnection previousConnection = connections[index - 1];
|
||||
previousConnection.End = connection.Start;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a <see cref="FollowPointConnection"/> from this <see cref="FollowPointRenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="connection">The <see cref="FollowPointConnection"/> to remove.</param>
|
||||
/// <returns>Whether <paramref name="connection"/> was removed.</returns>
|
||||
private void removeGroup(FollowPointConnection connection)
|
||||
{
|
||||
RemoveInternal(connection);
|
||||
|
||||
int index = connections.IndexOf(connection);
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
// Update the previous connection's end point to the next connection's start point
|
||||
// h1 -> -> -> h2 -> -> -> h3
|
||||
// prevGroup connection nextGroup
|
||||
// The current connection's end point is used since there may not be a next connection
|
||||
FollowPointConnection previousConnection = connections[index - 1];
|
||||
previousConnection.End = connection.End;
|
||||
}
|
||||
|
||||
connections.Remove(connection);
|
||||
}
|
||||
|
||||
private void onStartTimeChanged(FollowPointConnection connection)
|
||||
{
|
||||
// Naive but can be improved if performance becomes an issue
|
||||
removeGroup(connection);
|
||||
addConnection(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
private readonly ApproachCircleProxyContainer approachCircles;
|
||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||
private readonly ConnectionRenderer<OsuHitObject> connectionLayer;
|
||||
private readonly FollowPointRenderer followPoints;
|
||||
|
||||
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
|
||||
|
||||
@ -30,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
connectionLayer = new FollowPointRenderer
|
||||
followPoints = new FollowPointRenderer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 2,
|
||||
@ -64,11 +63,18 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
};
|
||||
|
||||
base.Add(h);
|
||||
|
||||
followPoints.AddFollowPoints((DrawableOsuHitObject)h);
|
||||
}
|
||||
|
||||
public override void PostProcess()
|
||||
public override bool Remove(DrawableHitObject h)
|
||||
{
|
||||
connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType<OsuHitObject>();
|
||||
bool result = base.Remove(h);
|
||||
|
||||
if (result)
|
||||
followPoints.RemoveFollowPoints((DrawableOsuHitObject)h);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
|
@ -32,7 +32,11 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
{
|
||||
editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
|
||||
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -42,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
},
|
||||
new TestDistanceSnapGrid(new HitObject(), grid_position)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
@ -57,12 +61,29 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLimitedDistance()
|
||||
{
|
||||
AddStep("create limited grid", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.SlateGray
|
||||
},
|
||||
new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private class TestDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
public new float DistanceSpacing => base.DistanceSpacing;
|
||||
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
|
||||
: base(hitObject, nextHitObject, centrePosition)
|
||||
{
|
||||
}
|
||||
|
||||
@ -77,7 +98,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
int beatIndex = 0;
|
||||
|
||||
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth; s += DistanceSpacing, beatIndex++)
|
||||
for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
|
||||
{
|
||||
AddInternal(new Circle
|
||||
{
|
||||
@ -90,7 +111,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
beatIndex = 0;
|
||||
|
||||
for (float s = centrePosition.X - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
|
||||
for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
|
||||
{
|
||||
AddInternal(new Circle
|
||||
{
|
||||
@ -103,7 +124,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
beatIndex = 0;
|
||||
|
||||
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight; s += DistanceSpacing, beatIndex++)
|
||||
for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
|
||||
{
|
||||
AddInternal(new Circle
|
||||
{
|
||||
@ -116,7 +137,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
beatIndex = 0;
|
||||
|
||||
for (float s = centrePosition.Y - DistanceSpacing; s >= 0; s -= DistanceSpacing, beatIndex++)
|
||||
for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
|
||||
{
|
||||
AddInternal(new Circle
|
||||
{
|
||||
@ -138,9 +159,9 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
|
||||
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
|
||||
|
||||
public float DurationToDistance(double referenceTime, double duration) => 0;
|
||||
public float DurationToDistance(double referenceTime, double duration) => (float)duration;
|
||||
|
||||
public double DistanceToDuration(double referenceTime, float distance) => 0;
|
||||
public double DistanceToDuration(double referenceTime, float distance) => distance;
|
||||
|
||||
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
|
||||
|
||||
|
@ -10,9 +10,9 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Editor
|
||||
}
|
||||
}
|
||||
|
||||
private class StartStopButton : Button
|
||||
private class StartStopButton : OsuButton
|
||||
{
|
||||
private IAdjustableClock adjustableClock;
|
||||
private bool started;
|
||||
|
@ -1,10 +1,12 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -19,53 +21,104 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
public class OsuButton : Button
|
||||
{
|
||||
private Box hover;
|
||||
public string Text
|
||||
{
|
||||
get => SpriteText?.Text;
|
||||
set
|
||||
{
|
||||
if (SpriteText != null)
|
||||
SpriteText.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Color4? backgroundColour;
|
||||
|
||||
public Color4 BackgroundColour
|
||||
{
|
||||
set
|
||||
{
|
||||
backgroundColour = value;
|
||||
Background.FadeColour(value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
protected Box Hover;
|
||||
protected Box Background;
|
||||
protected SpriteText SpriteText;
|
||||
|
||||
public OsuButton()
|
||||
{
|
||||
Height = 40;
|
||||
|
||||
Content.Masking = true;
|
||||
Content.CornerRadius = 5;
|
||||
AddInternal(Content = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Background = new Box
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Hover = new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White.Opacity(.1f),
|
||||
Blending = BlendingParameters.Additive,
|
||||
Depth = float.MinValue
|
||||
},
|
||||
SpriteText = CreateText(),
|
||||
new HoverClickSounds(HoverSampleSet.Loud),
|
||||
}
|
||||
});
|
||||
|
||||
Enabled.BindValueChanged(enabledChanged, true);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundColour = colours.BlueDark;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
hover = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Color4.White.Opacity(0.1f),
|
||||
Alpha = 0,
|
||||
Depth = -1
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Loud),
|
||||
});
|
||||
if (backgroundColour == null)
|
||||
BackgroundColour = colours.BlueDark;
|
||||
|
||||
Enabled.ValueChanged += enabledChanged;
|
||||
Enabled.TriggerChange();
|
||||
}
|
||||
|
||||
private void enabledChanged(ValueChangedEvent<bool> e)
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
|
||||
if (Enabled.Value)
|
||||
{
|
||||
Debug.Assert(backgroundColour != null);
|
||||
Background.FlashColour(backgroundColour.Value, 200);
|
||||
}
|
||||
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
hover.FadeIn(200);
|
||||
return true;
|
||||
if (Enabled.Value)
|
||||
Hover.FadeIn(200, Easing.OutQuint);
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hover.FadeOut(200);
|
||||
base.OnHoverLost(e);
|
||||
|
||||
Hover.FadeOut(300);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@ -80,12 +133,17 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override SpriteText CreateText() => new OsuSpriteText
|
||||
protected virtual SpriteText CreateText() => new OsuSpriteText
|
||||
{
|
||||
Depth = -1,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Bold)
|
||||
};
|
||||
|
||||
private void enabledChanged(ValueChangedEvent<bool> e)
|
||||
{
|
||||
this.FadeColour(e.NewValue ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,12 +168,13 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
}
|
||||
},
|
||||
progressBar = new ProgressBar
|
||||
progressBar = new HoverableProgressBar
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Height = progress_height,
|
||||
Height = progress_height / 2,
|
||||
FillColour = colours.Yellow,
|
||||
BackgroundColour = colours.YellowDarker.Opacity(0.5f),
|
||||
OnSeek = musicController.SeekTo
|
||||
}
|
||||
},
|
||||
@ -401,5 +402,20 @@ namespace osu.Game.Overlays
|
||||
return base.OnDragEnd(e);
|
||||
}
|
||||
}
|
||||
|
||||
private class HoverableProgressBar : ProgressBar
|
||||
{
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
this.ResizeHeightTo(progress_height / 2, 500, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
@ -9,21 +8,18 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class SidebarButton : Button
|
||||
public class SidebarButton : OsuButton
|
||||
{
|
||||
private readonly SpriteIcon drawableIcon;
|
||||
private readonly SpriteText headerText;
|
||||
private readonly Box selectionIndicator;
|
||||
private readonly Container text;
|
||||
public new Action<SettingsSection> Action;
|
||||
|
||||
private SettingsSection section;
|
||||
|
||||
@ -62,12 +58,11 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public SidebarButton()
|
||||
{
|
||||
BackgroundColour = OsuColour.Gray(60);
|
||||
Background.Alpha = 0;
|
||||
|
||||
Height = Sidebar.DEFAULT_WIDTH;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
BackgroundColour = Color4.Black;
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
text = new Container
|
||||
@ -99,7 +94,6 @@ namespace osu.Game.Overlays.Settings
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.Loud),
|
||||
});
|
||||
}
|
||||
|
||||
@ -108,23 +102,5 @@ namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
selectionIndicator.Colour = colours.Yellow;
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Action?.Invoke(section);
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Background.FadeTo(0.4f, 200);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
Background.FadeTo(0, 200);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,9 +123,9 @@ namespace osu.Game.Overlays
|
||||
var button = new SidebarButton
|
||||
{
|
||||
Section = section,
|
||||
Action = s =>
|
||||
Action = () =>
|
||||
{
|
||||
SectionsContainer.ScrollTo(s);
|
||||
SectionsContainer.ScrollTo(section);
|
||||
Sidebar.State = ExpandedState.Contracted;
|
||||
},
|
||||
};
|
||||
|
@ -12,8 +12,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract class CircularDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
: base(hitObject, centrePosition)
|
||||
protected CircularDistanceSnapGrid(HitObject hitObject, HitObject nextHitObject, Vector2 centrePosition)
|
||||
: base(hitObject, nextHitObject, centrePosition)
|
||||
{
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
float dx = Math.Max(centrePosition.X, DrawWidth - centrePosition.X);
|
||||
float dy = Math.Max(centrePosition.Y, DrawHeight - centrePosition.Y);
|
||||
float maxDistance = new Vector2(dx, dy).Length;
|
||||
int requiredCircles = (int)(maxDistance / DistanceSpacing);
|
||||
int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing));
|
||||
|
||||
for (int i = 0; i < requiredCircles; i++)
|
||||
{
|
||||
@ -65,15 +65,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
|
||||
{
|
||||
Vector2 direction = position - CentrePosition;
|
||||
if (MaxIntervals == 0)
|
||||
return (CentrePosition, StartTime);
|
||||
|
||||
Vector2 direction = position - CentrePosition;
|
||||
if (direction == Vector2.Zero)
|
||||
direction = new Vector2(0.001f, 0.001f);
|
||||
|
||||
float distance = direction.Length;
|
||||
|
||||
float radius = DistanceSpacing;
|
||||
int radialCount = Math.Max(1, (int)Math.Round(distance / radius));
|
||||
int radialCount = MathHelper.Clamp((int)Math.Round(distance / radius), 1, MaxIntervals);
|
||||
|
||||
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
|
||||
Vector2 snappedPosition = CentrePosition + normalisedDirection * radialCount * radius;
|
||||
|
@ -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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
@ -29,6 +30,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
protected double StartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of distance snapping intervals allowed.
|
||||
/// </summary>
|
||||
protected int MaxIntervals { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position which the grid is centred on.
|
||||
/// The first beat snapping tick is located at <see cref="CentrePosition"/> + <see cref="DistanceSpacing"/> in the desired direction.
|
||||
@ -49,12 +55,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly Cached gridCache = new Cached();
|
||||
private readonly HitObject hitObject;
|
||||
private readonly HitObject nextHitObject;
|
||||
|
||||
protected DistanceSnapGrid(HitObject hitObject, Vector2 centrePosition)
|
||||
protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
this.nextHitObject = nextHitObject;
|
||||
|
||||
CentrePosition = centrePosition;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
@ -74,6 +83,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void updateSpacing()
|
||||
{
|
||||
DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime);
|
||||
|
||||
if (nextHitObject == null)
|
||||
MaxIntervals = int.MaxValue;
|
||||
else
|
||||
{
|
||||
// +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors
|
||||
double maxDuration = nextHitObject.StartTime - StartTime + 1;
|
||||
MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing));
|
||||
}
|
||||
|
||||
gridCache.Invalidate();
|
||||
}
|
||||
|
||||
|
@ -128,17 +128,17 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
switch (controlPoint)
|
||||
{
|
||||
case TimingControlPoint timing:
|
||||
return new RowAttribute("timing", $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
|
||||
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
|
||||
|
||||
case DifficultyControlPoint difficulty:
|
||||
|
||||
return new RowAttribute("difficulty", $"{difficulty.SpeedMultiplier:n2}x");
|
||||
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
|
||||
|
||||
case EffectControlPoint effect:
|
||||
return new RowAttribute("effect", $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
|
||||
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
|
||||
|
||||
case SampleControlPoint sample:
|
||||
return new RowAttribute("sample", $"{sample.SampleBank} {sample.SampleVolume}%");
|
||||
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -2,28 +2,37 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class DifficultySection : Section<DifficultyControlPoint>
|
||||
{
|
||||
private OsuSpriteText multiplier;
|
||||
private SettingsSlider<double> multiplier;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new[]
|
||||
{
|
||||
multiplier = new OsuSpriteText(),
|
||||
multiplier = new SettingsSlider<double>
|
||||
{
|
||||
LabelText = "Speed Multiplier",
|
||||
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnControlPointChanged(ValueChangedEvent<DifficultyControlPoint> point)
|
||||
{
|
||||
multiplier.Text = $"Multiplier: {point.NewValue?.SpeedMultiplier:0.##}x";
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override DifficultyControlPoint CreatePoint()
|
||||
|
@ -4,29 +4,32 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class EffectSection : Section<EffectControlPoint>
|
||||
{
|
||||
private OsuSpriteText kiai;
|
||||
private OsuSpriteText omitBarLine;
|
||||
private LabelledSwitchButton kiai;
|
||||
private LabelledSwitchButton omitBarLine;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new[]
|
||||
{
|
||||
kiai = new OsuSpriteText(),
|
||||
omitBarLine = new OsuSpriteText(),
|
||||
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
|
||||
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
|
||||
{
|
||||
kiai.Text = $"Kiai: {(point.NewValue?.KiaiMode == true ? "on" : "off")}";
|
||||
omitBarLine.Text = $"Skip Bar Line: {(point.NewValue?.OmitFirstBarLine == true ? "on" : "off")}";
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override EffectControlPoint CreatePoint()
|
||||
|
@ -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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -14,9 +15,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
public class RowAttribute : CompositeDrawable, IHasTooltip
|
||||
{
|
||||
private readonly string header;
|
||||
private readonly string content;
|
||||
private readonly Func<string> content;
|
||||
|
||||
public RowAttribute(string header, string content)
|
||||
public RowAttribute(string header, Func<string> content)
|
||||
{
|
||||
this.header = header;
|
||||
this.content = content;
|
||||
@ -54,6 +55,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
};
|
||||
}
|
||||
|
||||
public string TooltipText => content;
|
||||
public string TooltipText => content();
|
||||
}
|
||||
}
|
||||
|
@ -2,31 +2,43 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class SampleSection : Section<SampleControlPoint>
|
||||
{
|
||||
private OsuSpriteText bank;
|
||||
private OsuSpriteText volume;
|
||||
private LabelledTextBox bank;
|
||||
private SettingsSlider<int> volume;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new[]
|
||||
Flow.AddRange(new Drawable[]
|
||||
{
|
||||
bank = new OsuSpriteText(),
|
||||
volume = new OsuSpriteText(),
|
||||
bank = new LabelledTextBox
|
||||
{
|
||||
Label = "Bank Name",
|
||||
},
|
||||
volume = new SettingsSlider<int>
|
||||
{
|
||||
Bindable = new SampleControlPoint().SampleVolumeBindable,
|
||||
LabelText = "Volume",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnControlPointChanged(ValueChangedEvent<SampleControlPoint> point)
|
||||
{
|
||||
bank.Text = $"Bank: {point.NewValue?.SampleBank}";
|
||||
volume.Text = $"Volume: {point.NewValue?.SampleVolume}%";
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bank.Current = point.NewValue.SampleBankBindable;
|
||||
volume.Bindable = point.NewValue.SampleVolumeBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override SampleControlPoint CreatePoint()
|
||||
|
@ -3,30 +3,42 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
internal class TimingSection : Section<TimingControlPoint>
|
||||
{
|
||||
private OsuSpriteText bpm;
|
||||
private OsuSpriteText timeSignature;
|
||||
private SettingsSlider<double> bpm;
|
||||
private SettingsEnumDropdown<TimeSignatures> timeSignature;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Flow.AddRange(new[]
|
||||
Flow.AddRange(new Drawable[]
|
||||
{
|
||||
bpm = new OsuSpriteText(),
|
||||
timeSignature = new OsuSpriteText(),
|
||||
bpm = new BPMSlider
|
||||
{
|
||||
Bindable = new TimingControlPoint().BeatLengthBindable,
|
||||
LabelText = "BPM",
|
||||
},
|
||||
timeSignature = new SettingsEnumDropdown<TimeSignatures>
|
||||
{
|
||||
LabelText = "Time Signature"
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
|
||||
{
|
||||
bpm.Text = $"BPM: {point.NewValue?.BPM:0.##}";
|
||||
timeSignature.Text = $"Signature: {point.NewValue?.TimeSignature}";
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bpm.Bindable = point.NewValue.BeatLengthBindable;
|
||||
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override TimingControlPoint CreatePoint()
|
||||
@ -39,5 +51,35 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
TimeSignature = reference.TimeSignature
|
||||
};
|
||||
}
|
||||
|
||||
private class BPMSlider : SettingsSlider<double>
|
||||
{
|
||||
private readonly BindableDouble beatLengthBindable = new BindableDouble();
|
||||
|
||||
private BindableDouble bpmBindable;
|
||||
|
||||
public override Bindable<double> Bindable
|
||||
{
|
||||
get => base.Bindable;
|
||||
set
|
||||
{
|
||||
// incoming will be beatlength
|
||||
|
||||
beatLengthBindable.UnbindBindings();
|
||||
beatLengthBindable.BindTo(value);
|
||||
|
||||
base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value))
|
||||
{
|
||||
MinValue = beatLengthToBpm(beatLengthBindable.MaxValue),
|
||||
MaxValue = beatLengthToBpm(beatLengthBindable.MinValue),
|
||||
Default = beatLengthToBpm(beatLengthBindable.Default),
|
||||
};
|
||||
|
||||
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
|
||||
}
|
||||
}
|
||||
|
||||
private double beatLengthToBpm(double beatLength) => 60000 / beatLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2019.1106.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
@ -118,8 +118,8 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1029.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2019.1106.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1106.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user