mirror of
https://github.com/ppy/osu.git
synced 2024-09-21 18:47:27 +08:00
Merge pull request #28309 from OliBomby/scale-tool
Add precise scaling control to osu! editor
This commit is contained in:
commit
bf7dc715de
@ -101,7 +101,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
RightToolbox.AddRange(new EditorToolboxGroup[]
|
RightToolbox.AddRange(new EditorToolboxGroup[]
|
||||||
{
|
{
|
||||||
new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, },
|
new TransformToolboxGroup
|
||||||
|
{
|
||||||
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||||
|
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||||
|
},
|
||||||
FreehandlSliderToolboxGroup
|
FreehandlSliderToolboxGroup
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||||
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
CanRotateAroundSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
||||||
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
|
CanRotateAroundPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
}
|
}
|
||||||
|
|
||||||
private OsuHitObject[]? objectsInRotation;
|
private OsuHitObject[]? objectsInRotation;
|
||||||
|
@ -24,6 +24,16 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
|
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether scaling anchored by the center of the playfield can currently be performed.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> CanScaleFromPlayfieldOrigin { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a single slider is currently selected, which results in a different scaling behaviour.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IEditorChangeHandler? changeHandler { get; set; }
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
@ -53,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
CanScaleX.Value = quad.Width > 0;
|
CanScaleX.Value = quad.Width > 0;
|
||||||
CanScaleY.Value = quad.Height > 0;
|
CanScaleY.Value = quad.Height > 0;
|
||||||
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
||||||
|
CanScaleFromPlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||||
|
IsScalingSlider.Value = selectedMovableObjects.Count() == 1 && selectedMovableObjects.First() is Slider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||||
@ -67,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
|
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||||
}
|
}
|
||||||
@ -92,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
|
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
||||||
|
|
||||||
foreach (var (ho, originalState) in objectsInScale)
|
foreach (var (ho, originalState) in objectsInScale)
|
||||||
{
|
{
|
||||||
@ -158,27 +170,36 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
|
|
||||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||||
/// <param name="scale">The scale to be clamped</param>
|
/// <param name="scale">The scale to be clamped</param>
|
||||||
/// <returns>The clamped scale vector</returns>
|
/// <returns>The clamped scale vector</returns>
|
||||||
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
|
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
||||||
{
|
{
|
||||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||||
|
if (objectsInScale == null)
|
||||||
|
return scale;
|
||||||
|
|
||||||
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
|
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
|
|
||||||
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
|
|
||||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
|
|
||||||
|
|
||||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
|
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||||
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
origin = slider.Position;
|
||||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
|
|
||||||
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
|
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||||
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
|
||||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
|
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||||
|
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||||
|
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
|
||||||
|
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
|
||||||
|
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
|
||||||
|
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||||
|
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
|
||||||
|
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||||
|
|
||||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||||
}
|
}
|
||||||
|
@ -78,11 +78,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
angleInput.TakeFocus();
|
||||||
|
angleInput.SelectAll();
|
||||||
|
});
|
||||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||||
rotationOrigin.Items.First().Select();
|
rotationOrigin.Items.First().Select();
|
||||||
|
|
||||||
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
{
|
{
|
||||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
|
212
osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
Normal file
212
osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
// 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 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.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseScalePopover : OsuPopover
|
||||||
|
{
|
||||||
|
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||||
|
|
||||||
|
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||||
|
private BindableNumber<float> scaleInputBindable = null!;
|
||||||
|
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||||
|
|
||||||
|
private RadioButton playfieldCentreButton = null!;
|
||||||
|
private RadioButton selectionCentreButton = null!;
|
||||||
|
|
||||||
|
private OsuCheckbox xCheckBox = null!;
|
||||||
|
private OsuCheckbox yCheckBox = null!;
|
||||||
|
|
||||||
|
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
||||||
|
{
|
||||||
|
this.scaleHandler = scaleHandler;
|
||||||
|
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
scaleInput = new SliderWithTextBoxInput<float>("Scale:")
|
||||||
|
{
|
||||||
|
Current = scaleInputBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
MinValue = 0.5f,
|
||||||
|
MaxValue = 2,
|
||||||
|
Precision = 0.001f,
|
||||||
|
Value = 1,
|
||||||
|
Default = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
scaleOrigin = new EditorRadioButtonCollection
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Items = new[]
|
||||||
|
{
|
||||||
|
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||||
|
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||||
|
selectionCentreButton = new RadioButton("Selection centre",
|
||||||
|
() => setOrigin(ScaleOrigin.SelectionCentre),
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(4),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
xCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "X-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
yCheckBox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Y-axis",
|
||||||
|
Current = { Value = true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||||
|
};
|
||||||
|
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||||
|
{
|
||||||
|
selectionCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to its centre." : string.Empty;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
scaleInput.TakeFocus();
|
||||||
|
scaleInput.SelectAll();
|
||||||
|
});
|
||||||
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
|
xCheckBox.Current.BindValueChanged(x => setAxis(x.NewValue, yCheckBox.Current.Value));
|
||||||
|
yCheckBox.Current.BindValueChanged(y => setAxis(xCheckBox.Current.Value, y.NewValue));
|
||||||
|
|
||||||
|
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||||
|
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||||
|
|
||||||
|
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||||
|
|
||||||
|
scaleInfo.BindValueChanged(scale =>
|
||||||
|
{
|
||||||
|
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
||||||
|
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAxisCheckBoxesEnabled()
|
||||||
|
{
|
||||||
|
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, true);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toggleAxisAvailable(xCheckBox.Current, scaleHandler.CanScaleX.Value);
|
||||||
|
toggleAxisAvailable(yCheckBox.Current, scaleHandler.CanScaleY.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleAxisAvailable(Bindable<bool> axisBindable, bool available)
|
||||||
|
{
|
||||||
|
// enable the bindable to allow setting the value
|
||||||
|
axisBindable.Disabled = false;
|
||||||
|
// restore the presumed default value given the axis's new availability state
|
||||||
|
axisBindable.Value = available;
|
||||||
|
axisBindable.Disabled = !available;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMaxScale()
|
||||||
|
{
|
||||||
|
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float max_scale = 10;
|
||||||
|
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
||||||
|
|
||||||
|
if (!scaleInfo.Value.XAxis)
|
||||||
|
scale.X = max_scale;
|
||||||
|
if (!scaleInfo.Value.YAxis)
|
||||||
|
scale.Y = max_scale;
|
||||||
|
|
||||||
|
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setOrigin(ScaleOrigin origin)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||||
|
updateMaxScale();
|
||||||
|
updateAxisCheckBoxesEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
||||||
|
|
||||||
|
private void setAxis(bool x, bool y)
|
||||||
|
{
|
||||||
|
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
scaleHandler.Begin();
|
||||||
|
updateMaxScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
if (IsLoaded) scaleHandler.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ScaleOrigin
|
||||||
|
{
|
||||||
|
PlayfieldCentre,
|
||||||
|
SelectionCentre
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PreciseScaleInfo(float Scale, ScaleOrigin Origin, bool XAxis, bool YAxis);
|
||||||
|
}
|
@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
private readonly Bindable<bool> canRotate = new BindableBool();
|
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
|
private EditorToolButton scaleButton = null!;
|
||||||
private Bindable<bool> canRotatePlayfieldOrigin = null!;
|
|
||||||
private Bindable<bool> canRotateSelectionOrigin = null!;
|
|
||||||
|
|
||||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||||
|
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||||
|
|
||||||
public TransformToolboxGroup()
|
public TransformToolboxGroup()
|
||||||
: base("transform")
|
: base("transform")
|
||||||
@ -45,7 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler)),
|
() => new PreciseRotationPopover(RotationHandler)),
|
||||||
// TODO: scale
|
scaleButton = new EditorToolButton("Scale",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseScalePopover(ScaleHandler))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -54,21 +56,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
// aggregate two values into canRotate
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
|
||||||
|
|
||||||
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
|
canScale.AddSource(ScaleHandler.CanScaleX);
|
||||||
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
canScale.AddSource(ScaleHandler.CanScaleY);
|
||||||
|
canScale.AddSource(ScaleHandler.CanScaleFromPlayfieldOrigin);
|
||||||
void updateCanRotateAggregate()
|
|
||||||
{
|
|
||||||
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
@ -82,6 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
rotateButton.TriggerClick();
|
rotateButton.TriggerClick();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case GlobalAction.EditorToggleScaleControl:
|
||||||
|
{
|
||||||
|
scaleButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
this.getTargetContainer = getTargetContainer;
|
this.getTargetContainer = getTargetContainer;
|
||||||
|
|
||||||
CanRotateSelectionOrigin.Value = true;
|
CanRotateAroundSelectionOrigin.Value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
|
@ -50,6 +50,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
Component.BorderColour = colours.Blue;
|
Component.BorderColour = colours.Blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SelectAll() => Component.SelectAll();
|
||||||
|
|
||||||
protected virtual OsuTextBox CreateTextBox() => new OsuTextBox();
|
protected virtual OsuTextBox CreateTextBox() => new OsuTextBox();
|
||||||
|
|
||||||
public override bool AcceptsFocus => true;
|
public override bool AcceptsFocus => true;
|
||||||
|
@ -87,6 +87,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
|
public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
|
||||||
|
|
||||||
|
public bool SelectAll() => textBox.SelectAll();
|
||||||
|
|
||||||
private bool updatingFromTextBox;
|
private bool updatingFromTextBox;
|
||||||
|
|
||||||
private void textChanged(ValueChangedEvent<string> change)
|
private void textChanged(ValueChangedEvent<string> change)
|
||||||
|
@ -142,6 +142,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
|
private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
|
||||||
@ -411,6 +412,9 @@ namespace osu.Game.Input.Bindings
|
|||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
||||||
EditorToggleRotateControl,
|
EditorToggleRotateControl,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
|
||||||
|
EditorToggleScaleControl,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
|
||||||
IncreaseOffset,
|
IncreaseOffset,
|
||||||
|
|
||||||
|
@ -369,6 +369,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
|
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Toggle scale control"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Increase mod speed"
|
/// "Increase mod speed"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
|
|
||||||
private void updateState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
CanRotateSelectionOrigin.Value = selectedItems.Count > 0;
|
CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable[]? objectsInRotation;
|
private Drawable[]? objectsInRotation;
|
||||||
|
@ -127,7 +127,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
if (rotationHandler != null)
|
if (rotationHandler != null)
|
||||||
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
|
canRotate.BindTo(rotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
|
|
||||||
if (scaleHandler != null)
|
if (scaleHandler != null)
|
||||||
{
|
{
|
||||||
|
@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether rotation anchored by the selection origin can currently be performed.
|
/// Whether rotation anchored by the selection origin can currently be performed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<bool> CanRotateSelectionOrigin { get; private set; } = new BindableBool();
|
public Bindable<bool> CanRotateAroundSelectionOrigin { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether rotation anchored by the center of the playfield can currently be performed.
|
/// Whether rotation anchored by the center of the playfield can currently be performed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<bool> CanRotatePlayfieldOrigin { get; private set; } = new BindableBool();
|
public Bindable<bool> CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a single, instant, atomic rotation operation.
|
/// Performs a single, instant, atomic rotation operation.
|
||||||
|
Loading…
Reference in New Issue
Block a user