mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'better-drawing' into dropdown-search
This commit is contained in:
commit
73189be6cc
@ -310,9 +310,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
assertPlaced(true);
|
||||
assertLength(760, tolerance: 10);
|
||||
assertLength(808, tolerance: 10);
|
||||
assertControlPointCount(5);
|
||||
assertControlPointType(0, PathType.BSpline(3));
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(3, null);
|
||||
@ -337,9 +337,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertPlaced(true);
|
||||
assertLength(600, tolerance: 10);
|
||||
assertControlPointCount(4);
|
||||
assertControlPointType(0, PathType.LINEAR);
|
||||
assertControlPointType(1, null);
|
||||
assertControlPointType(2, null);
|
||||
assertControlPointType(0, PathType.BSpline(4));
|
||||
assertControlPointType(1, PathType.BSpline(4));
|
||||
assertControlPointType(2, PathType.BSpline(4));
|
||||
assertControlPointType(3, null);
|
||||
}
|
||||
|
||||
|
@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3)));
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(4)));
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL));
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@ -49,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved(CanBeNull = true)]
|
||||
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
|
||||
@ -94,6 +95,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bSplineBuilder.CornerThreshold = e.NewValue;
|
||||
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
|
||||
}, true);
|
||||
|
||||
freehandToolboxGroup.CircleThreshold.BindValueChanged(e =>
|
||||
{
|
||||
Scheduler.AddOnce(updateSliderPathFromBSplineBuilder);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +203,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnDragEnd(e);
|
||||
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
bSplineBuilder.Finish();
|
||||
updateSliderPathFromBSplineBuilder();
|
||||
|
||||
// Change the state so it will snap the expected distance in endCurve.
|
||||
state = SliderPlacementState.Finishing;
|
||||
endCurve();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
@ -232,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
{
|
||||
segmentStart.Type = PathType.BSpline(3);
|
||||
segmentStart.Type = PathType.BSpline(4);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -300,7 +313,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||
else
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
@ -309,53 +325,126 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSliderPathFromBSplineBuilder()
|
||||
{
|
||||
IReadOnlyList<Vector2> builderPoints = bSplineBuilder.ControlPoints;
|
||||
IReadOnlyList<List<Vector2>> builderPoints = bSplineBuilder.ControlPoints;
|
||||
|
||||
if (builderPoints.Count == 0)
|
||||
if (builderPoints.Count == 0 || builderPoints[0].Count == 0)
|
||||
return;
|
||||
|
||||
int lastSegmentStart = 0;
|
||||
PathType? lastPathType = null;
|
||||
|
||||
HitObject.Path.ControlPoints.Clear();
|
||||
|
||||
// Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate.
|
||||
// Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started.
|
||||
// Iterate through generated segments and adding non-inheriting path types where appropriate.
|
||||
for (int i = 0; i < builderPoints.Count; i++)
|
||||
{
|
||||
bool isLastPoint = i == builderPoints.Count - 1;
|
||||
bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2];
|
||||
bool isLastSegment = i == builderPoints.Count - 1;
|
||||
var segment = builderPoints[i];
|
||||
|
||||
if (isNewSegment || isLastPoint)
|
||||
if (segment.Count == 0)
|
||||
continue;
|
||||
|
||||
// Replace this segment with a circular arc if it is a reasonable substitute.
|
||||
var circleArcSegment = tryCircleArc(segment);
|
||||
|
||||
if (circleArcSegment is not null)
|
||||
{
|
||||
int pointsInSegment = i - lastSegmentStart;
|
||||
|
||||
// Where possible, we can use the simpler LINEAR path type.
|
||||
PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3);
|
||||
|
||||
// Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined.
|
||||
if (lastPathType == pathType && lastPathType == PathType.LINEAR)
|
||||
pathType = null;
|
||||
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType));
|
||||
for (int j = lastSegmentStart + 1; j < i; j++)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j]));
|
||||
|
||||
if (isLastPoint)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i]));
|
||||
|
||||
// Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type.
|
||||
lastSegmentStart = (i += 2);
|
||||
if (pathType != null) lastPathType = pathType;
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[0], PathType.BSpline(4)));
|
||||
for (int j = 1; j < segment.Count - 1; j++)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[j]));
|
||||
}
|
||||
|
||||
if (isLastSegment)
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(segment[^1]));
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2[] tryCircleArc(List<Vector2> segment)
|
||||
{
|
||||
if (segment.Count < 3 || freehandToolboxGroup.CircleThreshold.Value == 0) return null;
|
||||
|
||||
// Assume the segment creates a reasonable circular arc and then check if it reasonable
|
||||
var points = PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), bSplineBuilder.Degree);
|
||||
var circleArcControlPoints = new[] { points[0], points[points.Count / 2], points[^1] };
|
||||
var circleArc = new CircularArcProperties(circleArcControlPoints);
|
||||
|
||||
if (!circleArc.IsValid) return null;
|
||||
|
||||
double length = circleArc.ThetaRange * circleArc.Radius;
|
||||
|
||||
if (length > 1000) return null;
|
||||
|
||||
double loss = 0;
|
||||
Vector2? lastPoint = null;
|
||||
Vector2? lastVec = null;
|
||||
Vector2? lastVec2 = null;
|
||||
int? lastDir = null;
|
||||
int? lastDir2 = null;
|
||||
double totalWinding = 0;
|
||||
|
||||
// Loop through the points and check if they are not too far away from the circular arc.
|
||||
// Also make sure it curves monotonically in one direction and at most one loop is done.
|
||||
foreach (var point in points)
|
||||
{
|
||||
var vec = point - circleArc.Centre;
|
||||
loss += Math.Pow((vec.Length - circleArc.Radius) / length, 2);
|
||||
|
||||
if (lastVec.HasValue)
|
||||
{
|
||||
double det = lastVec.Value.X * vec.Y - lastVec.Value.Y * vec.X;
|
||||
int dir = Math.Sign(det);
|
||||
|
||||
if (dir == 0)
|
||||
continue;
|
||||
|
||||
if (lastDir.HasValue && dir != lastDir)
|
||||
return null; // Circle center is not inside the polygon
|
||||
|
||||
lastDir = dir;
|
||||
}
|
||||
|
||||
lastVec = vec;
|
||||
|
||||
if (lastPoint.HasValue)
|
||||
{
|
||||
var vec2 = point - lastPoint.Value;
|
||||
|
||||
if (lastVec2.HasValue)
|
||||
{
|
||||
double dot = Vector2.Dot(vec2, lastVec2.Value);
|
||||
double det = lastVec2.Value.X * vec2.Y - lastVec2.Value.Y * vec2.X;
|
||||
double angle = Math.Atan2(det, dot);
|
||||
int dir2 = Math.Sign(angle);
|
||||
|
||||
if (dir2 == 0)
|
||||
continue;
|
||||
|
||||
if (lastDir2.HasValue && dir2 != lastDir2)
|
||||
return null; // Curvature changed, like in an S-shape
|
||||
|
||||
totalWinding += Math.Abs(angle);
|
||||
lastDir2 = dir2;
|
||||
}
|
||||
|
||||
lastVec2 = vec2;
|
||||
}
|
||||
|
||||
lastPoint = point;
|
||||
}
|
||||
|
||||
loss /= points.Count;
|
||||
|
||||
return loss > freehandToolboxGroup.CircleThreshold.Value || totalWinding > MathHelper.TwoPi ? null : circleArcControlPoints;
|
||||
}
|
||||
|
||||
private enum SliderPlacementState
|
||||
{
|
||||
Initial,
|
||||
ControlPoints,
|
||||
Drawing
|
||||
Drawing,
|
||||
Finishing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
}
|
||||
|
||||
public BindableFloat Tolerance { get; } = new BindableFloat(1.5f)
|
||||
public BindableFloat Tolerance { get; } = new BindableFloat(1.8f)
|
||||
{
|
||||
MinValue = 0.05f,
|
||||
MaxValue = 3f,
|
||||
MaxValue = 2.0f,
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
@ -31,8 +31,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
public BindableFloat CircleThreshold { get; } = new BindableFloat(0.0015f)
|
||||
{
|
||||
MinValue = 0f,
|
||||
MaxValue = 0.005f,
|
||||
Precision = 0.0001f
|
||||
};
|
||||
|
||||
// We map internal ranges to a more standard range of values for display to the user.
|
||||
private readonly BindableInt displayTolerance = new BindableInt(40)
|
||||
private readonly BindableInt displayTolerance = new BindableInt(90)
|
||||
{
|
||||
MinValue = 5,
|
||||
MaxValue = 100
|
||||
@ -44,8 +51,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
MaxValue = 100
|
||||
};
|
||||
|
||||
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 100
|
||||
};
|
||||
|
||||
private ExpandableSlider<int> toleranceSlider = null!;
|
||||
private ExpandableSlider<int> cornerThresholdSlider = null!;
|
||||
private ExpandableSlider<int> circleThresholdSlider = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -59,6 +73,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
cornerThresholdSlider = new ExpandableSlider<int>
|
||||
{
|
||||
Current = displayCornerThreshold
|
||||
},
|
||||
circleThresholdSlider = new ExpandableSlider<int>
|
||||
{
|
||||
Current = displayCircleThreshold
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -83,18 +101,32 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
|
||||
}, true);
|
||||
|
||||
displayCircleThreshold.BindValueChanged(threshold =>
|
||||
{
|
||||
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
|
||||
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
|
||||
|
||||
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
|
||||
}, true);
|
||||
|
||||
Tolerance.BindValueChanged(tolerance =>
|
||||
displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue)
|
||||
);
|
||||
CornerThreshold.BindValueChanged(threshold =>
|
||||
displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue)
|
||||
);
|
||||
CircleThreshold.BindValueChanged(threshold =>
|
||||
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
|
||||
);
|
||||
|
||||
float displayToInternalTolerance(float v) => v / 33f;
|
||||
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f);
|
||||
float displayToInternalTolerance(float v) => v / 50f;
|
||||
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
|
||||
|
||||
float displayToInternalCornerThreshold(float v) => v / 100f;
|
||||
int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f);
|
||||
|
||||
float displayToInternalCircleThreshold(float v) => v / 20000f;
|
||||
int internalToDisplayCircleThreshold(float v) => (int)Math.Round(v * 20000f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user