1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Merge pull request #14805 from bdach/rectangular-snap-grid

Add rectangular snap grid to osu! editor composer
This commit is contained in:
Dean Herbert 2021-09-22 00:12:03 +09:00 committed by GitHub
commit 60c9e9f704
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 407 additions and 8 deletions

View File

@ -0,0 +1,78 @@
// 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.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneOsuEditorGrids : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestGridExclusivity()
{
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(true);
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false);
}
private void rectangularGridActive(bool active)
{
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
AddStep("move cursor to (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
});
if (active)
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
else
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
}
[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Any());
gridSizeIs(4);
nextGridSizeIs(8);
nextGridSizeIs(16);
nextGridSizeIs(32);
nextGridSizeIs(4);
}
private void nextGridSizeIs(int size)
{
AddStep("toggle to next grid size", () => InputManager.Key(Key.G));
gridSizeIs(size);
}
private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single().Spacing == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size);
}
}

View File

@ -42,10 +42,12 @@ namespace osu.Game.Rulesets.Osu.Edit
}; };
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>(); private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{ {
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
}); });
private BindableList<HitObject> selectedHitObjects; private BindableList<HitObject> selectedHitObjects;
@ -63,6 +65,10 @@ namespace osu.Game.Rulesets.Osu.Edit
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
}, },
distanceSnapGridContainer = new Container distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }
@ -73,7 +79,19 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid(); placementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); distanceSnapToggle.ValueChanged += _ =>
{
updateDistanceSnapGrid();
if (distanceSnapToggle.Value == TernaryState.True)
rectangularGridSnapToggle.Value = TernaryState.False;
};
rectangularGridSnapToggle.ValueChanged += _ =>
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
distanceSnapToggle.Value = TernaryState.False;
};
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
@ -91,6 +109,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Cached distanceSnapGridCache = new Cached(); private readonly Cached distanceSnapGridCache = new Cached();
private double? lastDistanceSnapGridTime; private double? lastDistanceSnapGridTime;
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -122,13 +142,19 @@ namespace osu.Game.Rulesets.Osu.Edit
if (positionSnap.ScreenSpacePosition != screenSpacePosition) if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap; return positionSnap;
// will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
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));
}
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); if (rectangularGridSnapToggle.Value == TernaryState.True)
{
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
}
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -0,0 +1,69 @@
// 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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler<GlobalAction>
{
private static readonly int[] grid_sizes = { 4, 8, 16, 32 };
private int currentGridSizeIndex = grid_sizes.Length - 1;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
public OsuRectangularPositionSnapGrid()
: base(OsuPlayfield.BASE_SIZE / 2)
{
}
[BackgroundDependencyLoader]
private void load()
{
var gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
if (gridSizeIndex >= 0)
currentGridSizeIndex = gridSizeIndex;
updateSpacing();
}
private void nextGridSize()
{
currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length;
updateSpacing();
}
private void updateSpacing()
{
int gridSize = grid_sizes[currentGridSizeIndex];
editorBeatmap.BeatmapInfo.GridSize = gridSize;
Spacing = new Vector2(gridSize);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.EditorCycleGridDisplayMode:
nextGridSize();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -0,0 +1,104 @@
// 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.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene
{
private Container content;
protected override Container<Drawable> Content => content;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Gray
},
content = new Container
{
RelativeSizeAxes = Axes.Both
}
});
}
private static readonly object[][] test_cases =
{
new object[] { new Vector2(0, 0), new Vector2(10, 10) },
new object[] { new Vector2(240, 180), new Vector2(10, 15) },
new object[] { new Vector2(160, 120), new Vector2(30, 20) },
new object[] { new Vector2(480, 360), new Vector2(100, 100) },
};
[TestCaseSource(nameof(test_cases))]
public void TestRectangularGrid(Vector2 position, Vector2 spacing)
{
RectangularPositionSnapGrid grid = null;
AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position)
{
RelativeSizeAxes = Axes.Both,
Spacing = spacing
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
private class SnappingCursorContainer : CompositeDrawable
{
public Func<Vector2, Vector2> GetSnapPosition;
private readonly Drawable cursor;
public SnappingCursorContainer()
{
RelativeSizeAxes = Axes.Both;
InternalChild = cursor = new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(50),
Colour = Color4.Red
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updatePosition(GetContainingInputManager().CurrentState.Mouse.Position);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
base.OnMouseMove(e);
updatePosition(e.ScreenSpaceMousePosition);
return true;
}
private void updatePosition(Vector2 screenSpacePosition)
{
cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
}
}
}
}

View File

@ -75,6 +75,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft), new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
}; };
public IEnumerable<KeyBinding> InGameKeyBindings => new[] public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -284,6 +285,9 @@ namespace osu.Game.Input.Bindings
SeekReplayBackward, SeekReplayBackward,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))]
ToggleChatFocus ToggleChatFocus,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))]
EditorCycleGridDisplayMode
} }
} }

View File

@ -164,6 +164,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode"); public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
/// <summary>
/// "Cycle grid display mode"
/// </summary>
public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode");
/// <summary> /// <summary>
/// "Hold for HUD" /// "Hold for HUD"
/// </summary> /// </summary>

View File

@ -0,0 +1,113 @@
// 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.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Layout;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public class RectangularPositionSnapGrid : CompositeDrawable
{
/// <summary>
/// The position of the origin of this <see cref="RectangularPositionSnapGrid"/> in local coordinates.
/// </summary>
public Vector2 StartPosition { get; }
private Vector2 spacing = Vector2.One;
/// <summary>
/// The spacing between grid lines of this <see cref="RectangularPositionSnapGrid"/>.
/// </summary>
public Vector2 Spacing
{
get => spacing;
set
{
if (spacing.X <= 0 || spacing.Y <= 0)
throw new ArgumentException("Grid spacing must be positive.");
spacing = value;
gridCache.Invalidate();
}
}
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
public RectangularPositionSnapGrid(Vector2 startPosition)
{
StartPosition = startPosition;
AddLayout(gridCache);
}
protected override void Update()
{
base.Update();
if (!gridCache.IsValid)
{
ClearInternal();
createContent();
gridCache.Validate();
}
}
private void createContent()
{
var drawSize = DrawSize;
generateGridLines(Direction.Horizontal, StartPosition.Y, 0, -Spacing.Y);
generateGridLines(Direction.Horizontal, StartPosition.Y, drawSize.Y, Spacing.Y);
generateGridLines(Direction.Vertical, StartPosition.X, 0, -Spacing.X);
generateGridLines(Direction.Vertical, StartPosition.X, drawSize.X, Spacing.X);
}
private void generateGridLines(Direction direction, float startPosition, float endPosition, float step)
{
int index = 0;
float currentPosition = startPosition;
while ((endPosition - currentPosition) * Math.Sign(step) > 0)
{
var gridLine = new Box
{
Colour = Colour4.White,
Alpha = index == 0 ? 0.3f : 0.1f,
EdgeSmoothness = new Vector2(0.2f)
};
if (direction == Direction.Horizontal)
{
gridLine.RelativeSizeAxes = Axes.X;
gridLine.Height = 1;
gridLine.Y = currentPosition;
}
else
{
gridLine.RelativeSizeAxes = Axes.Y;
gridLine.Width = 1;
gridLine.X = currentPosition;
}
AddInternal(gridLine);
index += 1;
currentPosition = startPosition + index * step;
}
}
public Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = original - StartPosition;
Vector2 offset = Vector2.Divide(relativeToStart, Spacing);
Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y));
return StartPosition + Vector2.Multiply(roundedOffset, Spacing);
}
}
}