mirror of
https://github.com/ppy/osu.git
synced 2025-01-27 02:32:59 +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.
|
||||
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
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.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
|
||||
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 SelectionEditablePath editablePath;
|
||||
|
||||
private int lastEditablePathId = -1;
|
||||
|
||||
private int lastSliderPathVersion = -1;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
@ -59,10 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
pathCache.Validate();
|
||||
}
|
||||
|
||||
protected override void OnSelected()
|
||||
{
|
||||
initializeJuiceStreamPath();
|
||||
base.OnSelected();
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject _)
|
||||
{
|
||||
computeObjectBounds();
|
||||
pathCache.Invalidate();
|
||||
|
||||
if (lastSliderPathVersion != HitObject.Path.Version.Value)
|
||||
initializeJuiceStreamPath();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
Loading…
Reference in New Issue
Block a user