// 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.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Edit { /// /// Represents a for rulesets with the concept of distances between objects. /// /// The base type of supported objects. [Cached(typeof(IDistanceSnapProvider))] public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IKeyBindingHandler where TObject : HitObject { protected Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) { MinValue = 0.1, MaxValue = 6.0, Precision = 0.01, }; IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; } private ExpandableSlider> distanceSpacingSlider; private bool distanceSpacingScrollActive; [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } protected DistancedHitObjectComposer(Ruleset ruleset) : base(ruleset) { } [BackgroundDependencyLoader] private void load() { AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer { Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Child = new EditorToolboxGroup("snapping") { Child = distanceSpacingSlider = new ExpandableSlider> { Current = { BindTarget = DistanceSpacingMultiplier }, KeyboardStep = 0.1f, } } }); } protected override void LoadComplete() { base.LoadComplete(); if (!DistanceSpacingMultiplier.Disabled) { DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(v => { distanceSpacingSlider.ContractedLabelText = $"D. S. ({v.NewValue:0.##x})"; distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})"; if (v.NewValue != v.OldValue) onScreenDisplay?.Display(new DistanceSpacingToast(v.NewValue.ToLocalisableString(@"0.##x"))); EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue; }, true); } } public bool OnPressed(KeyBindingPressEvent e) { if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) { distanceSpacingScrollActive = true; return true; } return false; } public void OnReleased(KeyBindingReleaseEvent e) { if (!DistanceSpacingMultiplier.Disabled && e.Action == GlobalAction.EditorDistanceSpacing) distanceSpacingScrollActive = false; } protected override bool OnScroll(ScrollEvent e) { if (distanceSpacingScrollActive) { DistanceSpacingMultiplier.Value += e.ScrollDelta.Y * (e.IsPrecise ? 0.01f : 0.1f); return true; } return base.OnScroll(e); } public virtual float GetBeatSnapDistanceAt(HitObject referenceObject) { return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor); } public virtual float DurationToDistance(HitObject referenceObject, double duration) { double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); } public virtual double DistanceToDuration(HitObject referenceObject, float distance) { double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; } public virtual double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; public virtual float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) { double startTime = referenceObject.StartTime; double actualDuration = startTime + DistanceToDuration(referenceObject, distance); double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedEndTime > actualDuration + 1) snappedEndTime -= beatLength; return DurationToDistance(referenceObject, snappedEndTime - startTime); } protected class ExpandingToolboxContainer : ExpandingContainer { protected override double HoverExpansionDelay => 250; public ExpandingToolboxContainer() : base(130, 250) { RelativeSizeAxes = Axes.Y; Padding = new MarginPadding { Left = 10 }; FillFlow.Spacing = new Vector2(10); } } private class DistanceSpacingToast : Toast { public DistanceSpacingToast(LocalisableString value) : base("Distance Spacing", value, string.Empty) { } } } }