Merge pull request #26309 from OliBomby/grids-1

Add ability to change position, spacing, and rotation of the positional snap grid in the editor
This commit is contained in:
Dean Herbert 2024-06-05 23:36:45 +09:00 committed by GitHub
commit 318598730b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 559 additions and 212 deletions

View File

@ -8,7 +8,9 @@ using osu.Framework.Utils;
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;
@ -25,22 +27,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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]
@ -117,33 +119,56 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[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);
@ -159,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
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);
}
}

View File

@ -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)
{
}
}
}

View File

@ -24,6 +24,7 @@ using osu.Game.Rulesets.Edit.Tools;
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;
@ -65,6 +66,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
@ -80,10 +84,6 @@ namespace osu.Game.Rulesets.Osu.Edit
LayerBelowRuleset.AddRange(new Drawable[]
{
distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both
},
rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both
}
@ -99,8 +99,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
updatePositionSnapGrid();
RightToolbox.AddRange(new EditorToolboxGroup[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
@ -111,6 +114,23 @@ namespace osu.Game.Rulesets.Osu.Edit
);
}
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);
@ -151,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Cached distanceSnapGridCache = new Cached();
private double? lastDistanceSnapGridTime;
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
private PositionSnapGrid positionSnapGrid;
protected override void Update()
{
@ -209,9 +229,13 @@ namespace osu.Game.Rulesets.Osu.Edit
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
// 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 = positionSnapGrid.ToScreenSpace(pos);
}
}

View File

@ -1,69 +0,0 @@
// 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 partial 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; } = null!;
public OsuRectangularPositionSnapGrid()
: base(OsuPlayfield.BASE_SIZE / 2)
{
}
[BackgroundDependencyLoader]
private void load()
{
int 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

@ -16,7 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene
public partial class TestScenePositionSnapGrid : OsuManualInputManagerTestScene
{
private Container content;
protected override Container<Drawable> Content => content;
@ -33,28 +33,34 @@ namespace osu.Game.Tests.Visual.Editing
},
content = new Container
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
}
});
}
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) },
new object[] { new Vector2(0, 0), new Vector2(10, 10), 0f },
new object[] { new Vector2(240, 180), new Vector2(10, 15), 10f },
new object[] { new Vector2(160, 120), new Vector2(30, 20), -10f },
new object[] { new Vector2(480, 360), new Vector2(100, 100), 0f },
};
[TestCaseSource(nameof(test_cases))]
public void TestRectangularGrid(Vector2 position, Vector2 spacing)
public void TestRectangularGrid(Vector2 position, Vector2 spacing, float rotation)
{
RectangularPositionSnapGrid grid = null;
AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position)
AddStep("create grid", () =>
{
RelativeSizeAxes = Axes.Both,
Spacing = spacing
Child = grid = new RectangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both,
};
grid.StartPosition.Value = position;
grid.Spacing.Value = spacing;
grid.GridLineRotation.Value = rotation;
});
AddStep("add snapping cursor", () => Add(new SnappingCursorContainer

View File

@ -51,15 +51,16 @@ namespace osu.Game.Tournament.Screens.Editors
AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches"));
ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero)
ScrollContent.Add(grid = new RectangularPositionSnapGrid
{
Spacing = new Vector2(GRID_SPACING),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BypassAutoSizeAxes = Axes.Both,
Depth = float.MaxValue
});
grid.Spacing.Value = new Vector2(GRID_SPACING);
LadderInfo.Matches.CollectionChanged += (_, _) => updateMessage();
updateMessage();
}

View File

@ -0,0 +1,166 @@
// 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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public abstract partial class LinedPositionSnapGrid : PositionSnapGrid
{
protected void GenerateGridLines(Vector2 step, Vector2 drawSize)
{
if (Precision.AlmostEquals(step, Vector2.Zero))
return;
int index = 0;
// Make lines the same width independent of display resolution.
float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width;
float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X));
List<Box> generatedLines = new List<Box>();
while (true)
{
Vector2 currentPosition = StartPosition.Value + index * step;
index++;
if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2))
{
if (!isMovingTowardsBox(currentPosition, step, drawSize))
break;
continue;
}
var gridLine = new Box
{
Colour = Colour4.White,
Alpha = 0.1f,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.None,
Width = lineWidth,
Height = Vector2.Distance(p1, p2),
Position = (p1 + p2) / 2,
Rotation = rotation,
};
generatedLines.Add(gridLine);
}
if (generatedLines.Count == 0)
return;
generatedLines.First().Alpha = 0.2f;
AddRangeInternal(generatedLines);
}
private bool isMovingTowardsBox(Vector2 currentPosition, Vector2 step, Vector2 box)
{
return (currentPosition + step).LengthSquared < currentPosition.LengthSquared ||
(currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared;
}
/// <summary>
/// Determines if the line starting at <paramref name="lineStart"/> and going in the direction of <paramref name="lineDir"/>
/// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does.
/// </summary>
/// <param name="lineStart">The start point of the line.</param>
/// <param name="lineDir">The direction of the line.</param>
/// <param name="box">The width and height of the box.</param>
/// <param name="p1">The first intersection point.</param>
/// <param name="p2">The second intersection point.</param>
/// <returns>Whether the line definitely intersects the box.</returns>
private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2)
{
p1 = Vector2.Zero;
p2 = Vector2.Zero;
if (Precision.AlmostEquals(lineDir.X, 0))
{
// If the line is vertical, we only need to check if the X coordinate of the line is within the box.
if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X))
return false;
p1 = new Vector2(lineStart.X, 0);
p2 = new Vector2(lineStart.X, box.Y);
return true;
}
if (Precision.AlmostEquals(lineDir.Y, 0))
{
// If the line is horizontal, we only need to check if the Y coordinate of the line is within the box.
if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y))
return false;
p1 = new Vector2(0, lineStart.Y);
p2 = new Vector2(box.X, lineStart.Y);
return true;
}
float m = lineDir.Y / lineDir.X;
float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero.
float b = lineStart.Y - m * lineStart.X;
// Calculate intersection points with the sides of the box.
var p = new List<Vector2>(4);
if (0 <= b && b <= box.Y)
p.Add(new Vector2(0, b));
if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X)
p.Add(new Vector2((box.Y - b) * mInv, box.Y));
if (0 <= m * box.X + b && m * box.X + b <= box.Y)
p.Add(new Vector2(box.X, m * box.X + b));
if (0 <= -b * mInv && -b * mInv <= box.X)
p.Add(new Vector2(-b * mInv, 0));
switch (p.Count)
{
case 4:
// If there are 4 intersection points, the line is a diagonal of the box.
if (m > 0)
{
p1 = Vector2.Zero;
p2 = box;
}
else
{
p1 = new Vector2(0, box.Y);
p2 = new Vector2(box.X, 0);
}
break;
case 3:
// If there are 3 intersection points, the line goes through a corner of the box.
if (p[0] == p[1])
{
p1 = p[0];
p2 = p[2];
}
else
{
p1 = p[0];
p2 = p[1];
}
break;
case 2:
p1 = p[0];
p2 = p[1];
break;
}
return !Precision.AlmostEquals(p1, p2);
}
}
}

View File

@ -0,0 +1,93 @@
// 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.Bindables;
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 abstract partial class PositionSnapGrid : CompositeDrawable
{
/// <summary>
/// The position of the origin of this <see cref="PositionSnapGrid"/> in local coordinates.
/// </summary>
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>(Vector2.Zero);
protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
protected PositionSnapGrid()
{
StartPosition.BindValueChanged(_ => GridCache.Invalidate());
AddLayout(GridCache);
}
protected override void Update()
{
base.Update();
if (GridCache.IsValid) return;
ClearInternal();
if (DrawWidth > 0 && DrawHeight > 0)
CreateContent();
GridCache.Validate();
}
protected abstract void CreateContent();
protected void GenerateOutline(Vector2 drawSize)
{
// Make lines the same width independent of display resolution.
float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width;
AddRangeInternal(new[]
{
new Box
{
Colour = Colour4.White,
Alpha = 0.3f,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = lineWidth,
Y = 0,
},
new Box
{
Colour = Colour4.White,
Alpha = 0.3f,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Height = lineWidth,
Y = drawSize.Y,
},
new Box
{
Colour = Colour4.White,
Alpha = 0.3f,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Width = lineWidth,
X = 0,
},
new Box
{
Colour = Colour4.White,
Alpha = 0.3f,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Width = lineWidth,
X = drawSize.X,
},
});
}
public abstract Vector2 GetSnappedPosition(Vector2 original);
}
}

View File

@ -2,132 +2,51 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Layout;
using osu.Framework.Utils;
using osu.Framework.Bindables;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class RectangularPositionSnapGrid : CompositeDrawable
public partial class RectangularPositionSnapGrid : LinedPositionSnapGrid
{
/// <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.");
public Bindable<Vector2> Spacing { get; } = new Bindable<Vector2>(Vector2.One);
spacing = value;
gridCache.Invalidate();
}
/// <summary>
/// The rotation in degrees of the grid lines of this <see cref="RectangularPositionSnapGrid"/>.
/// </summary>
public BindableFloat GridLineRotation { get; } = new BindableFloat();
public RectangularPositionSnapGrid()
{
Spacing.BindValueChanged(_ => GridCache.Invalidate());
GridLineRotation.BindValueChanged(_ => 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();
if (DrawWidth > 0 && DrawHeight > 0)
createContent();
gridCache.Validate();
}
}
private void createContent()
protected override void CreateContent()
{
var drawSize = DrawSize;
var rot = Quaternion.FromAxisAngle(Vector3.UnitZ, MathHelper.DegreesToRadians(GridLineRotation.Value));
generateGridLines(Direction.Horizontal, StartPosition.Y, 0, -Spacing.Y);
generateGridLines(Direction.Horizontal, StartPosition.Y, drawSize.Y, Spacing.Y);
GenerateGridLines(Vector2.Transform(new Vector2(0, -Spacing.Value.Y), rot), drawSize);
GenerateGridLines(Vector2.Transform(new Vector2(0, Spacing.Value.Y), rot), drawSize);
generateGridLines(Direction.Vertical, StartPosition.X, 0, -Spacing.X);
generateGridLines(Direction.Vertical, StartPosition.X, drawSize.X, Spacing.X);
GenerateGridLines(Vector2.Transform(new Vector2(-Spacing.Value.X, 0), rot), drawSize);
GenerateGridLines(Vector2.Transform(new Vector2(Spacing.Value.X, 0), rot), drawSize);
GenerateOutline(drawSize);
}
private void generateGridLines(Direction direction, float startPosition, float endPosition, float step)
public override Vector2 GetSnappedPosition(Vector2 original)
{
int index = 0;
float currentPosition = startPosition;
// Make lines the same width independent of display resolution.
float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width;
List<Box> generatedLines = new List<Box>();
while (Precision.AlmostBigger((endPosition - currentPosition) * Math.Sign(step), 0))
{
var gridLine = new Box
{
Colour = Colour4.White,
Alpha = 0.1f,
};
if (direction == Direction.Horizontal)
{
gridLine.Origin = Anchor.CentreLeft;
gridLine.RelativeSizeAxes = Axes.X;
gridLine.Height = lineWidth;
gridLine.Y = currentPosition;
}
else
{
gridLine.Origin = Anchor.TopCentre;
gridLine.RelativeSizeAxes = Axes.Y;
gridLine.Width = lineWidth;
gridLine.X = currentPosition;
}
generatedLines.Add(gridLine);
index += 1;
currentPosition = startPosition + index * step;
}
if (generatedLines.Count == 0)
return;
generatedLines.First().Alpha = 0.3f;
generatedLines.Last().Alpha = 0.3f;
AddRangeInternal(generatedLines);
}
public Vector2 GetSnappedPosition(Vector2 original)
{
Vector2 relativeToStart = original - StartPosition;
Vector2 offset = Vector2.Divide(relativeToStart, Spacing);
Vector2 relativeToStart = GeometryUtils.RotateVector(original - StartPosition.Value, GridLineRotation.Value);
Vector2 offset = Vector2.Divide(relativeToStart, Spacing.Value);
Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y));
return StartPosition + Vector2.Multiply(roundedOffset, Spacing);
return StartPosition.Value + GeometryUtils.RotateVector(Vector2.Multiply(roundedOffset, Spacing.Value), -GridLineRotation.Value);
}
}
}

View File

@ -26,9 +26,7 @@ namespace osu.Game.Utils
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(float.DegreesToRadians(angle)) + point.Y * MathF.Sin(float.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(float.DegreesToRadians(angle)) + point.Y * MathF.Cos(float.DegreesToRadians(angle));
Vector2 ret = RotateVector(point, angle);
ret.X += origin.X;
ret.Y += origin.Y;
@ -36,6 +34,19 @@ namespace osu.Game.Utils
return ret;
}
/// <summary>
/// Rotate a vector around the origin.
/// </summary>
/// <param name="vector">The vector.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
public static Vector2 RotateVector(Vector2 vector, float angle)
{
return new Vector2(
vector.X * MathF.Cos(float.DegreesToRadians(angle)) + vector.Y * MathF.Sin(float.DegreesToRadians(angle)),
vector.X * -MathF.Sin(float.DegreesToRadians(angle)) + vector.Y * MathF.Cos(float.DegreesToRadians(angle))
);
}
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.