diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs index 5798869210..48aa74c5bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -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().Any()); - rectangularGridActive(false); + gridActive(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().Any()); - rectangularGridActive(true); + gridActive(true); AddStep("disable distance snap grid", () => InputManager.Key(Key.T)); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); - rectangularGridActive(true); + gridActive(true); AddStep("disable rectangular grid", () => InputManager.Key(Key.Y)); AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType().Any()); - rectangularGridActive(false); + gridActive(false); } [Test] @@ -117,33 +119,56 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [Test] public void TestGridSnapMomentaryToggle() { - rectangularGridActive(false); + gridActive(false); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); - rectangularGridActive(true); + gridActive(true); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); - rectangularGridActive(false); + gridActive(false); } - private void rectangularGridActive(bool active) + private void gridActive(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().Single(); - InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1))); + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(uniqueSnappingPosition(composer) + new Vector2(1, 1))); }); if (active) - AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0))); + { + AddAssert("placement blueprint at spacing + (0, 0)", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, + uniqueSnappingPosition(composer)); + }); + } else - AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1))); + { + AddAssert("placement blueprint at spacing + (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + return Precision.AlmostEquals(Editor.ChildrenOfType().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().Any()); + AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().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().Single().Spacing == new Vector2(size) + => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing.Value == new Vector2(size) && EditorBeatmap.BeatmapInfo.GridSize == size); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs new file mode 100644 index 0000000000..21cce553b1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -0,0 +1,171 @@ +// Copyright (c) ppy Pty Ltd . 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 + { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } = null!; + + /// + /// X position of the grid's origin. + /// + public BindableFloat StartPositionX { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.X / 2) + { + MinValue = 0f, + MaxValue = OsuPlayfield.BASE_SIZE.X, + Precision = 1f + }; + + /// + /// Y position of the grid's origin. + /// + public BindableFloat StartPositionY { get; } = new BindableFloat(OsuPlayfield.BASE_SIZE.Y / 2) + { + MinValue = 0f, + MaxValue = OsuPlayfield.BASE_SIZE.Y, + Precision = 1f + }; + + /// + /// The spacing between grid lines. + /// + public BindableFloat Spacing { get; } = new BindableFloat(4f) + { + MinValue = 4f, + MaxValue = 128f, + Precision = 1f + }; + + /// + /// Rotation of the grid lines in degrees. + /// + public BindableFloat GridLinesRotation { get; } = new BindableFloat(0f) + { + MinValue = -45f, + MaxValue = 45f, + Precision = 1f + }; + + /// + /// Read-only bindable representing the grid's origin. + /// Equivalent to new Vector2(StartPositionX, StartPositionY) + /// + public Bindable StartPosition { get; } = new Bindable(); + + /// + /// Read-only bindable representing the grid's spacing in both the X and Y dimension. + /// Equivalent to new Vector2(Spacing) + /// + public Bindable SpacingVector { get; } = new Bindable(); + + private ExpandableSlider startPositionXSlider = null!; + private ExpandableSlider startPositionYSlider = null!; + private ExpandableSlider spacingSlider = null!; + private ExpandableSlider gridLinesRotationSlider = null!; + + public OsuGridToolboxGroup() + : base("grid") + { + } + + private const float max_automatic_spacing = 64; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + startPositionXSlider = new ExpandableSlider + { + Current = StartPositionX, + KeyboardStep = 1, + }, + startPositionYSlider = new ExpandableSlider + { + Current = StartPositionY, + KeyboardStep = 1, + }, + spacingSlider = new ExpandableSlider + { + Current = Spacing, + KeyboardStep = 1, + }, + gridLinesRotationSlider = new ExpandableSlider + { + 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 e) + { + switch (e.Action) + { + case GlobalAction.EditorCycleGridDisplayMode: + nextGridSize(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index cc1d1fe89f..41f6b41f82 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -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); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs deleted file mode 100644 index efc6668ebf..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) ppy Pty Ltd . 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 - { - 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 e) - { - switch (e.Action) - { - case GlobalAction.EditorCycleGridDisplayMode: - nextGridSize(); - return true; - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - } -} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs similarity index 82% rename from osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs rename to osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs index e73a45e154..2721bc3602 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePositionSnapGrid.cs @@ -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 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 diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index 4074e681f9..a7f0a52003 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -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(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs new file mode 100644 index 0000000000..79b4fa2841 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -0,0 +1,166 @@ +// Copyright (c) ppy Pty Ltd . 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 generatedLines = new List(); + + 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; + } + + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + 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(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); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs new file mode 100644 index 0000000000..e576ac1e49 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . 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 + { + /// + /// The position of the origin of this in local coordinates. + /// + public Bindable StartPosition { get; } = new Bindable(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); + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs index cfc01fe17b..3bf0ef8ac3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -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 { - /// - /// The position of the origin of this in local coordinates. - /// - public Vector2 StartPosition { get; } - - private Vector2 spacing = Vector2.One; - /// /// The spacing between grid lines of this . /// - public Vector2 Spacing - { - get => spacing; - set - { - if (spacing.X <= 0 || spacing.Y <= 0) - throw new ArgumentException("Grid spacing must be positive."); + public Bindable Spacing { get; } = new Bindable(Vector2.One); - spacing = value; - gridCache.Invalidate(); - } + /// + /// The rotation in degrees of the grid lines of this . + /// + 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 generatedLines = new List(); - - 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); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 5ddffbef12..3738d8a7a1 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -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; } + /// + /// Rotate a vector around the origin. + /// + /// The vector. + /// The angle to rotate (in degrees). + 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)) + ); + } + /// /// Given a flip direction, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates.