mirror of
https://github.com/ppy/osu.git
synced 2025-01-27 19:35:15 +08:00
Add initial juice stream editing
This commit is contained in:
parent
6d49165664
commit
8cc1630655
@ -0,0 +1,159 @@
|
|||||||
|
// 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 System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public abstract class EditablePath : CompositeDrawable
|
||||||
|
{
|
||||||
|
public int PathId => path.InvalidationID;
|
||||||
|
|
||||||
|
public IReadOnlyList<JuiceStreamPathVertex> Vertices => path.Vertices;
|
||||||
|
|
||||||
|
public int VertexCount => path.Vertices.Count;
|
||||||
|
|
||||||
|
protected readonly Func<float, double> PositionToDistance;
|
||||||
|
|
||||||
|
protected IReadOnlyList<VertexState> VertexStates => vertexStates;
|
||||||
|
|
||||||
|
private readonly JuiceStreamPath path = new JuiceStreamPath();
|
||||||
|
|
||||||
|
// Invariant: `path.Vertices.Count == vertexStates.Count`
|
||||||
|
private readonly List<VertexState> vertexStates = new List<VertexState>
|
||||||
|
{
|
||||||
|
new VertexState { IsFixed = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<VertexState> previousVertexStates = new List<VertexState>();
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private IBeatSnapProvider beatSnapProvider { get; set; }
|
||||||
|
|
||||||
|
protected EditablePath(Func<float, double> positionToDistance)
|
||||||
|
{
|
||||||
|
PositionToDistance = positionToDistance;
|
||||||
|
|
||||||
|
Anchor = Anchor.BottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
while (path.Vertices.Count < InternalChildren.Count)
|
||||||
|
RemoveInternal(InternalChildren[^1]);
|
||||||
|
|
||||||
|
while (InternalChildren.Count < path.Vertices.Count)
|
||||||
|
AddInternal(new VertexPiece());
|
||||||
|
|
||||||
|
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
|
||||||
|
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
{
|
||||||
|
var piece = (VertexPiece)InternalChildren[i];
|
||||||
|
var vertex = path.Vertices[i];
|
||||||
|
piece.Position = new Vector2(vertex.X, (float)(vertex.Distance * distanceToYFactor));
|
||||||
|
piece.UpdateFrom(vertexStates[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeFromHitObject(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
var sliderPath = hitObject.Path;
|
||||||
|
path.ConvertFromSliderPath(sliderPath);
|
||||||
|
|
||||||
|
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||||
|
if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear))
|
||||||
|
{
|
||||||
|
path.ResampleVertices(hitObject.NestedHitObjects
|
||||||
|
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||||
|
.Select(h => (h.StartTime - hitObject.StartTime) * hitObject.Velocity));
|
||||||
|
}
|
||||||
|
|
||||||
|
vertexStates.Clear();
|
||||||
|
vertexStates.AddRange(path.Vertices.Select((_, i) => new VertexState
|
||||||
|
{
|
||||||
|
IsFixed = i == 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY);
|
||||||
|
|
||||||
|
if (beatSnapProvider == null) return;
|
||||||
|
|
||||||
|
double endTime = hitObject.StartTime + path.Distance / hitObject.Velocity;
|
||||||
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
|
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
return ToLocalSpace(screenSpacePosition) - new Vector2(0, DrawHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||||
|
|
||||||
|
protected void MoveSelectedVertices(double distanceDelta, float xDelta)
|
||||||
|
{
|
||||||
|
// Because the vertex list may be reordered due to distance change, the state list must be reordered as well.
|
||||||
|
previousVertexStates.Clear();
|
||||||
|
previousVertexStates.AddRange(vertexStates);
|
||||||
|
|
||||||
|
// We will recreate the path from scratch. Note that `Clear` leaves the first vertex.
|
||||||
|
int vertexCount = VertexCount;
|
||||||
|
path.Clear();
|
||||||
|
vertexStates.RemoveRange(1, vertexCount - 1);
|
||||||
|
|
||||||
|
for (int i = 1; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
var state = previousVertexStates[i];
|
||||||
|
double distance = state.VertexBeforeChange.Distance;
|
||||||
|
if (state.IsSelected)
|
||||||
|
distance += distanceDelta;
|
||||||
|
|
||||||
|
int newIndex = path.InsertVertex(Math.Max(0, distance));
|
||||||
|
vertexStates.Insert(newIndex, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, restore positions of the non-selected vertices.
|
||||||
|
for (int i = 0; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
if (!vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, move the selected vertices.
|
||||||
|
for (int i = 0; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
if (vertexStates[i].IsSelected && !vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X + xDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, correct the position of fixed vertices.
|
||||||
|
correctFixedVertexPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void correctFixedVertexPositions()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
{
|
||||||
|
if (vertexStates[i].IsFixed)
|
||||||
|
path.SetVertexPosition(i, vertexStates[i].VertexBeforeChange.X);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class SelectionEditablePath : EditablePath
|
||||||
|
{
|
||||||
|
// To handle when the editor is scrolled while dragging.
|
||||||
|
private Vector2 dragStartPosition;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
|
public SelectionEditablePath(Func<float, double> positionToDistance)
|
||||||
|
: base(positionToDistance)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
|
||||||
|
|
||||||
|
if (index == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (e.ControlPressed)
|
||||||
|
VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
|
||||||
|
else if (!VertexStates[index].IsSelected)
|
||||||
|
selectOnly(index);
|
||||||
|
|
||||||
|
// Don't inhabit right click, to show the context menu
|
||||||
|
return e.Button != MouseButton.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
if (e.Button != MouseButton.Left || getMouseTargetVertex(e.ScreenSpaceMouseDownPosition) == -1) return false;
|
||||||
|
|
||||||
|
dragStartPosition = ToRelativePosition(e.ScreenSpaceMouseDownPosition);
|
||||||
|
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
VertexStates[i].VertexBeforeChange = Vertices[i];
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDrag(DragEvent e)
|
||||||
|
{
|
||||||
|
Vector2 mousePosition = ToLocalSpace(e.ScreenSpaceMousePosition) - new Vector2(0, DrawHeight);
|
||||||
|
double distanceDelta = PositionToDistance(mousePosition.Y) - PositionToDistance(dragStartPosition.Y);
|
||||||
|
float xDelta = mousePosition.X - dragStartPosition.X;
|
||||||
|
MoveSelectedVertices(distanceDelta, xDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMouseTargetVertex(Vector2 screenSpacePosition)
|
||||||
|
{
|
||||||
|
for (int i = InternalChildren.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (i < VertexCount && InternalChildren[i].ReceivePositionalInputAt(screenSpacePosition))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectOnly(int index)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
VertexStates[i].IsSelected = i == index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
// 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 osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class VertexPiece : Circle
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour osuColour { get; set; }
|
||||||
|
|
||||||
|
public VertexPiece()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
Size = new Vector2(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateFrom(VertexState state)
|
||||||
|
{
|
||||||
|
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
|
||||||
|
Alpha = state.IsFixed ? 0.5f : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +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 osu.Game.Rulesets.Catch.Objects;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class VertexState
|
||||||
|
{
|
||||||
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
public bool IsFixed { get; set; }
|
||||||
|
|
||||||
|
public JuiceStreamPathVertex VertexBeforeChange { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Primitives;
|
|||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
@ -26,13 +28,24 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
private readonly Cached pathCache = new Cached();
|
private readonly Cached pathCache = new Cached();
|
||||||
|
|
||||||
|
private readonly SelectionEditablePath editablePath;
|
||||||
|
|
||||||
|
private int lastEditablePathId = -1;
|
||||||
|
|
||||||
|
private int lastSliderPathVersion = -1;
|
||||||
|
|
||||||
|
[Resolved(CanBeNull = true)]
|
||||||
|
[CanBeNull]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer()
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
|
editablePath = new SelectionEditablePath(positionToDistance)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +62,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
if (!IsSelected) return;
|
if (!IsSelected) return;
|
||||||
|
|
||||||
nestedOutlineContainer.Position = scrollingPath.Position = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
if (editablePath.PathId != lastEditablePathId)
|
||||||
|
updateHitObjectFromPath();
|
||||||
|
|
||||||
|
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
|
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
||||||
|
|
||||||
|
editablePath.UpdateFrom(HitObjectContainer, HitObject);
|
||||||
|
|
||||||
if (pathCache.IsValid) return;
|
if (pathCache.IsValid) return;
|
||||||
|
|
||||||
@ -59,10 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
pathCache.Validate();
|
pathCache.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnSelected()
|
||||||
|
{
|
||||||
|
initializeJuiceStreamPath();
|
||||||
|
base.OnSelected();
|
||||||
|
}
|
||||||
|
|
||||||
private void onDefaultsApplied(HitObject _)
|
private void onDefaultsApplied(HitObject _)
|
||||||
{
|
{
|
||||||
computeObjectBounds();
|
computeObjectBounds();
|
||||||
pathCache.Invalidate();
|
pathCache.Invalidate();
|
||||||
|
|
||||||
|
if (lastSliderPathVersion != HitObject.Path.Version.Value)
|
||||||
|
initializeJuiceStreamPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void computeObjectBounds()
|
private void computeObjectBounds()
|
||||||
@ -81,6 +109,30 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double positionToDistance(float relativeYPosition)
|
||||||
|
{
|
||||||
|
double time = HitObjectContainer.TimeAtPosition(relativeYPosition, HitObject.StartTime);
|
||||||
|
return (time - HitObject.StartTime) * HitObject.Velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeJuiceStreamPath()
|
||||||
|
{
|
||||||
|
editablePath.InitializeFromHitObject(HitObject);
|
||||||
|
|
||||||
|
// Record the current ID to update the hit object only when a change is made to the path.
|
||||||
|
lastEditablePathId = editablePath.PathId;
|
||||||
|
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHitObjectFromPath()
|
||||||
|
{
|
||||||
|
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||||
|
editorBeatmap?.Update(HitObject);
|
||||||
|
|
||||||
|
lastEditablePathId = editablePath.PathId;
|
||||||
|
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
Loading…
Reference in New Issue
Block a user