Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to change position, spacing, and rotation of the positional snap grid in the editor #26309

Merged
merged 36 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f3b88c3
Add rotation to snap grid visual
OliBomby Dec 28, 2023
f2edd70
add rotation to snapped position
OliBomby Dec 28, 2023
2193601
fix typo
OliBomby Dec 28, 2023
92c3b14
Added Triangular snap grid
OliBomby Dec 28, 2023
d0c8b28
clean up code duplication
OliBomby Dec 28, 2023
a20c430
fix wrong grid cache being used
OliBomby Dec 28, 2023
b16c232
add basic control by grid tool box
OliBomby Dec 28, 2023
0ce1a48
Add comment
OliBomby Dec 28, 2023
f223487
improve code
OliBomby Dec 28, 2023
351cfbf
Fix snapping going out of bounds
OliBomby Dec 28, 2023
8ef9bdf
clarify comment
OliBomby Dec 28, 2023
040fd5e
Add option to change grid type
OliBomby Dec 29, 2023
8a33105
Make it actually possible to change grid type
OliBomby Dec 29, 2023
847f04e
reduce opacity of middle cardinal lines
OliBomby Dec 29, 2023
d0ca3f2
Add circular grid
OliBomby Dec 29, 2023
f649fa1
Added bindables and binding with BindTo
OliBomby Dec 29, 2023
1c75357
fix compile
OliBomby Dec 30, 2023
9a8c41f
Saving exact grid spacing
OliBomby Dec 30, 2023
493e3a5
use G to change grid type
OliBomby Dec 31, 2023
e47d570
improve UI
OliBomby Dec 31, 2023
904ea2e
move OutlineTriangle code down
OliBomby Dec 31, 2023
33e559f
add integer keyboard step to sliders
OliBomby Dec 31, 2023
20e338b
also hide grid from points button when not hovered
OliBomby Dec 31, 2023
8425c72
fix rectangular and triangular grid tests
OliBomby Dec 31, 2023
31d1799
Create TestSceneCircularPositionSnapGrid.cs
OliBomby Dec 31, 2023
9796fcf
Merge position snap grid tests into single file
OliBomby Dec 31, 2023
c5edf43
fix grid test
OliBomby Dec 31, 2023
594b6fe
Add back the old keybind for cycling grid spacing
OliBomby Dec 31, 2023
39f4a1a
conflict fixes
OliBomby Jan 1, 2024
de14da9
Remove other grid types
OliBomby Jan 1, 2024
460c584
fix code quality
OliBomby Jan 1, 2024
1428cbf
Remove Masking from PositionSnapGrid
OliBomby Feb 1, 2024
9f19ab0
Merge branch 'master' into grids-1
bdach May 24, 2024
4977019
fix nitpick
OliBomby May 24, 2024
4fcb902
Merge branch 'master' into grids-1
bdach Jun 3, 2024
212be6b
Merge branch 'master' into grids-1
peppy Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading