1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 02:22:59 +08:00

Merge pull request #7734 from peppy/editor-slider-repeat

Add the ability to extend hold notes (spinners / sliders etc.) via timeline
This commit is contained in:
Dan Balasescu 2020-02-06 15:35:16 +09:00 committed by GitHub
commit 8d12d820f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 352 additions and 77 deletions

View File

@ -36,7 +36,11 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
public double EndTime => StartTime + Duration;
public double EndTime
{
get => StartTime + Duration;
set => Duration = value - StartTime;
}
public double Duration { get; set; }
}

View File

@ -110,7 +110,11 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double EndTime
{
get => StartTime + this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;

View File

@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public class HoldNote : ManiaHitObject, IHasEndTime
{
public double EndTime => StartTime + Duration;
public double EndTime
{
get => StartTime + Duration;
set => Duration = value - StartTime;
}
private double duration;

View File

@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class Slider : OsuHitObject, IHasCurve
{
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double EndTime
{
get => StartTime + this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double Duration => EndTime - StartTime;
private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();
@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
endPositionCache.Invalidate();
updateNestedPositions();
}
}

View File

@ -18,7 +18,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
private const float base_distance = 100;
public double EndTime => StartTime + Duration;
public double EndTime
{
get => StartTime + Duration;
set => Duration = value - StartTime;
}
public double Duration { get; set; }

View File

@ -11,7 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class Swell : TaikoHitObject, IHasEndTime
{
public double EndTime => StartTime + Duration;
public double EndTime
{
get => StartTime + Duration;
set => Duration = value - StartTime;
}
public double Duration { get; set; }

View File

@ -1,6 +1,8 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
@ -10,6 +12,17 @@ namespace osu.Game.Tests.Visual.Editor
[TestFixture]
public class TestSceneTimelineBlueprintContainer : TimelineTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(TimelineHitObjectBlueprint),
};
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer();
protected override void LoadComplete()
{
base.LoadComplete();
Clock.Seek(10000);
}
}
}

View File

@ -27,7 +27,12 @@ namespace osu.Game.Tests.Visual.Editor
};
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap());
private readonly EditorBeatmap editorBeatmap;
public TestSceneTimingScreen()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
[BackgroundDependencyLoader]
private void load()

View File

@ -13,7 +13,6 @@ using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
@ -38,7 +37,9 @@ namespace osu.Game.Tests.Visual.Editor
{
Beatmap.Value = new WaveformTestBeatmap(audio);
var editorBeatmap = new EditorBeatmap((Beatmap<HitObject>)Beatmap.Value.Beatmap);
var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset);
var editorBeatmap = new EditorBeatmap(playable);
Dependencies.Cache(editorBeatmap);
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);

View File

@ -11,6 +11,8 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Rulesets.Catch;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests
@ -20,11 +22,18 @@ namespace osu.Game.Tests
/// </summary>
public class WaveformTestBeatmap : WorkingBeatmap
{
private readonly Beatmap beatmap;
private readonly ITrackStore trackStore;
public WaveformTestBeatmap(AudioManager audioManager)
: base(new BeatmapInfo(), audioManager)
: this(audioManager, new WaveformBeatmap())
{
}
public WaveformTestBeatmap(AudioManager audioManager, Beatmap beatmap)
: base(beatmap.BeatmapInfo, audioManager)
{
this.beatmap = beatmap;
trackStore = audioManager.GetTrackStore(getZipReader());
}
@ -34,11 +43,11 @@ namespace osu.Game.Tests
trackStore?.Dispose();
}
private Stream getStream() => TestResources.GetTestBeatmapStream();
private static Stream getStream() => TestResources.GetTestBeatmapStream();
private ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream());
private static ZipArchiveReader getZipReader() => new ZipArchiveReader(getStream());
protected override IBeatmap GetBeatmap() => createTestBeatmap();
protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null;
@ -57,10 +66,16 @@ namespace osu.Game.Tests
}
}
private Beatmap createTestBeatmap()
private class WaveformBeatmap : TestBeatmap
{
public WaveformBeatmap()
: base(new CatchRuleset().RulesetInfo)
{
}
protected override Beatmap CreateBeatmap()
{
using (var reader = getZipReader())
{
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))))
using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);

View File

@ -26,7 +26,12 @@ namespace osu.Game.Rulesets.Objects.Legacy
public List<IList<HitSampleInfo>> NodeSamples { get; set; }
public int RepeatCount { get; set; }
public double EndTime => StartTime + this.SpanCount() * Distance / Velocity;
public double EndTime
{
get => StartTime + this.SpanCount() * Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double Duration => EndTime - StartTime;
public double Velocity = 1;

View File

@ -1,6 +1,8 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
@ -11,7 +13,8 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The time at which the HitObject ends.
/// </summary>
double EndTime { get; }
[JsonIgnore]
double EndTime { get; set; }
/// <summary>
/// The duration of the HitObject.

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The amount of times the HitObject repeats.
/// </summary>
int RepeatCount { get; }
int RepeatCount { get; set; }
/// <summary>
/// The samples to be played when each node of the <see cref="IHasRepeats"/> is hit.<br />

View File

@ -179,11 +179,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
{
var targetTime = (position.X / Content.DrawWidth) * track.Length;
return (position, beatSnapProvider.SnapTime(targetTime));
}
public double GetTimeFromScreenSpacePosition(Vector2 position)
=> getTimeFromPosition(Content.ToLocalSpace(position));
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) =>
(position, beatSnapProvider.SnapTime(getTimeFromPosition(position)));
private double getTimeFromPosition(Vector2 localPosition) =>
(localPosition.X / Content.DrawWidth) * track.Length;
public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException();

View File

@ -49,20 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void OnDrag(DragEvent e)
{
if (timeline != null)
{
var timelineQuad = timeline.ScreenSpaceDrawQuad;
var mouseX = e.ScreenSpaceMousePosition.X;
// scroll if in a drag and dragging outside visible extents
if (mouseX > timelineQuad.TopRight.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
else if (mouseX < timelineQuad.TopLeft.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
handleScrollViaDrag(e);
base.OnDrag(e);
lastDragEvent = e;
}
protected override void OnDragEnd(DragEndEvent e)
@ -74,7 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void Update()
{
// trigger every frame so drags continue to update selection while playback is scrolling the timeline.
if (IsDragged)
if (lastDragEvent != null)
OnDrag(lastDragEvent);
base.Update();
@ -82,10 +71,33 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject);
protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject)
{
OnDragHandled = handleScrollViaDrag
};
protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new TimelineDragBox(performSelect);
private void handleScrollViaDrag(DragEvent e)
{
lastDragEvent = e;
if (lastDragEvent == null)
return;
if (timeline != null)
{
var timelineQuad = timeline.ScreenSpaceDrawQuad;
var mouseX = e.ScreenSpaceMousePosition.X;
// scroll if in a drag and dragging outside visible extents
if (mouseX > timelineQuad.TopRight.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
else if (mouseX < timelineQuad.TopLeft.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
}
internal class TimelineSelectionHandler : SelectionHandler
{
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation

View File

@ -1,12 +1,18 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -19,17 +25,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
private readonly Circle circle;
private readonly Container extensionBar;
[UsedImplicitly]
private readonly Bindable<double> startTime;
public const float THICKNESS = 3;
public Action<DragEvent> OnDragHandled;
private readonly DragBar dragBar;
private readonly List<Container> shadowComponents = new List<Container>();
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 16;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || circle.ReceivePositionalInputAt(screenSpacePos);
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
{
@ -44,26 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
if (hitObject is IHasEndTime)
{
AddInternal(extensionBar = new Container
{
CornerRadius = 2,
Masking = true,
Size = new Vector2(1, THICKNESS),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
Colour = Color4.Black,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
});
}
AddInternal(circle = new Circle
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
@ -71,9 +62,65 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
BorderColour = Color4.Black,
BorderThickness = THICKNESS,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = Color4.Black
},
};
shadowComponents.Add(circle);
if (hitObject is IHasEndTime)
{
DragBar dragBarUnderlay;
Container extensionBar;
AddRangeInternal(new Drawable[]
{
extensionBar = new Container
{
Masking = true,
Size = new Vector2(1, thickness),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = Color4.Black
},
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
},
circle,
// only used for drawing the shadow
dragBarUnderlay = new DragBar(null),
// cover up the shadow on the join
new Box
{
Height = thickness,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
},
dragBar = new DragBar(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) },
});
shadowComponents.Add(dragBarUnderlay);
shadowComponents.Add(extensionBar);
}
else
{
AddInternal(circle);
}
updateShadows();
}
protected override void Update()
@ -84,18 +131,46 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Width = (float)(HitObject.GetEndTime() - HitObject.StartTime);
}
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos) ||
circle.ReceivePositionalInputAt(screenSpacePos) ||
dragBar?.ReceivePositionalInputAt(screenSpacePos) == true;
protected override void OnSelected()
{
circle.BorderColour = Color4.Orange;
if (extensionBar != null)
extensionBar.Colour = Color4.Orange;
updateShadows();
}
private void updateShadows()
{
foreach (var s in shadowComponents)
{
if (State == SelectionState.Selected)
{
s.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius / 2,
Colour = Color4.Orange,
};
}
else
{
s.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = State == SelectionState.Selected ? Color4.Orange : Color4.Black
};
}
}
}
protected override void OnDeselected()
{
circle.BorderColour = Color4.Black;
if (extensionBar != null)
extensionBar.Colour = Color4.Black;
updateShadows();
}
public override Quad SelectionQuad
@ -103,14 +178,130 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
get
{
// correctly include the circle in the selection quad region, as it is usually outside the blueprint itself.
var circleQuad = circle.ScreenSpaceDrawQuad;
var actualQuad = ScreenSpaceDrawQuad;
var leftQuad = circle.ScreenSpaceDrawQuad;
var rightQuad = dragBar?.ScreenSpaceDrawQuad ?? ScreenSpaceDrawQuad;
return new Quad(circleQuad.TopLeft, Vector2.ComponentMax(actualQuad.TopRight, circleQuad.TopRight),
circleQuad.BottomLeft, Vector2.ComponentMax(actualQuad.BottomRight, circleQuad.BottomRight));
return new Quad(leftQuad.TopLeft, Vector2.ComponentMax(rightQuad.TopRight, leftQuad.TopRight),
leftQuad.BottomLeft, Vector2.ComponentMax(rightQuad.BottomRight, leftQuad.BottomRight));
}
}
public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public class DragBar : Container
{
private readonly HitObject hitObject;
[Resolved]
private Timeline timeline { get; set; }
public Action<DragEvent> OnDragHandled;
public override bool HandlePositionalInput => hitObject != null;
public DragBar(HitObject hitObject)
{
this.hitObject = hitObject;
CornerRadius = 2;
Masking = true;
Size = new Vector2(5, 1);
Anchor = Anchor.CentreRight;
Origin = Anchor.Centre;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
}
};
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private bool hasMouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
hasMouseDown = true;
updateState();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
hasMouseDown = false;
updateState();
base.OnMouseUp(e);
}
private void updateState()
{
Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White;
}
protected override bool OnDragStart(DragStartEvent e) => true;
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
protected override void OnDrag(DragEvent e)
{
base.OnDrag(e);
OnDragHandled?.Invoke(e);
var time = timeline.GetTimeFromScreenSpacePosition(e.ScreenSpaceMousePosition);
switch (hitObject)
{
case IHasRepeats repeatHitObject:
// find the number of repeats which can fit in the requested time.
var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1);
var proposedCount = Math.Max(0, (int)((time - hitObject.StartTime) / lengthOfOneRepeat) - 1);
if (proposedCount == repeatHitObject.RepeatCount)
return;
repeatHitObject.RepeatCount = proposedCount;
break;
case IHasEndTime endTimeHitObject:
var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time));
if (endTimeHitObject.EndTime == snappedTime)
return;
endTimeHitObject.EndTime = snappedTime;
break;
}
beatmap.UpdateHitObject(hitObject);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
OnDragHandled?.Invoke(null);
}
}
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tests.Beatmaps
{
public TestBeatmap(RulesetInfo ruleset)
{
var baseBeatmap = createTestBeatmap();
var baseBeatmap = CreateBeatmap();
BeatmapInfo = baseBeatmap.BeatmapInfo;
ControlPointInfo = baseBeatmap.ControlPointInfo;
@ -37,6 +37,8 @@ namespace osu.Game.Tests.Beatmaps
};
}
protected virtual Beatmap CreateBeatmap() => createTestBeatmap();
private static Beatmap createTestBeatmap()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data)))