diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index da73c2addb..19d3390f56 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -146,5 +146,40 @@ namespace osu.Game.Rulesets.Osu.Utils slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); } + + /// + /// Rotate a slider about its start position by the specified angle. + /// + /// The slider to be rotated. + /// The angle to rotate the slider by. + public static void RotateSlider(Slider slider, float rotation) + { + void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + + var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(); + foreach (var point in controlPoints) + point.Position = rotateVector(point.Position, rotation); + + slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); + } + + /// + /// Rotate a vector by the specified angle. + /// + /// The vector to be rotated. + /// The angle to rotate the vector by. + /// The rotated vector. + private static Vector2 rotateVector(Vector2 vector, float rotation) + { + float angle = (float)Math.Atan2(vector.Y, vector.X) + rotation; + float length = vector.Length; + return new Vector2( + length * (float)Math.Cos(angle), + length * (float)Math.Sin(angle) + ); + } } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index d1bc3b45df..ef1c258a8d 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -40,12 +40,21 @@ namespace osu.Game.Rulesets.Osu.Utils float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - positionInfos.Add(new ObjectPositionInfo(hitObject) + ObjectPositionInfo positionInfo; + positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length }); + if (hitObject is Slider) + { + var endPositionVector = hitObject.EndPosition - hitObject.Position; + float absoluteRotation = (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + positionInfo.Rotation = absoluteRotation - absoluteAngle; + absoluteAngle = absoluteRotation; + } + previousPosition = hitObject.EndPosition; previousAngle = absoluteAngle; } @@ -124,9 +133,16 @@ namespace osu.Game.Rulesets.Osu.Utils if (previous != null) { - Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; - Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + if (previous.HitObject is Slider s) + { + previousAbsoluteAngle = getSliderRotation(s); + } + else + { + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + } } float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; @@ -141,6 +157,16 @@ namespace osu.Game.Rulesets.Osu.Utils posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionModified = lastEndPosition + posRelativeToPrev; + + if (!(current.HitObject is Slider slider)) + return; + + Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); + Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation - current.RotationOriginal); + centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); + + float relativeRotation = (float)Math.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - (float)Math.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); + RotateSlider(slider, relativeRotation); } /// @@ -287,6 +313,27 @@ namespace osu.Game.Rulesets.Osu.Utils ); } + private static Vector2 calculateCentreOfMass(Slider slider) + { + int count = 0; + Vector2 sum = Vector2.Zero; + double pathDistance = slider.Distance; + + for (double i = 0; i < pathDistance; i++) + { + sum += slider.Path.PositionAt(i / pathDistance); + count++; + } + + return sum / count; + } + + private static float getSliderRotation(Slider slider) + { + var endPositionVector = slider.EndPosition - slider.Position; + return (float)Math.Atan2(endPositionVector.Y, endPositionVector.X); + } + public class ObjectPositionInfo { /// @@ -309,6 +356,13 @@ namespace osu.Game.Rulesets.Osu.Utils /// public float DistanceFromPrevious { get; set; } + /// + /// The rotation of the hit object, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to its end position, relative to its jump angle. + /// For hit circles and spinners, this property is ignored. + /// + public float Rotation { get; set; } + /// /// The hit object associated with this . /// @@ -325,6 +379,7 @@ namespace osu.Game.Rulesets.Osu.Utils public Vector2 PositionOriginal { get; } public Vector2 PositionModified { get; set; } public Vector2 EndPositionModified { get; set; } + public float RotationOriginal { get; } public ObjectPositionInfo PositionInfo { get; } public OsuHitObject HitObject => PositionInfo.HitObject; @@ -334,6 +389,15 @@ namespace osu.Game.Rulesets.Osu.Utils PositionInfo = positionInfo; PositionModified = PositionOriginal = HitObject.Position; EndPositionModified = HitObject.EndPosition; + + if (HitObject is Slider slider) + { + RotationOriginal = getSliderRotation(slider); + } + else + { + RotationOriginal = 0; + } } } }