1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge pull request #18156 from ekrctb/catch-editor-per-object-sv

Automatically adjust per-hit object slider velocity in osu!catch editor
This commit is contained in:
Dean Herbert 2022-05-19 19:12:38 +09:00 committed by GitHub
commit 77b4bd97f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 211 additions and 238 deletions

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene public class TestSceneJuiceStreamPlacementBlueprint : CatchPlacementBlueprintTestScene
{ {
private const double velocity = 0.5; private const double velocity_factor = 0.5;
private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; private JuiceStream lastObject => LastObject?.HitObject as JuiceStream;
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var playable = base.GetPlayableBeatmap(); var playable = base.GetPlayableBeatmap();
playable.Difficulty.SliderTickRate = 5; playable.Difficulty.SliderTickRate = 5;
playable.Difficulty.SliderMultiplier = velocity * 10; playable.Difficulty.SliderMultiplier = velocity_factor * 10;
return playable; return playable;
} }
@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]
@ -66,28 +67,21 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestVelocityLimit() public void TestSliderVelocityChange()
{ {
double[] times = { 100, 300 }; double[] times = { 100, 300 };
float[] positions = { 200, 500 }; float[] positions = { 200, 500 };
addPlacementSteps(times, positions); addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300 }); addPathCheckStep(times, positions);
}
[Test] AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
public void TestPreviousVerticesAreFixed()
{
double[] times = { 100, 300, 500, 700 };
float[] positions = { 200, 400, 100, 500 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 200, 300, 200, 300 });
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestClampedPositionIsRestored()
{ {
double[] times = { 100, 300, 500 }; double[] times = { 100, 300, 500 };
float[] positions = { 200, 200, 0, 250 }; float[] positions = { 200, 200, -3000, 250 };
addMoveAndClickSteps(times[0], positions[0]); addMoveAndClickSteps(times[0], positions[0]);
addMoveAndClickSteps(times[1], positions[1]); addMoveAndClickSteps(times[1], positions[1]);
@ -97,15 +91,6 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPathCheckStep(times, new float[] { 200, 200, 250 }); addPathCheckStep(times, new float[] { 200, 200, 250 });
} }
[Test]
public void TestFirstVertexIsFixed()
{
double[] times = { 100, 200 };
float[] positions = { 100, 300 };
addPlacementSteps(times, positions);
addPathCheckStep(times, new float[] { 100, 150 });
}
[Test] [Test]
public void TestOutOfOrder() public void TestOutOfOrder()
{ {

View File

@ -101,31 +101,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
} }
[Test] [Test]
public void TestClampedPositionIsRestored() public void TestSliderVelocityChange()
{ {
const double velocity = 0.25; double[] times = { 100, 300 };
double[] times = { 100, 500, 700 }; float[] positions = { 200, 300 };
float[] positions = { 100, 100, 100 }; addBlueprintStep(times, positions);
addBlueprintStep(times, positions, velocity); AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddMouseMoveStep(times[1], 200); AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault);
addVertexCheckStep(3, 1, times[1], 200);
addVertexCheckStep(3, 2, times[2], 150);
AddMouseMoveStep(times[1], 100);
addVertexCheckStep(3, 1, times[1], 100);
// Stored position is restored.
addVertexCheckStep(3, 2, times[2], positions[2]);
AddMouseMoveStep(times[1], 300);
addDragEndStep();
addDragStartStep(times[1], 300);
AddMouseMoveStep(times[1], 100);
// Position is different because a changed position is committed when the previous drag is ended.
addVertexCheckStep(3, 2, times[2], 250);
} }
[Test] [Test]
@ -174,7 +159,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150); addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150); addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 220); addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]); addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(750, 180); addAddVertexSteps(750, 180);
@ -234,10 +219,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{ {
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
for (int i = 1; i < times.Length; i++) for (int i = 1; i < times.Length; i++)
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]); path.Add(times[i] - times[0], positions[i] - positions[0]);
var sliderPath = new SliderPath(); var sliderPath = new SliderPath();
path.ConvertToSliderPath(sliderPath, 0); path.ConvertToSliderPath(sliderPath, 0, velocity);
addBlueprintStep(times[0], positions[0], sliderPath, velocity); addBlueprintStep(times[0], positions[0], sliderPath, velocity);
} }
@ -245,11 +230,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () => private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
{ {
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity; double expectedTime = time - hitObject.StartTime;
float expectedX = x - hitObject.OriginalX; float expectedX = x - hitObject.OriginalX;
var vertices = getVertices(); var vertices = getVertices();
return vertices.Count == count && return vertices.Count == count &&
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) && Precision.AlmostEquals(vertices[index].Time, expectedTime, 1e-3) &&
Precision.AlmostEquals(vertices[index].X, expectedX); Precision.AlmostEquals(vertices[index].X, expectedX);
}); });

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -37,14 +36,14 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
case 0: case 0:
{ {
double distance = rng.NextDouble() * scale * 2 - scale; double time = rng.NextDouble() * scale * 2 - scale;
if (integralValues) if (integralValues)
distance = Math.Round(distance); time = Math.Round(time);
float oldX = path.PositionAtDistance(distance); float oldX = path.PositionAtTime(time);
int index = path.InsertVertex(distance); int index = path.InsertVertex(time);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1)); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount + 1));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(oldX)); Assert.That(path.Vertices[index].X, Is.EqualTo(oldX));
break; break;
} }
@ -52,20 +51,20 @@ namespace osu.Game.Rulesets.Catch.Tests
case 1: case 1:
{ {
int index = rng.Next(path.Vertices.Count); int index = rng.Next(path.Vertices.Count);
double distance = path.Vertices[index].Distance; double time = path.Vertices[index].Time;
float newX = (float)(rng.NextDouble() * scale * 2 - scale); float newX = (float)(rng.NextDouble() * scale * 2 - scale);
if (integralValues) if (integralValues)
newX = MathF.Round(newX); newX = MathF.Round(newX);
path.SetVertexPosition(index, newX); path.SetVertexPosition(index, newX);
Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount)); Assert.That(path.Vertices.Count, Is.EqualTo(vertexCount));
Assert.That(path.Vertices[index].Distance, Is.EqualTo(distance)); Assert.That(path.Vertices[index].Time, Is.EqualTo(time));
Assert.That(path.Vertices[index].X, Is.EqualTo(newX)); Assert.That(path.Vertices[index].X, Is.EqualTo(newX));
break; break;
} }
} }
assertInvariants(path.Vertices, checkSlope); assertInvariants(path.Vertices);
} }
} }
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(10, 5); path.Add(10, 5);
path.Add(20, -5); path.Add(20, -5);
int removeCount = path.RemoveVertices((v, i) => v.Distance == 10 && i == 1); int removeCount = path.RemoveVertices((v, i) => v.Time == 10 && i == 1);
Assert.That(removeCount, Is.EqualTo(1)); Assert.That(removeCount, Is.EqualTo(1));
Assert.That(path.Vertices, Is.EqualTo(new[] Assert.That(path.Vertices, Is.EqualTo(new[]
{ {
@ -131,8 +130,9 @@ namespace osu.Game.Rulesets.Catch.Tests
})); }));
} }
[Test] [TestCase(10)]
public void TestRandomConvertFromSliderPath() [TestCase(0.1)]
public void TestRandomConvertFromSliderPath(double velocity)
{ {
var rng = new Random(1); var rng = new Random(1);
var path = new JuiceStreamPath(); var path = new JuiceStreamPath();
@ -162,28 +162,28 @@ namespace osu.Game.Rulesets.Catch.Tests
else else
sliderPath.ExpectedDistance.Value = null; sliderPath.ExpectedDistance.Value = null;
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath, velocity);
Assert.That(path.Vertices[0].Distance, Is.EqualTo(0)); Assert.That(path.Vertices[0].Time, Is.EqualTo(0));
Assert.That(path.Distance, Is.EqualTo(sliderPath.Distance).Within(1e-3)); Assert.That(path.Duration * velocity, Is.EqualTo(sliderPath.Distance).Within(1e-3));
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
double[] sampleDistances = Enumerable.Range(0, 10) double[] sampleTimes = Enumerable.Range(0, 10)
.Select(_ => rng.NextDouble() * sliderPath.Distance) .Select(_ => rng.NextDouble() * sliderPath.Distance / velocity)
.ToArray(); .ToArray();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
path.ResampleVertices(sampleDistances); path.ResampleVertices(sampleTimes);
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices);
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
float expected = sliderPath.PositionAt(distance / sliderPath.Distance).X; float expected = sliderPath.PositionAt(time * velocity / sliderPath.Distance).X;
Assert.That(path.PositionAtDistance(distance), Is.EqualTo(expected).Within(1e-3)); Assert.That(path.PositionAtTime(time), Is.EqualTo(expected).Within(1e-3));
} }
} }
} }
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Catch.Tests
do do
{ {
double distance = rng.NextDouble() * 1e3; double time = rng.NextDouble() * 1e3;
float x = (float)(rng.NextDouble() * 1e3); float x = (float)(rng.NextDouble() * 1e3);
path.Add(distance, x); path.Add(time, x);
} while (rng.Next(5) != 0); } while (rng.Next(5) != 0);
float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT); float sliderStartY = (float)(rng.NextDouble() * JuiceStreamPath.OSU_PLAYFIELD_HEIGHT);
path.ConvertToSliderPath(sliderPath, sliderStartY); double requiredVelocity = path.ComputeRequiredVelocity();
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); double velocity = Math.Clamp(requiredVelocity, 1, 100);
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true); path.ConvertToSliderPath(sliderPath, sliderStartY, velocity);
foreach (var point in sliderPath.ControlPoints) foreach (var point in sliderPath.ControlPoints)
{ {
@ -219,11 +219,18 @@ namespace osu.Game.Rulesets.Catch.Tests
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
} }
Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
// The path is preserved only if required velocity is used.
if (velocity < requiredVelocity) continue;
Assert.That(sliderPath.Distance / velocity, Is.EqualTo(path.Duration).Within(1e-3));
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
double distance = rng.NextDouble() * path.Distance; double time = rng.NextDouble() * path.Duration;
float expected = path.PositionAtDistance(distance); float expected = path.PositionAtTime(time);
Assert.That(sliderPath.PositionAt(distance / sliderPath.Distance).X, Is.EqualTo(expected).Within(1e-3)); Assert.That(sliderPath.PositionAt(time * velocity / sliderPath.Distance).X, Is.EqualTo(expected).Within(3e-3));
} }
} }
} }
@ -244,7 +251,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Add(20, 0); path.Add(20, 0);
checkNewId(); checkNewId();
path.RemoveVertices((v, _) => v.Distance == 20); path.RemoveVertices((v, _) => v.Time == 20);
checkNewId(); checkNewId();
path.ResampleVertices(new double[] { 5, 10, 15 }); path.ResampleVertices(new double[] { 5, 10, 15 });
@ -253,7 +260,7 @@ namespace osu.Game.Rulesets.Catch.Tests
path.Clear(); path.Clear();
checkNewId(); checkNewId();
path.ConvertFromSliderPath(new SliderPath()); path.ConvertFromSliderPath(new SliderPath(), 1);
checkNewId(); checkNewId();
void checkNewId() void checkNewId()
@ -263,25 +270,19 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
} }
private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices, bool checkSlope) private void assertInvariants(IReadOnlyList<JuiceStreamPathVertex> vertices)
{ {
Assert.That(vertices, Is.Not.Empty); Assert.That(vertices, Is.Not.Empty);
for (int i = 0; i < vertices.Count; i++) for (int i = 0; i < vertices.Count; i++)
{ {
Assert.That(double.IsFinite(vertices[i].Distance)); Assert.That(double.IsFinite(vertices[i].Time));
Assert.That(float.IsFinite(vertices[i].X)); Assert.That(float.IsFinite(vertices[i].X));
} }
for (int i = 1; i < vertices.Count; i++) for (int i = 1; i < vertices.Count; i++)
{ {
Assert.That(vertices[i].Distance, Is.GreaterThanOrEqualTo(vertices[i - 1].Distance)); Assert.That(vertices[i].Time, Is.GreaterThanOrEqualTo(vertices[i - 1].Time));
if (!checkSlope) continue;
float xDiff = Math.Abs(vertices[i].X - vertices[i - 1].X);
double distanceDiff = vertices[i].Distance - vertices[i - 1].Distance;
Assert.That(xDiff, Is.LessThanOrEqualTo(distanceDiff).Within(Precision.FLOAT_EPSILON));
} }
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public int VertexCount => path.Vertices.Count; public int VertexCount => path.Vertices.Count;
protected readonly Func<float, double> PositionToDistance; protected readonly Func<float, double> PositionToTime;
protected IReadOnlyList<VertexState> VertexStates => vertexStates; protected IReadOnlyList<VertexState> VertexStates => vertexStates;
@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; }
protected EditablePath(Func<float, double> positionToDistance) protected EditablePath(Func<float, double> positionToTime)
{ {
PositionToDistance = positionToDistance; PositionToTime = positionToTime;
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
} }
@ -59,13 +59,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
while (InternalChildren.Count < path.Vertices.Count) while (InternalChildren.Count < path.Vertices.Count)
AddInternal(new VertexPiece()); AddInternal(new VertexPiece());
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
{ {
var piece = (VertexPiece)InternalChildren[i]; var piece = (VertexPiece)InternalChildren[i];
var vertex = path.Vertices[i]; var vertex = path.Vertices[i];
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor)); piece.Position = new Vector2(vertex.X, (float)(vertex.Time * timeToYFactor));
piece.UpdateFrom(vertexStates[i]); piece.UpdateFrom(vertexStates[i]);
} }
} }
@ -73,14 +73,14 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void InitializeFromHitObject(JuiceStream hitObject) public void InitializeFromHitObject(JuiceStream hitObject)
{ {
var sliderPath = hitObject.Path; var sliderPath = hitObject.Path;
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{ {
path.ResampleVertices(hitObject.NestedHitObjects path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity)); .Select(h => h.StartTime - hitObject.StartTime));
} }
vertexStates.Clear(); vertexStates.Clear();
@ -92,11 +92,26 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject) public void UpdateHitObjectFromPath(JuiceStream hitObject)
{ {
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY); // The SV setting may need to be changed for the current path.
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity();
// The value is pre-rounded here because setting it to the bindable will rounded to the nearest value
// but it should be always rounded up to satisfy the required minimum velocity condition.
//
// This is rounded to integers instead of using the precision of the bindable
// because it results in a smaller number of non-redundant control points.
//
// The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved.
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
if (beatSnapProvider == null) return; if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity; double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime); double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity; hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
} }
@ -108,9 +123,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
protected int AddVertex(double distance, float x) protected int AddVertex(double time, float x)
{ {
int index = path.InsertVertex(distance); int index = path.InsertVertex(time);
path.SetVertexPosition(index, x); path.SetVertexPosition(index, x);
vertexStates.Insert(index, new VertexState()); vertexStates.Insert(index, new VertexState());
@ -138,9 +153,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
return true; return true;
} }
protected void MoveSelectedVertices(double distanceDelta, float xDelta) protected void MoveSelectedVertices(double timeDelta, float xDelta)
{ {
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well. // Because the vertex list may be reordered due to time change, the state list must be reordered as well.
previousVertexStates.Clear(); previousVertexStates.Clear();
previousVertexStates.AddRange(vertexStates); previousVertexStates.AddRange(vertexStates);
@ -152,11 +167,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 1; i < vertexCount; i++) for (int i = 1; i < vertexCount; i++)
{ {
var state = previousVertexStates[i]; var state = previousVertexStates[i];
double distance = state.VertexBeforeChange.Distance; double time = state.VertexBeforeChange.Time;
if (state.IsSelected) if (state.IsSelected)
distance += distanceDelta; time += timeDelta;
int newIndex = path.InsertVertex(Math.Max(0, distance)); int newIndex = path.InsertVertex(Math.Max(0, time));
vertexStates.Insert(newIndex, state); vertexStates.Insert(newIndex, state);
} }

View File

@ -15,15 +15,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
/// </summary> /// </summary>
private JuiceStreamPathVertex lastVertex; private JuiceStreamPathVertex lastVertex;
public PlacementEditablePath(Func<float, double> positionToDistance) public PlacementEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddNewVertex() public void AddNewVertex()
{ {
var endVertex = Vertices[^1]; var endVertex = Vertices[^1];
int index = AddVertex(endVertex.Distance, endVertex.X); int index = AddVertex(endVertex.Time, endVertex.X);
for (int i = 0; i < VertexCount; i++) for (int i = 0; i < VertexCount; i++)
{ {
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void MoveLastVertex(Vector2 screenSpacePosition) public void MoveLastVertex(Vector2 screenSpacePosition)
{ {
Vector2 position = ToRelativePosition(screenSpacePosition); Vector2 position = ToRelativePosition(screenSpacePosition);
double distanceDelta = PositionToDistance(position.Y) - lastVertex.Distance; double timeDelta = PositionToTime(position.Y) - lastVertex.Time;
float xDelta = position.X - lastVertex.X; float xDelta = position.X - lastVertex.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
private readonly Path drawablePath; private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>(); private readonly List<(double Time, float X)> vertices = new List<(double, float)>();
public ScrollingPath() public ScrollingPath()
{ {
@ -35,16 +35,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject) public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{ {
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity); double timeToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1);
computeDistanceXs(hitObject); computeTimeXs(hitObject);
drawablePath.Vertices = vertices drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor))) .Select(v => new Vector2(v.X, (float)(v.Time * timeToYFactor)))
.ToArray(); .ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero); drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
} }
private void computeDistanceXs(JuiceStream hitObject) private void computeTimeXs(JuiceStream hitObject)
{ {
vertices.Clear(); vertices.Clear();
@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (sliderVertices.Count == 0) if (sliderVertices.Count == 0)
return; return;
double distance = 0; double time = 0;
Vector2 lastPosition = Vector2.Zero; Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++) for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{ {
foreach (var position in sliderVertices) foreach (var position in sliderVertices)
{ {
distance += Vector2.Distance(lastPosition, position); time += Vector2.Distance(lastPosition, position) / hitObject.Velocity;
lastPosition = position; lastPosition = position;
vertices.Add((distance, position.X)); vertices.Add((time, position.X));
} }
sliderVertices.Reverse(); sliderVertices.Reverse();

View File

@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[CanBeNull] [CanBeNull]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToDistance) public SelectionEditablePath(Func<float, double> positionToTime)
: base(positionToDistance) : base(positionToTime)
{ {
} }
public void AddVertex(Vector2 relativePosition) public void AddVertex(Vector2 relativePosition)
{ {
double distance = Math.Max(0, PositionToDistance(relativePosition.Y)); double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(distance, relativePosition.X); int index = AddVertex(time, relativePosition.X);
selectOnly(index); selectOnly(index);
} }
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition); Vector2 mousePosition = ToRelativePosition(e.ScreenSpaceMousePosition);
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y); double timeDelta = PositionToTime(mousePosition.Y) - PositionToTime(dragStartPosition.Y);
float xDelta = mousePosition.X - dragStartPosition.X; float xDelta = mousePosition.X - dragStartPosition.X;
MoveSelectedVertices(distanceDelta, xDelta); MoveSelectedVertices(timeDelta, xDelta);
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new PlacementEditablePath(positionToDistance) editablePath = new PlacementEditablePath(positionToTime)
}; };
} }
@ -121,10 +121,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
lastEditablePathId = editablePath.PathId; lastEditablePathId = editablePath.PathId;
} }
private double positionToDistance(float relativeYPosition) private double positionToTime(float relativeYPosition)
{ {
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
} }
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
scrollingPath = new ScrollingPath(), scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(), nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToDistance) editablePath = new SelectionEditablePath(positionToTime)
}; };
} }
@ -145,10 +145,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius); return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
} }
private double positionToDistance(float relativeYPosition) private double positionToTime(float relativeYPosition)
{ {
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime); double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
return (time - HitObject.StartTime) * HitObject.Velocity; return time - HitObject.StartTime;
} }
private void initializeJuiceStreamPath() private void initializeJuiceStreamPath()

View File

@ -27,10 +27,16 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
[JsonIgnore] [JsonIgnore]
public double Velocity { get; private set; } private double velocityFactor;
[JsonIgnore] [JsonIgnore]
public double TickDistance { get; private set; } private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.
@ -43,10 +49,8 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// However, the <see cref="SliderPath"/> representation is difficult to work with. /// However, the <see cref="SliderPath"/> representation is difficult to work with.
/// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s. /// This <see cref="JuiceStreamPath"/> represents the path in a more convenient way, a polyline connecting list of <see cref="JuiceStreamPathVertex"/>s.
/// </para> /// </para>
/// <para>
/// The path can be regarded as a function from the closed interval <c>[Vertices[0].Distance, Vertices[^1].Distance]</c> to the x position, given by <see cref="PositionAtDistance"/>.
/// To ensure the path is convertible to a <see cref="SliderPath"/>, the slope of the function must not be more than <c>1</c> everywhere,
/// and this slope condition is always maintained as an invariant.
/// </para>
/// </summary> /// </summary>
public class JuiceStreamPath public class JuiceStreamPath
{ {
@ -46,9 +41,9 @@ namespace osu.Game.Rulesets.Catch.Objects
public int InvalidationID { get; private set; } = 1; public int InvalidationID { get; private set; } = 1;
/// <summary> /// <summary>
/// The difference between first vertex's <see cref="JuiceStreamPathVertex.Distance"/> and last vertex's <see cref="JuiceStreamPathVertex.Distance"/>. /// The difference between first vertex's <see cref="JuiceStreamPathVertex.Time"/> and last vertex's <see cref="JuiceStreamPathVertex.Time"/>.
/// </summary> /// </summary>
public double Distance => vertices[^1].Distance - vertices[0].Distance; public double Duration => vertices[^1].Time - vertices[0].Time;
/// <remarks> /// <remarks>
/// This list should always be non-empty. /// This list should always be non-empty.
@ -59,15 +54,15 @@ namespace osu.Game.Rulesets.Catch.Objects
}; };
/// <summary> /// <summary>
/// Compute the x-position of the path at the given <paramref name="distance"/>. /// Compute the x-position of the path at the given <paramref name="time"/>.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When the given distance is outside of the path, the x position at the corresponding endpoint is returned, /// When the given time is outside of the path, the x position at the corresponding endpoint is returned,
/// </remarks> /// </remarks>
public float PositionAtDistance(double distance) public float PositionAtTime(double time)
{ {
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
return positionAtDistance(distance, index); return positionAtTime(time, index);
} }
/// <summary> /// <summary>
@ -81,19 +76,19 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Insert a vertex at given <paramref name="distance"/>. /// Insert a vertex at given <paramref name="time"/>.
/// The <see cref="PositionAtDistance"/> is used as the position of the new vertex. /// The <see cref="PositionAtTime"/> is used as the position of the new vertex.
/// Thus, the set of points of the path is not changed (up to floating-point precision). /// Thus, the set of points of the path is not changed (up to floating-point precision).
/// </summary> /// </summary>
/// <returns>The index of the new vertex.</returns> /// <returns>The index of the new vertex.</returns>
public int InsertVertex(double distance) public int InsertVertex(double time)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(distance)); throw new ArgumentOutOfRangeException(nameof(time));
int index = vertexIndexAtDistance(distance); int index = vertexIndexAtTime(time);
float x = positionAtDistance(distance, index); float x = positionAtTime(time, index);
vertices.Insert(index, new JuiceStreamPathVertex(distance, x)); vertices.Insert(index, new JuiceStreamPathVertex(time, x));
invalidate(); invalidate();
return index; return index;
@ -101,7 +96,6 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary> /// <summary>
/// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>. /// Move the vertex of given <paramref name="index"/> to the given position <paramref name="newX"/>.
/// When the distances between vertices are too small for the new vertex positions, the adjacent vertices are moved towards <paramref name="newX"/>.
/// </summary> /// </summary>
public void SetVertexPosition(int index, float newX) public void SetVertexPosition(int index, float newX)
{ {
@ -111,32 +105,17 @@ namespace osu.Game.Rulesets.Catch.Objects
if (!float.IsFinite(newX)) if (!float.IsFinite(newX))
throw new ArgumentOutOfRangeException(nameof(newX)); throw new ArgumentOutOfRangeException(nameof(newX));
var newVertex = new JuiceStreamPathVertex(vertices[index].Distance, newX); vertices[index] = new JuiceStreamPathVertex(vertices[index].Time, newX);
for (int i = index - 1; i >= 0 && !canConnect(vertices[i], newVertex); i--)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
for (int i = index + 1; i < vertices.Count; i++)
{
float clampedX = clampToConnectablePosition(newVertex, vertices[i]);
vertices[i] = new JuiceStreamPathVertex(vertices[i].Distance, clampedX);
}
vertices[index] = newVertex;
invalidate(); invalidate();
} }
/// <summary> /// <summary>
/// Add a new vertex at given <paramref name="distance"/> and position. /// Add a new vertex at given <paramref name="time"/> and position.
/// Adjacent vertices are moved when necessary in the same way as <see cref="SetVertexPosition"/>.
/// </summary> /// </summary>
public void Add(double distance, float x) public void Add(double time, float x)
{ {
int index = InsertVertex(distance); int index = InsertVertex(time);
SetVertexPosition(index, x); SetVertexPosition(index, x);
} }
@ -163,22 +142,22 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
/// <summary> /// <summary>
/// Recreate this path by using difference set of vertices at given distances. /// Recreate this path by using difference set of vertices at given time points.
/// In addition to the given <paramref name="sampleDistances"/>, the first vertex and the last vertex are always added to the new path. /// In addition to the given <paramref name="sampleTimes"/>, the first vertex and the last vertex are always added to the new path.
/// New vertices use the positions on the original path. Thus, <see cref="PositionAtDistance"/>s at <paramref name="sampleDistances"/> are preserved. /// New vertices use the positions on the original path. Thus, <see cref="PositionAtTime"/>s at <paramref name="sampleTimes"/> are preserved.
/// </summary> /// </summary>
public void ResampleVertices(IEnumerable<double> sampleDistances) public void ResampleVertices(IEnumerable<double> sampleTimes)
{ {
var sampledVertices = new List<JuiceStreamPathVertex>(); var sampledVertices = new List<JuiceStreamPathVertex>();
foreach (double distance in sampleDistances) foreach (double time in sampleTimes)
{ {
if (!double.IsFinite(distance)) if (!double.IsFinite(time))
throw new ArgumentOutOfRangeException(nameof(sampleDistances)); throw new ArgumentOutOfRangeException(nameof(sampleTimes));
double clampedDistance = Math.Clamp(distance, vertices[0].Distance, vertices[^1].Distance); double clampedTime = Math.Clamp(time, vertices[0].Time, vertices[^1].Time);
float x = PositionAtDistance(clampedDistance); float x = PositionAtTime(clampedTime);
sampledVertices.Add(new JuiceStreamPathVertex(clampedDistance, x)); sampledVertices.Add(new JuiceStreamPathVertex(clampedTime, x));
} }
sampledVertices.Sort(); sampledVertices.Sort();
@ -196,37 +175,62 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <remarks> /// <remarks>
/// Duplicated vertices are automatically removed. /// Duplicated vertices are automatically removed.
/// </remarks> /// </remarks>
public void ConvertFromSliderPath(SliderPath sliderPath) public void ConvertFromSliderPath(SliderPath sliderPath, double velocity)
{ {
var sliderPathVertices = new List<Vector2>(); var sliderPathVertices = new List<Vector2>();
sliderPath.GetPathToProgress(sliderPathVertices, 0, 1); sliderPath.GetPathToProgress(sliderPathVertices, 0, 1);
double distance = 0; double time = 0;
vertices.Clear(); vertices.Clear();
vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X)); vertices.Add(new JuiceStreamPathVertex(0, sliderPathVertices.FirstOrDefault().X));
for (int i = 1; i < sliderPathVertices.Count; i++) for (int i = 1; i < sliderPathVertices.Count; i++)
{ {
distance += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]); time += Vector2.Distance(sliderPathVertices[i - 1], sliderPathVertices[i]) / velocity;
if (!Precision.AlmostEquals(vertices[^1].Distance, distance)) if (!Precision.AlmostEquals(vertices[^1].Time, time))
vertices.Add(new JuiceStreamPathVertex(distance, sliderPathVertices[i].X)); Add(time, sliderPathVertices[i].X);
} }
invalidate(); invalidate();
} }
/// <summary>
/// Computes the minimum slider velocity required to convert this path to a <see cref="SliderPath"/>.
/// </summary>
public double ComputeRequiredVelocity()
{
double maximumSlope = 0;
for (int i = 1; i < vertices.Count; i++)
{
double xDifference = Math.Abs((double)vertices[i].X - vertices[i - 1].X);
double timeDifference = vertices[i].Time - vertices[i - 1].Time;
// A short segment won't affect the resulting path much anyways so ignore it to avoid divide-by-zero.
if (Precision.AlmostEquals(timeDifference, 0))
continue;
maximumSlope = Math.Max(maximumSlope, xDifference / timeDifference);
}
return maximumSlope;
}
/// <summary> /// <summary>
/// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>. /// Convert the path of this <see cref="JuiceStreamPath"/> to a <see cref="SliderPath"/> and write the result to <paramref name="sliderPath"/>.
/// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>. /// The resulting slider is "folded" to make it vertically contained in the playfield `(0..<see cref="OSU_PLAYFIELD_HEIGHT"/>)` assuming the slider start position is <paramref name="sliderStartY"/>.
///
/// The velocity of the converted slider is assumed to be <paramref name="velocity"/>.
/// To preserve the path, <paramref name="velocity"/> should be at least the value returned by <see cref="ComputeRequiredVelocity"/>.
/// </summary> /// </summary>
public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY) public void ConvertToSliderPath(SliderPath sliderPath, float sliderStartY, double velocity)
{ {
const float margin = 1; const float margin = 1;
// Note: these two variables and `sliderPath` are modified by the local functions. // Note: these two variables and `sliderPath` are modified by the local functions.
double currentDistance = 0; double currentTime = 0;
Vector2 lastPosition = new Vector2(vertices[0].X, 0); Vector2 lastPosition = new Vector2(vertices[0].X, 0);
sliderPath.ControlPoints.Clear(); sliderPath.ControlPoints.Clear();
@ -237,10 +241,10 @@ namespace osu.Game.Rulesets.Catch.Objects
sliderPath.ControlPoints[^1].Type = PathType.Linear; sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X; float deltaX = vertices[i].X - lastPosition.X;
double length = vertices[i].Distance - currentDistance; double length = (vertices[i].Time - currentTime) * velocity;
// Should satisfy `deltaX^2 + deltaY^2 = length^2`. // Should satisfy `deltaX^2 + deltaY^2 = length^2`.
// By invariants, the expression inside the `sqrt` is (almost) non-negative. // The expression inside the `sqrt` is (almost) non-negative if the slider velocity is large enough.
double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX)); double deltaY = Math.Sqrt(Math.Max(0, length * length - (double)deltaX * deltaX));
// When `deltaY` is small, one segment is always enough. // When `deltaY` is small, one segment is always enough.
@ -280,59 +284,38 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
Vector2 nextPosition = new Vector2(nextX, nextY); Vector2 nextPosition = new Vector2(nextX, nextY);
sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition)); sliderPath.ControlPoints.Add(new PathControlPoint(nextPosition));
currentDistance += Vector2.Distance(lastPosition, nextPosition); currentTime += Vector2.Distance(lastPosition, nextPosition) / velocity;
lastPosition = nextPosition; lastPosition = nextPosition;
} }
} }
/// <summary> /// <summary>
/// Find the index at which a new vertex with <paramref name="distance"/> can be inserted. /// Find the index at which a new vertex with <paramref name="time"/> can be inserted.
/// </summary> /// </summary>
private int vertexIndexAtDistance(double distance) private int vertexIndexAtTime(double time)
{ {
// The position of `(distance, Infinity)` is uniquely determined because infinite positions are not allowed. // The position of `(time, Infinity)` is uniquely determined because infinite positions are not allowed.
int i = vertices.BinarySearch(new JuiceStreamPathVertex(distance, float.PositiveInfinity)); int i = vertices.BinarySearch(new JuiceStreamPathVertex(time, float.PositiveInfinity));
return i < 0 ? ~i : i; return i < 0 ? ~i : i;
} }
/// <summary> /// <summary>
/// Compute the position at the given <paramref name="distance"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtDistance"/>. /// Compute the position at the given <paramref name="time"/>, assuming <paramref name="index"/> is the vertex index returned by <see cref="vertexIndexAtTime"/>.
/// </summary> /// </summary>
private float positionAtDistance(double distance, int index) private float positionAtTime(double time, int index)
{ {
if (index <= 0) if (index <= 0)
return vertices[0].X; return vertices[0].X;
if (index >= vertices.Count) if (index >= vertices.Count)
return vertices[^1].X; return vertices[^1].X;
double length = vertices[index].Distance - vertices[index - 1].Distance; double duration = vertices[index].Time - vertices[index - 1].Time;
if (Precision.AlmostEquals(length, 0)) if (Precision.AlmostEquals(duration, 0))
return vertices[index].X; return vertices[index].X;
float deltaX = vertices[index].X - vertices[index - 1].X; float deltaX = vertices[index].X - vertices[index - 1].X;
return (float)(vertices[index - 1].X + deltaX * ((distance - vertices[index - 1].Distance) / length)); return (float)(vertices[index - 1].X + deltaX * ((time - vertices[index - 1].Time) / duration));
}
/// <summary>
/// Check the two vertices can connected directly while satisfying the slope condition.
/// </summary>
private bool canConnect(JuiceStreamPathVertex vertex1, JuiceStreamPathVertex vertex2, float allowance = 0)
{
double xDistance = Math.Abs((double)vertex2.X - vertex1.X);
float length = (float)Math.Abs(vertex2.Distance - vertex1.Distance);
return xDistance <= length + allowance;
}
/// <summary>
/// Move the position of <paramref name="movableVertex"/> towards the position of <paramref name="fixedVertex"/>
/// until the vertex pair satisfies the condition <see cref="canConnect"/>.
/// </summary>
/// <returns>The resulting position of <paramref name="movableVertex"/>.</returns>
private float clampToConnectablePosition(JuiceStreamPathVertex fixedVertex, JuiceStreamPathVertex movableVertex)
{
float length = (float)Math.Abs(movableVertex.Distance - fixedVertex.Distance);
return Math.Clamp(movableVertex.X, fixedVertex.X - length, fixedVertex.X + length);
} }
private void invalidate() => InvalidationID++; private void invalidate() => InvalidationID++;

View File

@ -12,22 +12,22 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex> public readonly struct JuiceStreamPathVertex : IComparable<JuiceStreamPathVertex>
{ {
public readonly double Distance; public readonly double Time;
public readonly float X; public readonly float X;
public JuiceStreamPathVertex(double distance, float x) public JuiceStreamPathVertex(double time, float x)
{ {
Distance = distance; Time = time;
X = x; X = x;
} }
public int CompareTo(JuiceStreamPathVertex other) public int CompareTo(JuiceStreamPathVertex other)
{ {
int c = Distance.CompareTo(other.Distance); int c = Time.CompareTo(other.Time);
return c != 0 ? c : X.CompareTo(other.X); return c != 0 ? c : X.CompareTo(other.X);
} }
public override string ToString() => $"({Distance}, {X})"; public override string ToString() => $"({Time}, {X})";
} }
} }