diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs new file mode 100644 index 0000000000..2be0b7e9b2 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Edit; +using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests.Editor +{ + public class TestSceneCatchDistanceSnapGrid : OsuManualInputManagerTestScene + { + private readonly ManualClock manualClock = new ManualClock(); + + [Cached(typeof(Playfield))] + private readonly CatchPlayfield playfield; + + private ScrollingHitObjectContainer hitObjectContainer => playfield.HitObjectContainer; + + private readonly CatchDistanceSnapGrid distanceGrid; + + private readonly FruitOutline fruitOutline; + + private readonly Fruit fruit = new Fruit(); + + public TestSceneCatchDistanceSnapGrid() + { + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 500, + + Children = new Drawable[] + { + new ScrollingTestContainer(ScrollingDirection.Down) + { + RelativeSizeAxes = Axes.Both, + Child = playfield = new CatchPlayfield(new BeatmapDifficulty()) + { + RelativeSizeAxes = Axes.Both, + Clock = new FramedClock(manualClock) + } + }, + distanceGrid = new CatchDistanceSnapGrid(new double[] { 0, -1, 1 }), + fruitOutline = new FruitOutline() + }, + }; + } + + protected override void Update() + { + base.Update(); + + distanceGrid.StartTime = 100; + distanceGrid.StartX = 250; + + Vector2 screenSpacePosition = InputManager.CurrentState.Mouse.Position; + + var result = distanceGrid.GetSnappedPosition(screenSpacePosition); + + if (result != null) + { + fruit.OriginalX = hitObjectContainer.ToLocalSpace(result.ScreenSpacePosition).X; + + if (result.Time != null) + fruit.StartTime = result.Time.Value; + } + + fruitOutline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, fruit); + fruitOutline.UpdateFrom(fruit); + } + + protected override bool OnScroll(ScrollEvent e) + { + manualClock.CurrentTime -= e.ScrollDelta.Y * 50; + return true; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs new file mode 100644 index 0000000000..5808b332b0 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Edit +{ + public class CatchDistanceSnapGrid : CompositeDrawable + { + public double StartTime { get; set; } + + public float StartX { get; set; } + + private readonly double[] velocities; + + private readonly List verticalPaths = new List(); + + private readonly List> verticalLineVertices = new List>(); + + [Resolved] + private Playfield playfield { get; set; } + + private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer; + + public CatchDistanceSnapGrid(double[] velocities) + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.BottomLeft; + + this.velocities = velocities; + + for (int i = 0; i < velocities.Length; i++) + { + verticalPaths.Add(new SmoothPath + { + PathRadius = 2, + Alpha = 0.5f, + }); + + verticalLineVertices.Add(new List { Vector2.Zero, Vector2.Zero }); + } + + AddRangeInternal(verticalPaths); + } + + protected override void Update() + { + base.Update(); + + float startY = hitObjectContainer.PositionAtTime(StartTime); + + for (int i = 0; i < velocities.Length; i++) + { + double velocity = velocities[i]; + + // The line ends at the top of the screen. + double endTime = hitObjectContainer.TimeAtPosition(-hitObjectContainer.DrawHeight, hitObjectContainer.Time.Current); + + float x = (float)((endTime - StartTime) * velocity); + float y = hitObjectContainer.PositionAtTime(endTime, StartTime); + + List lineVertices = verticalLineVertices[i]; + lineVertices[0] = new Vector2(StartX, startY); + lineVertices[1] = lineVertices[0] + new Vector2(x, y); + + var verticalPath = verticalPaths[i]; + verticalPath.Vertices = verticalLineVertices[i]; + verticalPath.OriginPosition = verticalPath.PositionInBoundingBox(Vector2.Zero); + } + } + + [CanBeNull] + public SnapResult GetSnappedPosition(Vector2 screenSpacePosition) + { + double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition); + + return enumerateSnappingCandidates(time) + .OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition)) + .FirstOrDefault(); + } + + private IEnumerable enumerateSnappingCandidates(double time) + { + float y = hitObjectContainer.PositionAtTime(time); + + foreach (double velocity in velocities) + { + float x = (float)(StartX + (time - StartTime) * velocity); + Vector2 screenSpacePosition = hitObjectContainer.ToScreenSpace(new Vector2(x, y + hitObjectContainer.DrawHeight)); + yield return new SnapResult(screenSpacePosition, time); + } + } + + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + } +}