// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; 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 maximum number of distance snapping intervals allowed. /// protected int MaxIntervals { get; private set; } /// /// The position which the grid should start. /// The first beat snapping tick is located at + away from this point. /// protected readonly Vector2 StartPosition; /// /// The snapping time at . /// protected readonly double StartTime; [Resolved] protected OsuColour Colours { get; private set; } [Resolved] protected IPositionSnapProvider SnapProvider { get; private set; } [Resolved] private EditorBeatmap beatmap { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly double? endTime; protected readonly HitObject ReferenceObject; /// /// Creates a new . /// /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) { ReferenceObject = referenceObject; this.endTime = endTime; StartPosition = startPosition; StartTime = startTime; RelativeSizeAxes = Axes.Both; AddLayout(gridCache); } protected override void LoadComplete() { base.LoadComplete(); beatDivisor.BindValueChanged(_ => updateSpacing(), true); } private void updateSpacing() { DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject); if (endTime == 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 = endTime.Value - StartTime + 1; MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing)); } gridCache.Invalidate(); } protected override void Update() { base.Update(); if (!gridCache.IsValid) { ClearInternal(); CreateContent(); gridCache.Validate(); } } /// /// Creates the content which visualises the grid ticks. /// protected abstract void CreateContent(); /// /// 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 from the point of placement. /// The applicable colour. protected ColourInfo GetColourForIndexFromPlacement(int placementIndex) { var timingPoint = beatmap.ControlPointInfo.TimingPointAt(StartTime); var beatLength = timingPoint.BeatLength / beatDivisor.Value; var beatIndex = (int)Math.Round((StartTime - timingPoint.Time) / beatLength); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); int repeatIndex = placementIndex / beatDivisor.Value; return ColourInfo.SingleColour(colour).MultiplyAlpha(0.5f / (repeatIndex + 1)); } } }