mirror of
https://github.com/osukey/osukey.git
synced 2025-07-24 03:40:04 +09:00
Merge pull request #13937 from ekrctb/selection-editable-path
Add initial implementation of juice stream path editing
This commit is contained in:
@ -1,10 +1,17 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||||
{
|
{
|
||||||
@ -14,11 +21,52 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
|
|
||||||
protected override Container<Drawable> Content => contentContainer;
|
protected override Container<Drawable> Content => contentContainer;
|
||||||
|
|
||||||
|
[Cached(typeof(EditorBeatmap))]
|
||||||
|
[Cached(typeof(IBeatSnapProvider))]
|
||||||
|
protected readonly EditorBeatmap EditorBeatmap;
|
||||||
|
|
||||||
private readonly CatchEditorTestSceneContainer contentContainer;
|
private readonly CatchEditorTestSceneContainer contentContainer;
|
||||||
|
|
||||||
protected CatchSelectionBlueprintTestScene()
|
protected CatchSelectionBlueprintTestScene()
|
||||||
{
|
{
|
||||||
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
|
EditorBeatmap = new EditorBeatmap(new CatchBeatmap());
|
||||||
|
EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0;
|
||||||
|
EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||||
|
{
|
||||||
|
BeatLength = 100
|
||||||
|
});
|
||||||
|
|
||||||
|
base.Content.Add(new EditorBeatmapDependencyContainer(EditorBeatmap, new BindableBeatDivisor())
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
EditorBeatmap,
|
||||||
|
contentContainer = new CatchEditorTestSceneContainer()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void AddMouseMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
|
||||||
|
{
|
||||||
|
float y = HitObjectContainer.PositionAtTime(time);
|
||||||
|
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
|
||||||
|
InputManager.MoveMouseTo(pos);
|
||||||
|
});
|
||||||
|
|
||||||
|
private class EditorBeatmapDependencyContainer : Container
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly EditorClock editorClock;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly BindableBeatDivisor beatDivisor;
|
||||||
|
|
||||||
|
public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)
|
||||||
|
{
|
||||||
|
editorClock = new EditorClock(beatmap, beatDivisor);
|
||||||
|
this.beatDivisor = beatDivisor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,286 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||||
|
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.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||||
{
|
{
|
||||||
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||||
{
|
{
|
||||||
public TestSceneJuiceStreamSelectionBlueprint()
|
private JuiceStream hitObject;
|
||||||
|
|
||||||
|
private readonly ManualClock manualClock = new ManualClock();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
var hitObject = new JuiceStream
|
EditorBeatmap.Clear();
|
||||||
|
Content.Clear();
|
||||||
|
|
||||||
|
manualClock.CurrentTime = 0;
|
||||||
|
Content.Clock = new FramedClock(manualClock);
|
||||||
|
|
||||||
|
InputManager.ReleaseButton(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasicComponentLayout()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500 };
|
||||||
|
float[] positions = { 100, 200, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
for (int i = 0; i < times.Length; i++)
|
||||||
|
addVertexCheckStep(times.Length, i, times[i], positions[i]);
|
||||||
|
|
||||||
|
AddAssert("correct outline count", () =>
|
||||||
{
|
{
|
||||||
OriginalX = 100,
|
var expected = hitObject.NestedHitObjects.Count(h => !(h is TinyDroplet));
|
||||||
StartTime = 100,
|
return this.ChildrenOfType<FruitOutline>().Count() == expected;
|
||||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
});
|
||||||
|
AddAssert("correct vertex piece count", () =>
|
||||||
|
this.ChildrenOfType<VertexPiece>().Count() == times.Length);
|
||||||
|
|
||||||
|
AddAssert("first vertex is semitransparent", () =>
|
||||||
|
Precision.DefinitelyBigger(1, this.ChildrenOfType<VertexPiece>().First().Alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVertexDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 400, 700 };
|
||||||
|
float[] positions = { 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(500, 150);
|
||||||
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
|
addDragEndStep();
|
||||||
|
addDragStartStep(times[2], positions[2]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(300, 50);
|
||||||
|
addVertexCheckStep(3, 1, 300, 50);
|
||||||
|
addVertexCheckStep(3, 2, 500, 150);
|
||||||
|
|
||||||
|
AddMouseMoveStep(-100, 100);
|
||||||
|
addVertexCheckStep(3, 1, times[0], positions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500, 700 };
|
||||||
|
float[] positions = { 100, 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], positions[1]);
|
||||||
|
AddStep("press left", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddStep("release left", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
|
||||||
|
addDragStartStep(times[2], positions[2]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
|
||||||
|
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
|
||||||
|
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClampedPositionIsRestored()
|
||||||
|
{
|
||||||
|
const double velocity = 0.25;
|
||||||
|
double[] times = { 100, 500, 700 };
|
||||||
|
float[] positions = { 100, 100, 100 };
|
||||||
|
addBlueprintStep(times, positions, velocity);
|
||||||
|
|
||||||
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], 200);
|
||||||
|
addVertexCheckStep(3, 1, times[1], 200);
|
||||||
|
addVertexCheckStep(3, 2, times[2], 150);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], 100);
|
||||||
|
addVertexCheckStep(3, 1, times[1], 100);
|
||||||
|
// Stored position is restored.
|
||||||
|
addVertexCheckStep(3, 2, times[2], positions[2]);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], 300);
|
||||||
|
addDragEndStep();
|
||||||
|
addDragStartStep(times[1], 300);
|
||||||
|
|
||||||
|
AddMouseMoveStep(times[1], 100);
|
||||||
|
// Position is different because a changed position is committed when the previous drag is ended.
|
||||||
|
addVertexCheckStep(3, 2, times[2], 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollWhileDrag()
|
||||||
|
{
|
||||||
|
double[] times = { 300, 500 };
|
||||||
|
float[] positions = { 100, 100 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDragStartStep(times[1], positions[1]);
|
||||||
|
// This mouse move is necessary to start drag and capture the input.
|
||||||
|
AddMouseMoveStep(times[1], positions[1] + 50);
|
||||||
|
|
||||||
|
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
|
||||||
|
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
|
||||||
|
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateFromHitObject()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300 };
|
||||||
|
float[] positions = { 200, 200 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
AddStep("update hit object path", () =>
|
||||||
|
{
|
||||||
|
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||||
{
|
{
|
||||||
Vector2.Zero,
|
Vector2.Zero,
|
||||||
new Vector2(200, 100),
|
new Vector2(100, 100),
|
||||||
new Vector2(0, 200),
|
new Vector2(0, 200),
|
||||||
}),
|
});
|
||||||
};
|
EditorBeatmap.Update(hitObject);
|
||||||
var controlPoint = new ControlPointInfo();
|
|
||||||
controlPoint.Add(0, new TimingControlPoint
|
|
||||||
{
|
|
||||||
BeatLength = 100
|
|
||||||
});
|
});
|
||||||
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
|
AddAssert("path is updated", () => getVertices().Count > 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAddVertex()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 700 };
|
||||||
|
float[] positions = { 200, 200 };
|
||||||
|
addBlueprintStep(times, positions, 0.2);
|
||||||
|
|
||||||
|
addAddVertexSteps(500, 150);
|
||||||
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
|
addAddVertexSteps(90, 220);
|
||||||
|
addVertexCheckStep(4, 1, times[0], positions[0]);
|
||||||
|
|
||||||
|
addAddVertexSteps(750, 180);
|
||||||
|
addVertexCheckStep(5, 4, 750, 180);
|
||||||
|
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeleteVertex()
|
||||||
|
{
|
||||||
|
double[] times = { 100, 300, 500 };
|
||||||
|
float[] positions = { 100, 200, 150 };
|
||||||
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
|
addDeleteVertexSteps(times[1], positions[1]);
|
||||||
|
addVertexCheckStep(2, 1, times[2], positions[2]);
|
||||||
|
|
||||||
|
// The first vertex cannot be deleted.
|
||||||
|
addDeleteVertexSteps(times[0], positions[0]);
|
||||||
|
addVertexCheckStep(2, 0, times[0], positions[0]);
|
||||||
|
|
||||||
|
addDeleteVertexSteps(times[2], positions[2]);
|
||||||
|
addVertexCheckStep(1, 0, times[0], positions[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestVertexResampling()
|
||||||
|
{
|
||||||
|
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(100, 100),
|
||||||
|
new Vector2(50, 200),
|
||||||
|
}), 0.5);
|
||||||
|
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
|
||||||
|
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve);
|
||||||
|
addAddVertexSteps(150, 150);
|
||||||
|
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
|
||||||
|
{
|
||||||
|
hitObject = new JuiceStream
|
||||||
|
{
|
||||||
|
StartTime = time,
|
||||||
|
X = x,
|
||||||
|
Path = sliderPath,
|
||||||
|
};
|
||||||
|
EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity;
|
||||||
|
EditorBeatmap.Add(hitObject);
|
||||||
|
EditorBeatmap.Update(hitObject);
|
||||||
|
Assert.That(hitObject.Velocity, Is.EqualTo(velocity));
|
||||||
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
|
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
private void addBlueprintStep(double[] times, float[] positions, double velocity = 0.5)
|
||||||
|
{
|
||||||
|
var path = new JuiceStreamPath();
|
||||||
|
for (int i = 1; i < times.Length; i++)
|
||||||
|
path.Add((times[i] - times[0]) * velocity, positions[i] - positions[0]);
|
||||||
|
|
||||||
|
var sliderPath = new SliderPath();
|
||||||
|
path.ConvertToSliderPath(sliderPath, 0);
|
||||||
|
addBlueprintStep(times[0], positions[0], sliderPath, velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<JuiceStreamPathVertex> getVertices() => this.ChildrenOfType<EditablePath>().Single().Vertices;
|
||||||
|
|
||||||
|
private void addVertexCheckStep(int count, int index, double time, float x) => AddAssert($"vertex {index} of {count} at {time}, {x}", () =>
|
||||||
|
{
|
||||||
|
double expectedDistance = (time - hitObject.StartTime) * hitObject.Velocity;
|
||||||
|
float expectedX = x - hitObject.OriginalX;
|
||||||
|
var vertices = getVertices();
|
||||||
|
return vertices.Count == count &&
|
||||||
|
Precision.AlmostEquals(vertices[index].Distance, expectedDistance, 1e-3) &&
|
||||||
|
Precision.AlmostEquals(vertices[index].X, expectedX);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void addDragStartStep(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("start dragging", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDragEndStep() => AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
private void addAddVertexSteps(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("add vertex", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDeleteVertexSteps(double time, float x)
|
||||||
|
{
|
||||||
|
AddMouseMoveStep(time, x);
|
||||||
|
AddStep("delete vertex", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,190 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
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 int AddVertex(double distance, float x)
|
||||||
|
{
|
||||||
|
int index = path.InsertVertex(distance);
|
||||||
|
path.SetVertexPosition(index, x);
|
||||||
|
vertexStates.Insert(index, new VertexState());
|
||||||
|
|
||||||
|
correctFixedVertexPositions();
|
||||||
|
|
||||||
|
Debug.Assert(vertexStates.Count == VertexCount);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool RemoveVertex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= path.Vertices.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (vertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
path.RemoveVertices((_, i) => i == index);
|
||||||
|
|
||||||
|
vertexStates.RemoveAt(index);
|
||||||
|
if (vertexStates.Count == 0)
|
||||||
|
vertexStates.Add(new VertexState());
|
||||||
|
|
||||||
|
Debug.Assert(vertexStates.Count == VertexCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,130 @@
|
|||||||
|
// 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.Cursor;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
public class SelectionEditablePath : EditablePath, IHasContextMenu
|
||||||
|
{
|
||||||
|
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||||
|
|
||||||
|
// 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 void AddVertex(Vector2 relativePosition)
|
||||||
|
{
|
||||||
|
double distance = Math.Max(0, PositionToDistance(relativePosition.Y));
|
||||||
|
int index = AddVertex(distance, relativePosition.X);
|
||||||
|
selectOnly(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || VertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (e.Button == MouseButton.Left && e.ShiftPressed)
|
||||||
|
{
|
||||||
|
RemoveVertex(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.ControlPressed)
|
||||||
|
VertexStates[index].IsSelected = !VertexStates[index].IsSelected;
|
||||||
|
else if (!VertexStates[index].IsSelected)
|
||||||
|
selectOnly(index);
|
||||||
|
|
||||||
|
// Don't inhibit right click, to show the context menu
|
||||||
|
return e.Button != MouseButton.Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
int index = getMouseTargetVertex(e.ScreenSpaceMouseDownPosition);
|
||||||
|
if (index == -1 || VertexStates[index].IsFixed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (e.Button != MouseButton.Left)
|
||||||
|
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 = ToRelativePosition(e.ScreenSpaceMousePosition);
|
||||||
|
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 IEnumerable<MenuItem> getContextMenuItems()
|
||||||
|
{
|
||||||
|
int selectedCount = VertexStates.Count(state => state.IsSelected);
|
||||||
|
|
||||||
|
if (selectedCount != 0)
|
||||||
|
yield return new OsuMenuItem($"Delete selected {(selectedCount == 1 ? "vertex" : $"{selectedCount} vertices")}", MenuItemType.Destructive, deleteSelectedVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectOnly(int index)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VertexCount; i++)
|
||||||
|
VertexStates[i].IsSelected = i == index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelectedVertices()
|
||||||
|
{
|
||||||
|
for (int i = VertexCount - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (VertexStates[i].IsSelected)
|
||||||
|
RemoveVertex(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,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.Game.Rulesets.Catch.Objects;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the state of a vertex in the path of a <see cref="EditablePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class VertexState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vertex is selected.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vertex can be moved or deleted.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFixed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the vertex before a vertex moving operation starts.
|
||||||
|
/// This is used to implement "memory-less" moving operations (only the final position matters) to improve UX.
|
||||||
|
/// </summary>
|
||||||
|
public JuiceStreamPathVertex VertexBeforeChange { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,22 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
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;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
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;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||||
{
|
{
|
||||||
@ -17,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
|
||||||
|
|
||||||
|
public override MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||||
|
|
||||||
private float minNestedX;
|
private float minNestedX;
|
||||||
private float maxNestedX;
|
private float maxNestedX;
|
||||||
|
|
||||||
@ -26,13 +35,34 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
|
|
||||||
private readonly Cached pathCache = new Cached();
|
private readonly Cached pathCache = new Cached();
|
||||||
|
|
||||||
|
private readonly SelectionEditablePath editablePath;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="JuiceStreamPath.InvalidationID"/> of the <see cref="JuiceStreamPath"/> corresponding the current <see cref="SliderPath"/> of the hit object.
|
||||||
|
/// When the path is edited, the change is detected and the <see cref="SliderPath"/> of the hit object is updated.
|
||||||
|
/// </summary>
|
||||||
|
private int lastEditablePathId = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="SliderPath.Version"/> of the current <see cref="SliderPath"/> of the hit object.
|
||||||
|
/// When the <see cref="SliderPath"/> of the hit object is changed by external means, the change is detected and the <see cref="JuiceStreamPath"/> is re-initialized.
|
||||||
|
/// </summary>
|
||||||
|
private int lastSliderPathVersion = -1;
|
||||||
|
|
||||||
|
private Vector2 rightMouseDownPosition;
|
||||||
|
|
||||||
|
[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 +79,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 +95,38 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
pathCache.Validate();
|
pathCache.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnSelected()
|
||||||
|
{
|
||||||
|
initializeJuiceStreamPath();
|
||||||
|
base.OnSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
if (!IsSelected) return base.OnMouseDown(e);
|
||||||
|
|
||||||
|
switch (e.Button)
|
||||||
|
{
|
||||||
|
case MouseButton.Left when e.ControlPressed:
|
||||||
|
editablePath.AddVertex(editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MouseButton.Right:
|
||||||
|
// Record the mouse position to be used in the "add vertex" action.
|
||||||
|
rightMouseDownPosition = editablePath.ToRelativePosition(e.ScreenSpaceMouseDownPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnMouseDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
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 +145,38 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<MenuItem> getContextMenuItems()
|
||||||
|
{
|
||||||
|
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||||
|
{
|
||||||
|
editablePath.AddVertex(rightMouseDownPosition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
Reference in New Issue
Block a user