Skip to content

Commit

Permalink
Merge pull request ppy#26309 from OliBomby/grids-1
Browse files Browse the repository at this point in the history
Add ability to change position, spacing, and rotation of the positional snap grid in the editor
  • Loading branch information
peppy authored Jun 5, 2024
2 parents 058d7aa + 212be6b commit 3185987
Show file tree
Hide file tree
Showing 10 changed files with 555 additions and 208 deletions.
55 changes: 40 additions & 15 deletions osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;

Expand All @@ -25,22 +27,22 @@ public void TestGridToggles()
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));

AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false);
gridActive<RectangularPositionSnapGrid>(false);

AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));

AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(true);
gridActive<RectangularPositionSnapGrid>(true);

AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
rectangularGridActive(true);
gridActive<RectangularPositionSnapGrid>(true);

AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false);
gridActive<RectangularPositionSnapGrid>(false);
}

[Test]
Expand Down Expand Up @@ -117,33 +119,56 @@ public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled()
[Test]
public void TestGridSnapMomentaryToggle()
{
rectangularGridActive(false);
gridActive<RectangularPositionSnapGrid>(false);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
rectangularGridActive(true);
gridActive<RectangularPositionSnapGrid>(true);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
rectangularGridActive(false);
gridActive<RectangularPositionSnapGrid>(false);
}

private void rectangularGridActive(bool active)
private void gridActive<T>(bool active) where T : PositionSnapGrid
{
AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
AddStep("move cursor to (1, 1)", () =>
AddStep("move cursor to spacing + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
var composer = Editor.ChildrenOfType<T>().Single();
InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1)));
});

if (active)
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
{
AddAssert("placement blueprint at spacing + (0, 0)", () =>
{
var composer = Editor.ChildrenOfType<T>().Single();
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
uniqueSnappingPosition(composer));
});
}
else
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
{
AddAssert("placement blueprint at spacing + (1, 1)", () =>
{
var composer = Editor.ChildrenOfType<T>().Single();
return Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position,
uniqueSnappingPosition(composer) + new Vector2(1, 1));
});
}
}

private Vector2 uniqueSnappingPosition(PositionSnapGrid grid)
{
return grid switch
{
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
_ => Vector2.Zero
};
}

[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Any());
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridSizeIs(4);

nextGridSizeIs(8);
Expand All @@ -159,7 +184,7 @@ private void nextGridSizeIs(int size)
}

private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<OsuRectangularPositionSnapGrid>().Single().Spacing == new Vector2(size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size);
}
}
171 changes: 171 additions & 0 deletions osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osuTK;

namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;

/// <summary>
/// X position of the grid's origin.
/// </summary>
public BindableFloat StartPositionX { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.X / 2)
{
MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.X,
Precision = 1f
};

/// <summary>
/// Y position of the grid's origin.
/// </summary>
public BindableFloat StartPositionY { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.Y / 2)
{
MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.Y,
Precision = 1f
};

/// <summary>
/// The spacing between grid lines.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(4f)
{
MinValue = 4f,
MaxValue = 128f,
Precision = 1f
};

/// <summary>
/// Rotation of the grid lines in degrees.
/// </summary>
public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f)
{
MinValue = -45f,
MaxValue = 45f,
Precision = 1f
};

/// <summary>
/// Read-only bindable representing the grid's origin.
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
/// </summary>
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>();

/// <summary>
/// Read-only bindable representing the grid's spacing in both the X and Y dimension.
/// Equivalent to <code>new Vector2(Spacing)</code>
/// </summary>
public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>();

private ExpandableSlider<float> startPositionXSlider = null!;
private ExpandableSlider<float> startPositionYSlider = null!;
private ExpandableSlider<float> spacingSlider = null!;
private ExpandableSlider<float> gridLinesRotationSlider = null!;

public OsuGridToolboxGroup()
: base("grid")
{
}

private const float max_automatic_spacing = 64;

[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
startPositionXSlider = new ExpandableSlider<float>
{
Current = StartPositionX,
KeyboardStep = 1,
},
startPositionYSlider = new ExpandableSlider<float>
{
Current = StartPositionY,
KeyboardStep = 1,
},
spacingSlider = new ExpandableSlider<float>
{
Current = Spacing,
KeyboardStep = 1,
},
gridLinesRotationSlider = new ExpandableSlider<float>
{
Current = GridLinesRotation,
KeyboardStep = 1,
},
};

Spacing.Value = editorBeatmap.BeatmapInfo.GridSize;
}

protected override void LoadComplete()
{
base.LoadComplete();

StartPositionX.BindValueChanged(x =>
{
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}";
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
}, true);

StartPositionY.BindValueChanged(y =>
{
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}";
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}";
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
}, true);

Spacing.BindValueChanged(spacing =>
{
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}";
SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
}, true);

GridLinesRotation.BindValueChanged(rotation =>
{
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
}, true);
}

private void nextGridSize()
{
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
}

public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.EditorCycleGridDisplayMode:
nextGridSize();
return true;
}

return false;
}

public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}
38 changes: 31 additions & 7 deletions osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
Expand Down Expand Up @@ -65,6 +66,9 @@ protected override IEnumerable<TernaryButton> CreateTernaryButtons()
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();

[Cached]
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();

[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();

Expand All @@ -80,10 +84,6 @@ private void load()
LayerBelowRuleset.AddRange(new Drawable[]
{
distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both
}
Expand All @@ -99,8 +99,11 @@ private void load()
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();

updatePositionSnapGrid();

RightToolbox.AddRange(new EditorToolboxGroup[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
Expand All @@ -111,6 +114,23 @@ private void load()
);
}

private void updatePositionSnapGrid()
{
if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true);

var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();

rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);

positionSnapGrid = rectangularPositionSnapGrid;

positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid);
}

protected override ComposeBlueprintContainer CreateBlueprintContainer()
=> new OsuBlueprintContainer(this);

Expand Down Expand Up @@ -151,7 +171,7 @@ public override void SelectFromTimestamp(double timestamp, string objectDescript
private readonly Cached distanceSnapGridCache = new Cached();
private double? lastDistanceSnapGridTime;

private RectangularPositionSnapGrid rectangularPositionSnapGrid;
private PositionSnapGrid positionSnapGrid;

protected override void Update()
{
Expand Down Expand Up @@ -209,9 +229,13 @@ public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePositio
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));

// 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.
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);

result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos);
}
}

Expand Down
Loading

0 comments on commit 3185987

Please sign in to comment.