diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionMask.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionMask.cs index 5e68d5cdc9..1ba3e26e65 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionMask.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionMask.cs @@ -1,11 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks; +using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; @@ -15,6 +18,16 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestCaseSliderSelectionMask : HitObjectSelectionMaskTestCase { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SliderSelectionMask), + typeof(SliderCircleSelectionMask), + typeof(SliderBodyPiece), + typeof(SliderCircle), + typeof(ControlPointVisualiser), + typeof(ControlPointPiece) + }; + private readonly DrawableSlider drawableObject; public TestCaseSliderSelectionMask() diff --git a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointPiece.cs new file mode 100644 index 0000000000..7547b4523b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointPiece.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Objects; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components +{ + public class ControlPointPiece : CompositeDrawable + { + private readonly Slider slider; + private readonly int index; + + private readonly Path path; + private readonly CircularContainer marker; + + [Resolved] + private OsuColour colours { get; set; } + + public ControlPointPiece(Slider slider, int index) + { + this.slider = slider; + this.index = index; + + Origin = Anchor.Centre; + Size = new Vector2(10); + + InternalChildren = new Drawable[] + { + path = new SmoothPath + { + BypassAutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + PathWidth = 1 + }, + marker = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + } + }; + } + + protected override void Update() + { + base.Update(); + + Position = slider.StackedPosition + slider.ControlPoints[index]; + + marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow; + + path.ClearVertices(); + + if (index != slider.ControlPoints.Length - 1) + { + path.AddVertex(Vector2.Zero); + path.AddVertex(slider.ControlPoints[index + 1] - slider.ControlPoints[index]); + } + + path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override bool OnDrag(DragEvent e) + { + if (index == 0) + { + // Special handling for the head - only the position of the slider changes + slider.Position += e.Delta; + + // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta + var newControlPoints = slider.ControlPoints.ToArray(); + for (int i = 1; i < newControlPoints.Length; i++) + newControlPoints[i] -= e.Delta; + + slider.ControlPoints = newControlPoints; + slider.Curve.Calculate(true); + } + else + { + var newControlPoints = slider.ControlPoints.ToArray(); + newControlPoints[index] += e.Delta; + + slider.ControlPoints = newControlPoints; + slider.Curve.Calculate(true); + } + + return true; + } + + protected override bool OnDragEnd(DragEndEvent e) => true; + + private bool isSegmentSeparator + { + get + { + bool separator = false; + + if (index < slider.ControlPoints.Length - 1) + separator |= slider.ControlPoints[index + 1] == slider.ControlPoints[index]; + if (index > 0) + separator |= slider.ControlPoints[index - 1] == slider.ControlPoints[index]; + + return separator; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointVisualiser.cs new file mode 100644 index 0000000000..d8031c4f5b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/ControlPointVisualiser.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components +{ + public class ControlPointVisualiser : CompositeDrawable + { + private readonly Slider slider; + + private readonly Container pieces; + + public ControlPointVisualiser(Slider slider) + { + this.slider = slider; + + InternalChild = pieces = new Container { RelativeSizeAxes = Axes.Both }; + + slider.ControlPointsChanged += _ => updateControlPoints(); + updateControlPoints(); + } + + private void updateControlPoints() + { + while (slider.ControlPoints.Length > pieces.Count) + pieces.Add(new ControlPointPiece(slider, pieces.Count)); + while (slider.ControlPoints.Length < pieces.Count) + pieces.Remove(pieces[pieces.Count - 1]); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/SliderCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/SliderCirclePiece.cs index c5ecde5c4c..a8565fafb6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/SliderCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/Components/SliderCirclePiece.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components { this.slider = slider; this.position = position; + + slider.ControlPointsChanged += _ => UpdatePosition(); } protected override void UpdatePosition() diff --git a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderSelectionMask.cs b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderSelectionMask.cs index a411064f68..8478374a5f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderSelectionMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderSelectionMask.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks new SliderBodyPiece(sliderObject), headMask = new SliderCircleSelectionMask(slider.HeadCircle, sliderObject, SliderPosition.Start), new SliderCircleSelectionMask(slider.TailCircle, sliderObject, SliderPosition.End), + new ControlPointVisualiser(sliderObject), }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7f708ec182..4928a85653 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -90,6 +90,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Body.PathWidth = HitObject.Scale * 64; Ball.Scale = new Vector2(HitObject.Scale); }; + + slider.ControlPointsChanged += _ => Body.Refresh(); } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 6d6cba4936..6a836679a2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,7 +16,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { this.slider = slider; - Position = HitObject.Position - slider.Position; + h.PositionChanged += _ => updatePosition(); + slider.ControlPointsChanged += _ => updatePosition(); + + updatePosition(); } protected override void Update() @@ -33,5 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Action OnShake; protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); + + private void updatePosition() => Position = HitObject.Position - slider.Position; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 45c925b87a..cc88a6718b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking { + private readonly Slider slider; + /// /// The judgement text is provided by the . /// @@ -18,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) : base(hitCircle) { + this.slider = slider; + Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; @@ -25,7 +29,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AlwaysPresent = true; - Position = HitObject.Position - slider.Position; + hitCircle.PositionChanged += _ => updatePosition(); + slider.ControlPointsChanged += _ => updatePosition(); + + updatePosition(); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -33,5 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered && timeOffset >= 0) ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } + + private void updatePosition() => Position = HitObject.Position - slider.Position; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index 09d6f9459a..3d02f9a92d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -45,15 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces [BackgroundDependencyLoader] private void load() { - // Generate the entire curve - slider.Curve.GetPathToProgress(CurrentCurve, 0, 1); - SetVertices(CurrentCurve); - - // The body is sized to the full path size to avoid excessive autosize computations - Size = Path.Size; - - snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); - snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); + Refresh(); } public void UpdateProgress(double completionProgress) @@ -80,6 +72,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces setRange(start, end); } + public void Refresh() + { + // Generate the entire curve + slider.Curve.GetPathToProgress(CurrentCurve, 0, 1); + SetVertices(CurrentCurve); + + // The body is sized to the full path size to avoid excessive autosize computations + Size = Path.Size; + + snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); + snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); + + var lastSnakedStart = SnakedStart ?? 0; + var lastSnakedEnd = SnakedEnd ?? 0; + + SnakedStart = null; + SnakedEnd = null; + + setRange(lastSnakedStart, lastSnakedEnd); + } + private void setRange(double p0, double p1) { if (p0 > p1) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index a6f5bdb24e..de7ba8451b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects /// private const float base_scoring_distance = 100; + public event Action ControlPointsChanged; + public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; @@ -54,8 +56,18 @@ namespace osu.Game.Rulesets.Osu.Objects public Vector2[] ControlPoints { - get { return Curve.ControlPoints; } - set { Curve.ControlPoints = value; } + get => Curve.ControlPoints; + set + { + if (Curve.ControlPoints == value) + return; + Curve.ControlPoints = value; + + ControlPointsChanged?.Invoke(value); + + if (TailCircle != null) + TailCircle.Position = EndPosition; + } } public CurveType CurveType diff --git a/osu.Game/Rulesets/Objects/SliderCurve.cs b/osu.Game/Rulesets/Objects/SliderCurve.cs index e3c9c53a2b..575cbf8501 100644 --- a/osu.Game/Rulesets/Objects/SliderCurve.cs +++ b/osu.Game/Rulesets/Objects/SliderCurve.cs @@ -120,10 +120,33 @@ namespace osu.Game.Rulesets.Objects } } - public void Calculate() + private void calculateCumulativeLength() + { + double l = 0; + + cumulativeLength.Clear(); + cumulativeLength.Add(l); + + for (int i = 0; i < calculatedPath.Count - 1; ++i) + { + Vector2 diff = calculatedPath[i + 1] - calculatedPath[i]; + double d = diff.Length; + + l += d; + cumulativeLength.Add(l); + } + + Distance = l; + } + + public void Calculate(bool updateDistance = false) { calculatePath(); - calculateCumulativeLengthAndTrimPath(); + + if (!updateDistance) + calculateCumulativeLengthAndTrimPath(); + else + calculateCumulativeLength(); } private int indexOfDistance(double d)