diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs new file mode 100644 index 000000000000..b13663cb446b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs @@ -0,0 +1,126 @@ +// 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.Input.Events; +using osu.Game.Rulesets.Edit; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Edit.Blueprints +{ + public partial class GridPlacementBlueprint : PlacementBlueprint + { + [Resolved] + private HitObjectComposer? hitObjectComposer { get; set; } + + private OsuGridToolboxGroup gridToolboxGroup = null!; + private Vector2 originalOrigin; + private float originalSpacing; + private float originalRotation; + + [BackgroundDependencyLoader] + private void load(OsuGridToolboxGroup gridToolboxGroup) + { + this.gridToolboxGroup = gridToolboxGroup; + originalOrigin = gridToolboxGroup.StartPosition.Value; + originalSpacing = gridToolboxGroup.Spacing.Value; + originalRotation = gridToolboxGroup.GridLinesRotation.Value; + } + + public override void EndPlacement(bool commit) + { + if (!commit && PlacementActive != PlacementState.Finished) + { + gridToolboxGroup.StartPosition.Value = originalOrigin; + gridToolboxGroup.Spacing.Value = originalSpacing; + if (!gridToolboxGroup.GridLinesRotation.Disabled) + gridToolboxGroup.GridLinesRotation.Value = originalRotation; + } + + base.EndPlacement(commit); + + // You typically only place the grid once, so we switch back to the last tool after placement. + if (commit && hitObjectComposer is OsuHitObjectComposer osuHitObjectComposer) + osuHitObjectComposer.SetLastTool(); + } + + protected override bool OnClick(ClickEvent e) + { + if (e.Button == MouseButton.Left) + { + switch (PlacementActive) + { + case PlacementState.Waiting: + BeginPlacement(true); + return true; + + case PlacementState.Active: + EndPlacement(true); + return true; + } + } + + return base.OnClick(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Right) + { + // Reset the grid to the default values. + gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default; + gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default; + if (!gridToolboxGroup.GridLinesRotation.Disabled) + gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default; + EndPlacement(true); + return true; + } + + return base.OnMouseDown(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Left) + { + BeginPlacement(true); + return true; + } + + return base.OnDragStart(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + if (PlacementActive == PlacementState.Active) + EndPlacement(true); + + base.OnDragEnd(e); + } + + public override SnapType SnapType => ~SnapType.GlobalGrids; + + public override void UpdateTimeAndPosition(SnapResult result) + { + var pos = ToLocalSpace(result.ScreenSpacePosition); + + if (PlacementActive != PlacementState.Active) + gridToolboxGroup.StartPosition.Value = pos; + else + { + // Default to the original spacing and rotation if the distance is too small. + if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2) + { + gridToolboxGroup.Spacing.Value = originalSpacing; + if (!gridToolboxGroup.GridLinesRotation.Disabled) + gridToolboxGroup.GridLinesRotation.Value = originalRotation; + } + else + { + gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs b/osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs new file mode 100644 index 000000000000..626153a7fdaa --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs @@ -0,0 +1,29 @@ +// 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.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Osu.Edit.Blueprints; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class GridFromPointsTool : CompositionTool + { + public GridFromPointsTool() + : base("Grid") + { + TooltipText = """ + Left click to set the origin. + Left click again to set the spacing and rotation. + Right click to reset to default. + Click and drag to set the origin, spacing and rotation. + """; + } + + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.DraftingCompass }; + + public override PlacementBlueprint CreatePlacementBlueprint() => new GridPlacementBlueprint(); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs index 972224a230b3..2b88860cc8ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuGridToolboxGroup.cs @@ -11,12 +11,10 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; @@ -40,7 +38,6 @@ public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandle { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.X, - Precision = 1f }; /// @@ -50,7 +47,6 @@ public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandle { MinValue = 0f, MaxValue = OsuPlayfield.BASE_SIZE.Y, - Precision = 1f }; /// @@ -60,7 +56,6 @@ public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandle { MinValue = 4f, MaxValue = 128f, - Precision = 1f }; /// @@ -70,14 +65,13 @@ public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandle { MinValue = -180f, MaxValue = 180f, - Precision = 1f }; /// /// Read-only bindable representing the grid's origin. /// Equivalent to new Vector2(StartPositionX, StartPositionY) /// - public Bindable StartPosition { get; } = new Bindable(); + public Bindable StartPosition { get; } = new Bindable(OsuPlayfield.BASE_SIZE / 2); /// /// Read-only bindable representing the grid's spacing in both the X and Y dimension. @@ -93,8 +87,6 @@ public partial class OsuGridToolboxGroup : EditorToolboxGroup, IKeyBindingHandle private ExpandableSlider gridLinesRotationSlider = null!; private EditorRadioButtonCollection gridTypeButtons = null!; - private ExpandableButton useSelectedObjectPositionButton = null!; - public OsuGridToolboxGroup() : base("grid") { @@ -102,6 +94,26 @@ public OsuGridToolboxGroup() private const float max_automatic_spacing = 64; + public void SetGridFromPoints(Vector2 point1, Vector2 point2) + { + StartPositionX.Value = point1.X; + StartPositionY.Value = point1.Y; + + // Get the angle between the two points and normalize to the valid range. + if (!GridLinesRotation.Disabled) + { + float period = GridLinesRotation.MaxValue - GridLinesRotation.MinValue; + GridLinesRotation.Value = normalizeRotation(MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X)), period); + } + + // Divide the distance so that there is a good density of grid lines. + // This matches the maximum grid size of the grid size cycling hotkey. + float dist = Vector2.Distance(point1, point2); + while (dist >= max_automatic_spacing) + dist /= 2; + Spacing.Value = dist; + } + [BackgroundDependencyLoader] private void load() { @@ -117,20 +129,6 @@ private void load() Current = StartPositionY, KeyboardStep = 1, }, - useSelectedObjectPositionButton = new ExpandableButton - { - ExpandedLabelText = "Centre on selected object", - Action = () => - { - if (editorBeatmap.SelectedHitObjects.Count != 1) - return; - - var position = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position; - StartPosition.Value = new Vector2(MathF.Round(position.X), MathF.Round(position.Y)); - updateEnabledStates(); - }, - RelativeSizeAxes = Axes.X, - }, spacingSlider = new ExpandableSlider { Current = Spacing, @@ -179,15 +177,15 @@ protected override void LoadComplete() StartPositionX.BindValueChanged(x => { - startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; - startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}"; + startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}"; + startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}"; 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}"; + startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}"; + startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}"; StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue); }, true); @@ -195,13 +193,12 @@ protected override void LoadComplete() { StartPositionX.Value = pos.NewValue.X; StartPositionY.Value = pos.NewValue.Y; - updateEnabledStates(); }); Spacing.BindValueChanged(spacing => { - spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; - spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; + spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; + spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}"; SpacingVector.Value = new Vector2(spacing.NewValue); editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; }, true); @@ -219,34 +216,29 @@ protected override void LoadComplete() switch (v.NewValue) { case PositionSnapGridType.Square: - GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45; + GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 90); GridLinesRotation.MinValue = -45; GridLinesRotation.MaxValue = 45; break; case PositionSnapGridType.Triangle: - GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30; + GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 60); GridLinesRotation.MinValue = -30; GridLinesRotation.MaxValue = 30; break; } }, true); - editorBeatmap.BeatmapReprocessed += updateEnabledStates; - editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates()); expandingContainer?.Expanded.BindValueChanged(v => { gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; - updateEnabledStates(); }, true); } - private void updateEnabledStates() + private float normalizeRotation(float rotation, float period) { - useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true - && editorBeatmap.SelectedHitObjects.Count == 1 - && !Precision.AlmostEquals(StartPosition.Value, ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position, 0.5f); + return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f; } private void nextGridSize() diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 2b5de8a5d813..7c50558b9253 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -45,7 +45,8 @@ protected override DrawableRuleset CreateDrawableRuleset(Ruleset r { new HitCircleCompositionTool(), new SliderCompositionTool(), - new SpinnerCompositionTool() + new SpinnerCompositionTool(), + new GridFromPointsTool() }; private readonly Bindable rectangularGridSnapToggle = new Bindable(); @@ -79,13 +80,12 @@ private void load() // Give a bit of breathing room around the playfield content. PlayfieldContentContainer.Padding = new MarginPadding(10); - LayerBelowRuleset.AddRange(new Drawable[] - { + LayerBelowRuleset.Add( distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both } - }); + ); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid(); diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index a83744475875..3f5d612eb8e2 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -20,13 +20,13 @@ public SettingsButton() Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; } - public LocalisableString TooltipText { get; set; } - public IEnumerable Keywords { get; set; } = Array.Empty(); public BindableBool CanBeShown { get; } = new BindableBool(true); IBindable IConditionalFilterable.CanBeShown => CanBeShown; + public LocalisableString TooltipText { get; set; } + public override IEnumerable FilterTerms { get diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 316e8e55e88c..0499e10607ad 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -90,6 +90,9 @@ public abstract partial class HitObjectComposer : HitObjectComposer, IP private Bindable autoSeekOnPlacement; private readonly Bindable composerFocusMode = new Bindable(); + [CanBeNull] + private RadioButton lastTool; + protected DrawableRuleset DrawableRuleset { get; private set; } protected HitObjectComposer(Ruleset ruleset) @@ -213,8 +216,7 @@ private void load(OsuConfigManager config, [CanBeNull] Editor editor) }, }; - toolboxCollection.Items = CompositionTools - .Prepend(new SelectTool()) + toolboxCollection.Items = (CompositionTools.Prepend(new SelectTool())) .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .ToList(); @@ -231,7 +233,7 @@ private void load(OsuConfigManager config, [CanBeNull] Editor editor) sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b))); - setSelectTool(); + SetSelectTool(); EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; } @@ -256,7 +258,7 @@ protected override void LoadComplete() { // it's important this is performed before the similar code in EditorRadioButton disables the button. if (!timing.NewValue) - setSelectTool(); + SetSelectTool(); }); EditorBeatmap.HasTiming.BindValueChanged(hasTiming => @@ -460,14 +462,18 @@ private void selectionChanged(object sender, NotifyCollectionChangedEventArgs ch if (EditorBeatmap.SelectedHitObjects.Any()) { // ensure in selection mode if a selection is made. - setSelectTool(); + SetSelectTool(); } } - private void setSelectTool() => toolboxCollection.Items.First().Select(); + public void SetSelectTool() => toolboxCollection.Items.First().Select(); + + public void SetLastTool() => (lastTool ?? toolboxCollection.Items.First()).Select(); private void toolSelected(CompositionTool tool) { + lastTool = toolboxCollection.Items.OfType().FirstOrDefault(i => i.Tool == BlueprintContainer.CurrentTool); + BlueprintContainer.CurrentTool = tool; if (!(tool is SelectTool)) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index d2a54e8e03a2..a36de0243395 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -71,6 +71,11 @@ public virtual void EndPlacement(bool commit) PlacementActive = PlacementState.Finished; } + /// + /// Determines which objects to snap to for the snap result in . + /// + public virtual SnapType SnapType => SnapType.All; + /// /// Updates the time and position of this based on the provided snap information. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index f0296d45aa45..cbec8fc7a34f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -297,7 +297,7 @@ private void refreshTool() private void updatePlacementPosition() { - var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position); + var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType); // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);