// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components { /// /// A grid which takes user input and returns a quantized ("snapped") position and time. /// public abstract partial class DistanceSnapGrid : CompositeDrawable { /// /// The spacing between each tick of the beat snapping grid. /// protected float DistanceBetweenTicks { get; private set; } protected IBindable DistanceSpacingMultiplier { 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; protected readonly double? LatestEndTime; [Resolved] protected OsuColour Colours { get; private set; } [Resolved] protected IDistanceSnapProvider SnapProvider { get; private set; } [Resolved] protected EditorBeatmap Beatmap { get; private set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } /// /// When enabled, distance snap should only snap to the current time (as per the editor clock). /// This is to emulate stable behaviour. /// protected Bindable LimitedDistanceSnap { get; private set; } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { LimitedDistanceSnap = config.GetBindable(OsuSetting.EditorLimitedDistanceSnap); } private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); 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; LatestEndTime = endTime; StartPosition = startPosition; StartTime = startTime; RelativeSizeAxes = Axes.Both; AddLayout(gridCache); } protected override void LoadComplete() { base.LoadComplete(); beatDivisor.BindValueChanged(_ => updateSpacing()); DistanceSpacingMultiplier = SnapProvider.DistanceSpacingMultiplier.GetBoundCopy(); DistanceSpacingMultiplier.BindValueChanged(_ => updateSpacing(), true); } private void updateSpacing() { float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false); DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; if (LatestEndTime == null) MaxIntervals = int.MaxValue; else MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); 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 Color4 GetColourForIndexFromPlacement(int placementIndex) { var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength); var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); int repeatIndex = placementIndex / beatDivisor.Value; return colour.Opacity(0.5f / (repeatIndex + 1)); } } }