1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-14 05:47:20 +08:00

Merge pull request #26310 from OliBomby/grids-2

Add hexgrid and circular grid to the osu editor
This commit is contained in:
Bartłomiej Dach 2024-07-03 11:40:48 +02:00 committed by GitHub
commit 63136311b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 380 additions and 10 deletions

View File

@ -1,6 +1,7 @@
// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
@ -160,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch
{
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero
};
}

View File

@ -1,17 +1,24 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
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 osu.Game.Screens.Edit.Components.RadioButtons;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Edit
{
@ -20,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IExpandingContainer? expandingContainer { get; set; }
/// <summary>
/// X position of the grid's origin.
/// </summary>
@ -55,8 +65,8 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f)
{
MinValue = -45f,
MaxValue = 45f,
MinValue = -180f,
MaxValue = 180f,
Precision = 1f
};
@ -72,10 +82,13 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
public Bindable<Vector2> SpacingVector { get; } = new Bindable<Vector2>();
public Bindable<PositionSnapGridType> GridType { get; } = new Bindable<PositionSnapGridType>();
private ExpandableSlider<float> startPositionXSlider = null!;
private ExpandableSlider<float> startPositionYSlider = null!;
private ExpandableSlider<float> spacingSlider = null!;
private ExpandableSlider<float> gridLinesRotationSlider = null!;
private EditorRadioButtonCollection gridTypeButtons = null!;
public OsuGridToolboxGroup()
: base("grid")
@ -109,6 +122,31 @@ namespace osu.Game.Rulesets.Osu.Edit
Current = GridLinesRotation,
KeyboardStep = 1,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
gridTypeButtons = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Square",
() => GridType.Value = PositionSnapGridType.Square,
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Triangle",
() => GridType.Value = PositionSnapGridType.Triangle,
() => new OutlineTriangle(true, 20)),
new RadioButton("Circle",
() => GridType.Value = PositionSnapGridType.Circle,
() => new SpriteIcon { Icon = FontAwesome.Regular.Circle }),
}
},
}
},
};
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize;
@ -118,6 +156,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.LoadComplete();
gridTypeButtons.Items.First().Select();
StartPositionX.BindValueChanged(x =>
{
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}";
@ -145,6 +185,32 @@ namespace osu.Game.Rulesets.Osu.Edit
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
}, true);
expandingContainer?.Expanded.BindValueChanged(v =>
{
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
}, true);
GridType.BindValueChanged(v =>
{
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
switch (v.NewValue)
{
case PositionSnapGridType.Square:
GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45;
GridLinesRotation.MinValue = -45;
GridLinesRotation.MaxValue = 45;
break;
case PositionSnapGridType.Triangle:
GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30;
GridLinesRotation.MinValue = -30;
GridLinesRotation.MaxValue = 30;
break;
}
}, true);
}
private void nextGridSize()
@ -167,5 +233,42 @@ namespace osu.Game.Rulesets.Osu.Edit
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
public partial class OutlineTriangle : BufferedContainer
{
public OutlineTriangle(bool outlineOnly, float size)
: base(cachedFrameBuffer: true)
{
Size = new Vector2(size);
InternalChildren = new Drawable[]
{
new EquilateralTriangle { RelativeSizeAxes = Axes.Both },
};
if (outlineOnly)
{
AddInternal(new EquilateralTriangle
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Y,
Y = 0.48f,
Colour = Color4.Black,
Size = new Vector2(size - 7),
Blending = BlendingParameters.None,
});
}
Blending = BlendingParameters.Additive;
}
}
}
public enum PositionSnapGridType
{
Square,
Triangle,
Circle,
}
}

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
updatePositionSnapGrid();
OsuGridToolboxGroup.GridType.BindValueChanged(updatePositionSnapGrid, true);
RightToolbox.AddRange(new Drawable[]
{
@ -115,18 +115,45 @@ namespace osu.Game.Rulesets.Osu.Edit
);
}
private void updatePositionSnapGrid()
private void updatePositionSnapGrid(ValueChangedEvent<PositionSnapGridType> obj)
{
if (positionSnapGrid != null)
LayerBelowRuleset.Remove(positionSnapGrid, true);
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
switch (obj.NewValue)
{
case PositionSnapGridType.Square:
var rectangularPositionSnapGrid = new RectangularPositionSnapGrid();
rectangularPositionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
rectangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.SpacingVector);
rectangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = rectangularPositionSnapGrid;
positionSnapGrid = rectangularPositionSnapGrid;
break;
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
break;
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
positionSnapGrid = circularPositionSnapGrid;
break;
default:
throw new ArgumentOutOfRangeException(nameof(OsuGridToolboxGroup.GridType), OsuGridToolboxGroup.GridType, "Unsupported grid type.");
}
// Bind the start position to the toolbox sliders.
positionSnapGrid.StartPosition.BindTo(OsuGridToolboxGroup.StartPosition);
positionSnapGrid.RelativeSizeAxes = Axes.Both;
LayerBelowRuleset.Add(positionSnapGrid);

View File

@ -70,6 +70,51 @@ namespace osu.Game.Tests.Visual.Editing
}));
}
[TestCaseSource(nameof(test_cases))]
public void TestTriangularGrid(Vector2 position, Vector2 spacing, float rotation)
{
TriangularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new TriangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
grid.GridLineRotation.Value = rotation;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
[TestCaseSource(nameof(test_cases))]
public void TestCircularGrid(Vector2 position, Vector2 spacing, float rotation)
{
CircularPositionSnapGrid grid = null;
AddStep("create grid", () =>
{
Child = grid = new CircularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing.X;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
{
RelativeSizeAxes = Axes.Both,
GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
}));
}
private partial class SnappingCursorContainer : CompositeDrawable
{
public Func<Vector2, Vector2> GetSnapPosition;

View File

@ -119,9 +119,14 @@ namespace osu.Game.Graphics.UserInterface
Expanded.BindValueChanged(v =>
{
label.Text = v.NewValue ? expandedLabelText : contractedLabelText;
slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint);
slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
}, true);
Current.BindDisabledChanged(disabled =>
{
slider.Alpha = Expanded.Value ? disabled ? 0.3f : 1 : 0f;
});
}
}

View File

@ -0,0 +1,98 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class CircularPositionSnapGrid : PositionSnapGrid
{
/// <summary>
/// The spacing between grid lines of this <see cref="CircularPositionSnapGrid"/>.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(1f)
{
MinValue = 0f,
};
public CircularPositionSnapGrid()
{
Spacing.BindValueChanged(_ => GridCache.Invalidate());
}
protected override void CreateContent()
{
var drawSize = DrawSize;
// Calculate the required number of circles based on the maximum distance from the origin to the edge of the grid.
float dx = Math.Max(StartPosition.Value.X, DrawWidth - StartPosition.Value.X);
float dy = Math.Max(StartPosition.Value.Y, DrawHeight - StartPosition.Value.Y);
float maxDistance = new Vector2(dx, dy).Length;
// We need to add one because the first circle starts at zero radius.
int requiredCircles = (int)(maxDistance / Spacing.Value) + 1;
generateCircles(requiredCircles);
GenerateOutline(drawSize);
}
private void generateCircles(int count)
{
// Make lines the same width independent of display resolution.
float lineWidth = 2 * DrawWidth / ScreenSpaceDrawQuad.Width;
List<CircularProgress> generatedCircles = new List<CircularProgress>();
for (int i = 0; i < count; i++)
{
// Add a minimum diameter so the center circle is clearly visible.
float diameter = MathF.Max(lineWidth * 1.5f, i * Spacing.Value * 2);
var gridCircle = new CircularProgress
{
Position = StartPosition.Value,
Origin = Anchor.Centre,
Size = new Vector2(diameter),
InnerRadius = lineWidth * 1f / diameter,
Colour = Colour4.White,
Alpha = 0.2f,
Progress = 1,
};
generatedCircles.Add(gridCircle);
}
if (generatedCircles.Count == 0)
return;
generatedCircles.First().Alpha = 0.8f;
AddInternal(new Container
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = generatedCircles,
});
}
public override Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = original - StartPosition.Value;
if (relativeToStart.LengthSquared < Precision.FLOAT_EPSILON)
return StartPosition.Value;
float length = relativeToStart.Length;
float wantedLength = MathF.Round(length / Spacing.Value) * Spacing.Value;
return StartPosition.Value + Vector2.Multiply(relativeToStart, wantedLength / length);
}
}
}

View File

@ -0,0 +1,89 @@
// 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.Bindables;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class TriangularPositionSnapGrid : LinedPositionSnapGrid
{
/// <summary>
/// The spacing between grid lines of this <see cref="TriangularPositionSnapGrid"/>.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(1f)
{
MinValue = 0f,
};
/// <summary>
/// The rotation in degrees of the grid lines of this <see cref="TriangularPositionSnapGrid"/>.
/// </summary>
public BindableFloat GridLineRotation { get; } = new BindableFloat();
public TriangularPositionSnapGrid()
{
Spacing.BindValueChanged(_ => GridCache.Invalidate());
GridLineRotation.BindValueChanged(_ => GridCache.Invalidate());
}
private static readonly float sqrt3 = float.Sqrt(3);
private static readonly float sqrt3_over2 = sqrt3 / 2;
private static readonly float one_over_sqrt3 = 1 / sqrt3;
protected override void CreateContent()
{
var drawSize = DrawSize;
float stepSpacing = Spacing.Value * sqrt3_over2;
var step1 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 30);
var step2 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 90);
var step3 = GeometryUtils.RotateVector(new Vector2(stepSpacing, 0), -GridLineRotation.Value - 150);
GenerateGridLines(step1, drawSize);
GenerateGridLines(-step1, drawSize);
GenerateGridLines(step2, drawSize);
GenerateGridLines(-step2, drawSize);
GenerateGridLines(step3, drawSize);
GenerateGridLines(-step3, drawSize);
GenerateOutline(drawSize);
}
public override Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value);
Vector2 hex = pixelToHex(relativeToStart);
return StartPosition.Value + GeometryUtils.RotateVector(hexToPixel(hex), -GridLineRotation.Value);
}
private Vector2 pixelToHex(Vector2 pixel)
{
float x = pixel.X / Spacing.Value;
float y = pixel.Y / Spacing.Value;
// Algorithm from Charles Chambers
// with modifications and comments by Chris Cox 2023
// <https://gitlab.com/chriscox/hex-coordinates>
float t = sqrt3 * y + 1; // scaled y, plus phase
float temp1 = MathF.Floor(t + x); // (y+x) diagonal, this calc needs floor
float temp2 = t - x; // (y-x) diagonal, no floor needed
float temp3 = 2 * x + 1; // scaled horizontal, no floor needed, needs +1 to get correct phase
float qf = (temp1 + temp3) / 3.0f; // pseudo x with fraction
float rf = (temp1 + temp2) / 3.0f; // pseudo y with fraction
float q = MathF.Floor(qf); // pseudo x, quantized and thus requires floor
float r = MathF.Floor(rf); // pseudo y, quantized and thus requires floor
return new Vector2(q, r);
}
private Vector2 hexToPixel(Vector2 hex)
{
// Taken from <https://www.redblobgames.com/grids/hexagons/#hex-to-pixel>
// with modifications for the different definition of size.
return new Vector2(Spacing.Value * (hex.X - hex.Y / 2), Spacing.Value * one_over_sqrt3 * 1.5f * hex.Y);
}
}
}