1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-12 18:13:16 +08:00

Remove dependence of blueprint containers on IPositionSnapProvider

This commit is contained in:
Bartłomiej Dach 2025-01-21 12:16:36 +01:00
parent 2d46da1520
commit 15b6e28ebe
No known key found for this signature in database
17 changed files with 263 additions and 196 deletions

View File

@ -1,16 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public partial class CatchBlueprintContainer : ComposeBlueprintContainer public partial class CatchBlueprintContainer : ComposeBlueprintContainer
{ {
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
public CatchBlueprintContainer(CatchHitObjectComposer composer) public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer) : base(composer)
{ {
@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
} }
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction> public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
{ {
private const float distance_snap_radius = 50; public const float DISTANCE_SNAP_RADIUS = 50;
private CatchDistanceSnapGrid distanceSnapGrid = null!; private CatchDistanceSnapGrid distanceSnapGrid = null!;
@ -135,22 +135,12 @@ namespace osu.Game.Rulesets.Catch.Edit
DistanceSnapProvider.HandleToggleViaKey(key); DistanceSnapProvider.HandleToggleViaKey(key);
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
{ {
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
return snapResult;
result.ScreenSpacePosition.X = screenSpacePosition.X; return null;
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{
result = snapResult;
}
}
return result;
} }
private PalpableCatchHitObject? getLastSnappableHitObject(double time) private PalpableCatchHitObject? getLastSnappableHitObject(double time)

View File

@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
set => InternalChild = value; set => InternalChild = value;
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new NotImplementedException();
}
} }
} }

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private EditorBeatmap? editorBeatmap { get; set; } private EditorBeatmap? editorBeatmap { get; set; }
[Resolved] [Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; } private ManiaHitObjectComposer? positionSnapProvider { get; set; }
private EditBodyPiece body = null!; private EditBodyPiece body = null!;
private EditHoldNoteEndPiece head = null!; private EditHoldNoteEndPiece head = null!;

View File

@ -1,17 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
{ {
public ManiaBlueprintContainer(HitObjectComposer composer) public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.FindSnappedPositionAndTime(movePosition);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> SplitControlPointsRequested; public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPositionSnapProvider positionSnapProvider { get; set; } private OsuHitObjectComposer positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; } private IDistanceSnapProvider distanceSnapProvider { get; set; }
@ -433,7 +433,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition); SnapResult result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition)
?? positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition)
?? positionSnapProvider?.TrySnapToPositionGrid(newHeadPosition);
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
@ -453,7 +455,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
else else
{ {
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;

View File

@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@ -8,12 +11,15 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuBlueprintContainer : ComposeBlueprintContainer public partial class OsuBlueprintContainer : ComposeBlueprintContainer
{ {
public OsuBlueprintContainer(HitObjectComposer composer) public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
public OsuBlueprintContainer(OsuHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
@ -36,5 +42,64 @@ namespace osu.Game.Rulesets.Osu.Edit
return base.CreateHitObjectBlueprintFor(hitObject); return base.CreateHitObjectBlueprintFor(hitObject);
} }
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
for (int i = 0; i < blueprints.Count; i++)
{
if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions))
return true;
}
// if no positional snapping could be performed, try unrestricted snapping from the earliest
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.TrySnapToDistanceGrid(movePosition) ?? Composer.TrySnapToPositionGrid(movePosition) ?? new SnapResult(movePosition, null);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<HitObject> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = Composer.TrySnapToNearbyObjects(testPosition);
if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and apply any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(blueprint, delta)))
{
ApplySnapResultTime(positionalResult, blueprint.Item.StartTime);
return true;
}
}
return false;
}
} }
} }

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
@ -222,10 +223,15 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) [CanBeNull]
{ public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition)
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{ {
if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return null;
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return snapResult;
// In the case of snapping to nearby objects, a time value is not provided. // In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
@ -234,44 +240,38 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied. // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same. // the time value if the proposed positions are roughly the same.
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition)); (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)) if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
snapResult.Time = distanceSnappedTime; snapResult.Time = distanceSnappedTime;
}
return snapResult; return snapResult;
} }
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); [CanBeNull]
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition)
{
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return null;
if (snapType.HasFlag(SnapType.RelativeGrids)) var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
{
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
result.Time = time;
}
} }
if (snapType.HasFlag(SnapType.GlobalGrids)) [CanBeNull]
public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition)
{ {
if (rectangularGridSnapToggle.Value == TernaryState.True) if (rectangularGridSnapToggle.Value != TernaryState.True)
{ return null;
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition));
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. // We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
} return new SnapResult(positionSnapGrid.ToScreenSpace(pos), null, playfield);
}
return result;
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -1,16 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
public partial class TaikoBlueprintContainer : ComposeBlueprintContainer public partial class TaikoBlueprintContainer : ComposeBlueprintContainer
{ {
public TaikoBlueprintContainer(HitObjectComposer composer) public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer;
public TaikoBlueprintContainer(TaikoHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject); new TaikoSelectionBlueprint(hitObject);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.FindSnappedPositionAndTime(movePosition);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

View File

@ -111,6 +111,11 @@ namespace osu.Game.Overlays.SkinEditor
SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray());
} }
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<ISerialisableDrawable> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
throw new System.NotImplementedException();
}
/// <summary> /// <summary>
/// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints).
/// </summary> /// </summary>

View File

@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary> /// </summary>
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this); protected abstract ComposeBlueprintContainer CreateBlueprintContainer();
protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector(); protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector();
@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit
/// <returns>The most relevant <see cref="Playfield"/>.</returns> /// <returns>The most relevant <see cref="Playfield"/>.</returns>
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null;
if (snapType.HasFlag(SnapType.GlobalGrids))
{
if (playfield is ScrollingPlayfield scrollingPlayfield)
{
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
// apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
// convert back to screen space
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
}
}
return new SnapResult(screenSpacePosition, targetTime, playfield);
}
#endregion #endregion
} }
@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />. /// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
/// </summary> /// </summary>
[Cached] [Cached]
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider public abstract partial class HitObjectComposer : CompositeDrawable
{ {
public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60; public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60;
public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120; public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120;
@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit
/// <param name="timestamp">The time instant to seek to, in milliseconds.</param> /// <param name="timestamp">The time instant to seek to, in milliseconds.</param>
/// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param> /// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param>
public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { }
#region IPositionSnapProvider
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
#endregion
} }
} }

View File

@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit
} }
} }
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{
var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield;
if (scrollingPlayfield == null)
return new SnapResult(screenSpacePosition, null);
double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
// apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
// convert back to screen space
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield);
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>(); private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
[Resolved(canBeNull: true)]
private IPositionSnapProvider snapProvider { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private IEditorChangeHandler changeHandler { get; set; }
@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected void RemoveBlueprintFor(T item) protected void RemoveBlueprintFor(T item)
{ {
if (!blueprintMap.Remove(item, out var blueprint)) if (!blueprintMap.Remove(item, out var blueprintToRemove))
return; return;
blueprint.Deselect(); blueprintToRemove.Deselect();
blueprint.Selected -= OnBlueprintSelected; blueprintToRemove.Selected -= OnBlueprintSelected;
blueprint.Deselected -= OnBlueprintDeselected; blueprintToRemove.Deselected -= OnBlueprintDeselected;
SelectionBlueprints.Remove(blueprint, true); SelectionBlueprints.Remove(blueprintToRemove, true);
if (movementBlueprints?.Contains(blueprint) == true) if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true)
finishSelectionMovement(); finishSelectionMovement();
OnBlueprintRemoved(blueprint.Item); OnBlueprintRemoved(blueprintToRemove.Item);
} }
/// <summary> /// <summary>
@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2[][] movementBlueprintsOriginalPositions; private (SelectionBlueprint<T> blueprint, Vector2[] originalSnapPositions)[] movementBlueprints;
private SelectionBlueprint<T>[] movementBlueprints;
/// <summary> /// <summary>
/// Whether a blueprint is currently being dragged. /// Whether a blueprint is currently being dragged.
@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false; return false;
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray();
movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
return true; return true;
} }
@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
Debug.Assert(movementBlueprintsOriginalPositions != null); return TryMoveBlueprints(e, movementBlueprints);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
if (snapProvider != null)
{
for (int i = 0; i < movementBlueprints.Length; i++)
{
if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
return true;
}
} }
// if no positional snapping could be performed, try unrestricted snapping from the earliest protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<T> blueprint, Vector2[] originalSnapPositions)> blueprints);
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
// Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
if (result == null)
{
return SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint));
}
return ApplySnapResult(movementBlueprints, result);
}
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<T> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta)))
return true;
}
return false;
}
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
/// <summary> /// <summary>
/// Finishes the current movement of selected blueprints. /// Finishes the current movement of selected blueprints.
@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
movementBlueprintsOriginalPositions = null;
movementBlueprints = null; movementBlueprints = null;
return true; return true;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// A blueprint container generally displayed as an overlay to a ruleset's playfield. /// A blueprint container generally displayed as an overlay to a ruleset's playfield.
/// </summary> /// </summary>
public partial class ComposeBlueprintContainer : EditorBlueprintContainer public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer
{ {
private readonly Container<PlacementBlueprint> placementBlueprintContainer; private readonly Container<PlacementBlueprint> placementBlueprintContainer;
@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks> /// </remarks>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
public ComposeBlueprintContainer(HitObjectComposer composer) protected ComposeBlueprintContainer(HitObjectComposer composer)
: base(composer) : base(composer)
{ {
placementBlueprintContainer = new Container<PlacementBlueprint> placementBlueprintContainer = new Container<PlacementBlueprint>
@ -340,7 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementTimeAndPosition() private void updatePlacementTimeAndPosition()
{ {
var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); SnapResult snapResult = new SnapResult(InputManager.CurrentState.Mouse.Position, null); // Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); TODO
// if no time was found from positional snapping, we should still quantize to the beat. // if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);

View File

@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
public partial class EditorBlueprintContainer : BlueprintContainer<HitObject> public abstract partial class EditorBlueprintContainer : BlueprintContainer<HitObject>
{ {
[Resolved] [Resolved]
protected EditorClock EditorClock { get; private set; } protected EditorClock EditorClock { get; private set; }
@ -73,15 +73,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints) protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime); => blueprints.OrderBy(b => b.Item.StartTime);
protected override bool ApplySnapResult(SelectionBlueprint<HitObject>[] blueprints, SnapResult result) protected void ApplySnapResultTime(SnapResult result, double referenceTime)
{ {
if (!base.ApplySnapResult(blueprints, result)) if (!result.Time.HasValue)
return false; return;
if (result.Time.HasValue)
{
// Apply the start time at the newly snapped-to position // Apply the start time at the newly snapped-to position
double offset = result.Time.Value - blueprints.First().Item.StartTime; double offset = result.Time.Value - referenceTime;
if (offset != 0) if (offset != 0)
{ {
@ -93,9 +91,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
return true;
}
protected override void AddBlueprintFor(HitObject item) protected override void AddBlueprintFor(HitObject item)
{ {
if (item is IBarLine) if (item is IBarLine)

View File

@ -22,7 +22,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
[Cached] [Cached]
public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider public partial class Timeline : ZoomableScrollContainer
{ {
private const float timeline_height = 80; private const float timeline_height = 80;
@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return (float)(time / editorClock.TrackLength * Content.DrawWidth); return (float)(time / editorClock.TrackLength * Content.DrawWidth);
} }
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));

View File

@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return base.OnDragStart(e); return base.OnDragStart(e);
} }
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
private float dragTimeAccumulated; private float dragTimeAccumulated;
protected override void Update() protected override void Update()