Skip to content

Commit

Permalink
Add precise rotation control to osu! editor
Browse files Browse the repository at this point in the history
  • Loading branch information
bdach committed Jul 23, 2023
1 parent bfd7e87 commit 06accf9
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 49 deletions.
5 changes: 5 additions & 0 deletions osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ private void load()

// we may be entering the screen with a selection already active
updateDistanceSnapGrid();

RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
}

protected override ComposeBlueprintContainer CreateBlueprintContainer()
Expand Down
107 changes: 107 additions & 0 deletions osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Timing;
using osuTK;

namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseRotationPopover : OsuPopover
{
private readonly SelectionRotationHandler rotationHandler;

public IBindable<PreciseRotationInfo> RotationInfo => rotationInfo;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));

private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;

public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
}

[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 300,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
angleInput = new SliderWithTextBoxInput<float>("Rotation angle (degrees):")
{
Current = new BindableNumber<float>
{
MinValue = -180,
MaxValue = 180,
Precision = 1
},
TransferValueOnCommit = false
},
rotationOrigin = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.ObjectGroup })
}
}
}
};
}

protected override void LoadComplete()
{
base.LoadComplete();

ScheduleAfterChildren(() => angleInput.TakeFocus());
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();

rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
});
}

protected override void PopIn()
{
base.PopIn();
rotationHandler.Begin();
}

protected override void PopOut()
{
base.PopOut();

if (IsLoaded)
rotationHandler.Commit();
}
}

public enum RotationOrigin
{
PlayfieldCentre,
SelectionCentre
}

public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
}
114 changes: 114 additions & 0 deletions osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// 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.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;

namespace osu.Game.Rulesets.Osu.Edit
{
public partial class TransformToolboxGroup : EditorToolboxGroup
{
private readonly Bindable<bool> canRotate = new BindableBool();
private readonly Bindable<TernaryState> rotationState = new Bindable<TernaryState>();

private readonly Bindable<TernaryState> scaleState = new Bindable<TernaryState>();

private DrawableTernaryButtonWithPopover rotateButton = null!;

public SelectionRotationHandler RotationHandler { get; init; } = null!;

public TransformToolboxGroup()
: base("transform")
{
}

[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
rotateButton = new DrawableTernaryButtonWithPopover(new TernaryButton(rotationState, "Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }))
{
RelativeSizeAxes = Axes.X,
Popover = () => new PreciseRotationPopover(RotationHandler),
},
new DrawableTernaryButton(new TernaryButton(scaleState, "Scale", () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }))
{
RelativeSizeAxes = Axes.X,
},
}
};
}

protected override void LoadComplete()
{
base.LoadComplete();

canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Alpha = canRotate.Value ? 1 : 0, true);
}

protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat) return false;

if (e.ControlPressed && e.Key == Key.R)
{
rotateButton.TriggerClick();
return true;
}

return base.OnKeyDown(e);
}

private partial class DrawableTernaryButtonWithPopover : DrawableTernaryButton, IHasPopover
{
public Func<Popover>? Popover { get; init; }

public DrawableTernaryButtonWithPopover(TernaryButton button)
: base(button)
{
}

public Popover? GetPopover()
{
var popover = Popover?.Invoke();

popover?.State.BindValueChanged(state =>
{
if (state.NewValue == Visibility.Hidden)
Button.Bindable.Value = TernaryState.False;
});

return popover;
}

protected override void UpdateSelectionState()
{
base.UpdateSelectionState();

if (Button.Bindable.Value == TernaryState.True)
this.ShowPopover();
}
}
}
}
1 change: 1 addition & 0 deletions osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public LocalisableString PlaceholderText

public string Text
{
get => Component.Text;
set => Component.Text = value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal partial class DrawableTernaryButton : OsuButton
public partial class DrawableTernaryButton : OsuButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultIconColour;
Expand Down Expand Up @@ -57,7 +57,7 @@ protected override void LoadComplete()
{
base.LoadComplete();

Button.Bindable.BindValueChanged(_ => updateSelectionState(), true);
Button.Bindable.BindValueChanged(_ => UpdateSelectionState(), true);

Action = onAction;
}
Expand All @@ -67,7 +67,7 @@ private void onAction()
Button.Toggle();
}

private void updateSelectionState()
protected virtual void UpdateSelectionState()
{
if (!IsLoaded)
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public abstract partial class BlueprintContainer<T> : CompositeDrawable, IKeyBin

public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }

protected SelectionHandler<T> SelectionHandler { get; private set; }
public SelectionHandler<T> SelectionHandler { get; private set; }

private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();

Expand Down
4 changes: 2 additions & 2 deletions osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindi
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }

protected SelectionRotationHandler RotationHandler { get; private set; }
public SelectionRotationHandler RotationHandler { get; private set; }

protected SelectionHandler()
{
Expand Down Expand Up @@ -87,7 +87,7 @@ public SelectionBox CreateSelectionBox()
RotationHandler = RotationHandler,
OnScale = HandleScale,
OnFlip = HandleFlip,
OnReverse = HandleReverse,
OnReverse = HandleReverse
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public virtual void Begin()
/// <remarks>
/// <para>
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
/// As such, the values of <see cref="rotation"/> and <see cref="origin"/> supplied should be relative to the state of the objects being rotated
/// As such, the values of <paramref name="rotation"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being rotated
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
/// </para>
/// <para>
Expand Down
Loading

0 comments on commit 06accf9

Please sign in to comment.