// 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 System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osuTK; namespace osu.Game.Rulesets.Osu.Edit { public class OsuHitObjectComposer : HitObjectComposer { public OsuHitObjectComposer(Ruleset ruleset) : base(ruleset) { } protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuEditRuleset(ruleset, beatmap, mods); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { new HitCircleCompositionTool(), new SliderCompositionTool(), new SpinnerCompositionTool() }; private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; protected override IEnumerable> Toggles => base.Toggles.Concat(new[] { distanceSnapToggle }); private BindableList selectedHitObjects; private Bindable placementObject; [BackgroundDependencyLoader] private void load() { LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject.ValueChanged += _ => updateDistanceSnapGrid(); distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); // we may be entering the screen with a selection already active updateDistanceSnapGrid(); } protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) => new OsuBlueprintContainer(hitObjects); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; private readonly Cached distanceSnapGridCache = new Cached(); private double? lastDistanceSnapGridTime; protected override void Update() { base.Update(); if (!(BlueprintContainer.CurrentTool is SelectTool)) { if (EditorClock.CurrentTime != lastDistanceSnapGridTime) { distanceSnapGridCache.Invalidate(); lastDistanceSnapGridTime = EditorClock.CurrentTime; } if (!distanceSnapGridCache.IsValid) updateDistanceSnapGrid(); } } public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); } private void updateDistanceSnapGrid() { distanceSnapGridContainer.Clear(); distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; if (!distanceSnapToggle.Value) return; switch (BlueprintContainer.CurrentTool) { case SelectTool _: if (!EditorBeatmap.SelectedHitObjects.Any()) return; distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects); break; default: if (!CursorInPlacementArea) return; distanceSnapGrid = createDistanceSnapGrid(Enumerable.Empty()); break; } if (distanceSnapGrid != null) { distanceSnapGridContainer.Add(distanceSnapGrid); distanceSnapGridCache.Validate(); } } private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects) { if (BlueprintContainer.CurrentTool is SpinnerCompositionTool) return null; var objects = selectedHitObjects.ToList(); if (objects.Count == 0) // use accurate time value to give more instantaneous feedback to the user. return createGrid(h => h.StartTime <= EditorClock.CurrentTimeAccurate); double minTime = objects.Min(h => h.StartTime); return createGrid(h => h.StartTime < minTime, objects.Count + 1); } /// /// Creates a grid from the last matching a predicate to a target . /// /// A predicate that matches s where the grid can start from. /// Only the last matching the predicate is used. /// An offset from the selected via at which the grid should stop. /// The from a selected to a target . private OsuDistanceSnapGrid createGrid(Func sourceSelector, int targetOffset = 1) { if (targetOffset < 1) throw new ArgumentOutOfRangeException(nameof(targetOffset)); int sourceIndex = -1; for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++) { if (!sourceSelector(EditorBeatmap.HitObjects[i])) break; sourceIndex = i; } if (sourceIndex == -1) return null; HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; int targetIndex = sourceIndex + targetOffset; HitObject targetObject = null; // Keep advancing the target object while its start time falls before the end time of the source object while (true) { if (targetIndex >= EditorBeatmap.HitObjects.Count) break; if (EditorBeatmap.HitObjects[targetIndex].StartTime >= sourceObject.GetEndTime()) { targetObject = EditorBeatmap.HitObjects[targetIndex]; break; } targetIndex++; } if (sourceObject is Spinner) return null; return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject); } } }