// Copyright (c) ppy Pty Ltd . 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; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { /// /// A grid which takes user input and returns a quantized ("snapped") position and time. /// public abstract class DistanceSnapGrid : CompositeDrawable { /// /// The spacing between each tick of the beat snapping grid. /// protected float DistanceSpacing { get; private set; } /// /// The snapping time at . /// protected double StartTime { get; private set; } /// /// The maximum number of distance snapping intervals allowed. /// protected int MaxIntervals { get; private set; } /// /// The position which the grid is centred on. /// The first beat snapping tick is located at + in the desired direction. /// protected readonly Vector2 CentrePosition; [Resolved] protected OsuColour Colours { get; private set; } [Resolved] protected IDistanceSnapProvider SnapProvider { get; private set; } [Resolved] private IEditorBeatmap beatmap { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } private readonly Cached gridCache = new Cached(); private readonly HitObject hitObject; private readonly HitObject nextHitObject; protected DistanceSnapGrid(HitObject hitObject, [CanBeNull] HitObject nextHitObject, Vector2 centrePosition) { this.hitObject = hitObject; this.nextHitObject = nextHitObject; CentrePosition = centrePosition; RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load() { StartTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime; } protected override void LoadComplete() { base.LoadComplete(); beatDivisor.BindValueChanged(_ => updateSpacing(), true); } 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(); } public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0) gridCache.Invalidate(); return base.Invalidate(invalidation, source, shallPropagate); } protected override void Update() { base.Update(); if (!gridCache.IsValid) { ClearInternal(); CreateContent(CentrePosition); gridCache.Validate(); } } /// /// Creates the content which visualises the grid ticks. /// protected abstract void CreateContent(Vector2 centrePosition); /// /// Snaps a position to this grid. /// /// The original position in coordinate space local to this . /// A tuple containing the snapped position in coordinate space local to this and the respective time value. public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position); /// /// Retrieves the applicable colour for a beat index. /// /// The 0-based beat index. /// The applicable colour. protected ColourInfo GetColourForBeatIndex(int index) { int beat = (index + 1) % beatDivisor.Value; ColourInfo colour = Colours.Gray5; for (int i = 0; i < BindableBeatDivisor.VALID_DIVISORS.Length; i++) { int divisor = BindableBeatDivisor.VALID_DIVISORS[i]; if ((beat * divisor) % beatDivisor.Value == 0) { colour = BindableBeatDivisor.GetColourFor(divisor, Colours); break; } } int repeatIndex = index / beatDivisor.Value; return colour.MultiplyAlpha(0.5f / (repeatIndex + 1)); } } }