From a41b6fcf57eafc4276583f2ad658ea5bb1569d75 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 22:14:55 +0100 Subject: [PATCH 01/26] Add optimization and separated segments --- .../TestSceneInteractivePathDrawing.cs | 17 ++-- .../Utils/IncrementalBSplineBuilder.cs | 83 +++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index df0f3a2647..73a7ec4aab 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -73,15 +73,18 @@ private void updateControlPointsViz() { controlPointViz.Clear(); - foreach (var cp in bSplineBuilder.ControlPoints) + foreach (var segment in bSplineBuilder.ControlPoints) { - controlPointViz.Add(new Box + foreach (var cp in segment) { - Origin = Anchor.Centre, - Size = new Vector2(10), - Position = cp, - Colour = Color4.LightGreen, - }); + controlPointViz.Add(new Box + { + Origin = Anchor.Centre, + Size = new Vector2(10), + Position = cp, + Colour = Color4.LightGreen, + }); + } } } diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index ee48292459..1033ab565c 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Caching; using osu.Framework.Graphics.Primitives; using osuTK; @@ -75,9 +74,9 @@ private static float getAbsWindingAt(List path, List cumulativeD Value = new List() }; - private readonly Cached> controlPoints = new Cached> + private readonly Cached>> controlPoints = new Cached>> { - Value = new List() + Value = new List>() }; private int degree; @@ -166,7 +165,7 @@ public IReadOnlyList OutputPath /// /// The list of control points of the B-Spline. This is inferred from the input path. /// - public IReadOnlyList ControlPoints + public IReadOnlyList> ControlPoints { get { @@ -288,17 +287,16 @@ private void regenerateApproximatedPathControlPoints() if (vertices.Count < 2) { - controlPoints.Value = vertices; + controlPoints.Value = new List> { vertices }; return; } - controlPoints.Value = new List(); + controlPoints.Value = new List>(); Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cps = controlPoints.Value; - cps.Add(vertices[0]); + var cpss = controlPoints.Value; // Populate each segment between corners with control points that have density proportional to the // product of Tolerance and curvature. @@ -315,63 +313,62 @@ private void regenerateApproximatedPathControlPoints() Vector2 c1 = getPathAt(vertices, distances, cornerTs[i]); Line linearConnection = new Line(c0, c1); - var tmp = new List(); + var cps = new List { c0 }; + var segmentPath = new List(); bool allOnLine = true; float onLineThreshold = 5 * Tolerance * step_size; if (t1 > t0) { int nSteps = (int)((t1 - t0) / step_size); + float currentWinding = 0.5f * Tolerance; for (int j = 0; j < nSteps; ++j) { float t = t0 + j * step_size; - totalWinding += getAbsWindingAt(vertices, distances, t); - } + float winding = getAbsWindingAt(vertices, distances, t); + totalWinding += winding; + currentWinding += winding; - float currentWinding = 0; + Vector2 p = getPathAt(vertices, distances, t); + segmentPath.Add(p); - for (int j = 0; j < nSteps; ++j) - { - float t = t0 + j * step_size; + if (currentWinding < Tolerance) continue; + + if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) + allOnLine = false; - // Don't permit control points too close to the next corner as they are redundant. - // However, ignore this limitation on the last segment of the path as we would like - // it to follow the user's drawing as closely as possible. - if (currentWinding + tmp.Count * Tolerance > totalWinding - Tolerance * 0.5f && i < cornerTs.Count - 1) - break; + cps.Add(p); + currentWinding -= Tolerance; + } + } - if (currentWinding > Tolerance) - { - Vector2 p = getPathAt(vertices, distances, t); - if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) - allOnLine = false; + if (allOnLine) + { + cpss.Add(new List { c0, c1 }); + continue; + } - tmp.Add(p); - currentWinding -= Tolerance; - } + cps.Add(c1); - currentWinding += getAbsWindingAt(vertices, distances, t); - } + if (cps.Count > 2 && cps.Count < 1000) + { + int res = (int)(totalWinding * 10); + cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, res, 100, 5, interpolatorResolution: res * 2, initialControlPoints: cps); } - if (!allOnLine) - cps.AddRange(tmp); - - // Insert the corner at the end of the segment as a sharp control point consisting of - // degree many regular control points, meaning that the BSpline will have a kink here. - // Special case the last corner which will be the end of the path and thus automatically - // duplicated degree times by BSplineToPiecewiseLinear down the line. - if (i == cornerTs.Count - 1) - cps.Add(c1); - else - cps.AddRange(Enumerable.Repeat(c1, degree)); + cpss.Add(cps); } } private void redrawApproximatedPath() { - outputCache.Value = PathApproximator.BSplineToPiecewiseLinear(ControlPoints.ToArray(), degree); + outputCache.Value = new List(); + + foreach (var segment in ControlPoints) + { + outputCache.Value.AddRange(PathApproximator.BSplineToPiecewiseLinear(segment.ToArray(), degree)); + } } /// @@ -382,7 +379,7 @@ public void Clear() inputPath.Clear(); cumulativeInputPathLength.Clear(); - controlPoints.Value = new List(); + controlPoints.Value = new List>(); outputCache.Value = new List(); } From 8126b50b28c7c7f7e71e44302bee7fba220f2e82 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 22:15:24 +0100 Subject: [PATCH 02/26] decrease on line threshold for small segments --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 1033ab565c..40753c6932 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -316,7 +316,7 @@ private void regenerateApproximatedPathControlPoints() var cps = new List { c0 }; var segmentPath = new List(); bool allOnLine = true; - float onLineThreshold = 5 * Tolerance * step_size; + float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); if (t1 > t0) { From ca7f4583a548b89040efeaf632a172dd3b9e9e15 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 22:15:35 +0100 Subject: [PATCH 03/26] add line between control points --- .../Visual/Drawables/TestSceneInteractivePathDrawing.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index 73a7ec4aab..c71b853080 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Graphics; using osuTK.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,7 @@ public partial class TestSceneInteractivePathDrawing : FrameworkTestScene { private readonly Path rawDrawnPath; private readonly Path approximatedDrawnPath; + private readonly Path controlPointPath; private readonly Container controlPointViz; private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); @@ -39,6 +41,12 @@ public TestSceneInteractivePathDrawing() Colour = Color4.Blue, PathRadius = 3, }, + controlPointPath = new Path + { + Colour = Color4.LightGreen, + PathRadius = 1, + Alpha = 0.5f, + }, controlPointViz = new Container { RelativeSizeAxes = Axes.Both, @@ -71,6 +79,7 @@ public TestSceneInteractivePathDrawing() private void updateControlPointsViz() { + controlPointPath.Vertices = bSplineBuilder.ControlPoints.SelectMany(o => o).ToArray(); controlPointViz.Clear(); foreach (var segment in bSplineBuilder.ControlPoints) From fb2568a66afaaa3398a5af12dbf6258517f9a96f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 22:47:28 +0100 Subject: [PATCH 04/26] adjust max control point count --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 40753c6932..f93fe20587 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -351,7 +351,7 @@ private void regenerateApproximatedPathControlPoints() cps.Add(c1); - if (cps.Count > 2 && cps.Count < 1000) + if (cps.Count > 2 && cps.Count < 100) { int res = (int)(totalWinding * 10); cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, res, 100, 5, interpolatorResolution: res * 2, initialControlPoints: cps); From 5529f580be838194e2f9fbefadd2ad807d5d0a10 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 24 Nov 2023 22:47:39 +0100 Subject: [PATCH 05/26] reduce excess resolution --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index f93fe20587..0c9eb3dcae 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -354,7 +354,7 @@ private void regenerateApproximatedPathControlPoints() if (cps.Count > 2 && cps.Count < 100) { int res = (int)(totalWinding * 10); - cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, res, 100, 5, interpolatorResolution: res * 2, initialControlPoints: cps); + cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, res, 100, 5, interpolatorResolution: res, initialControlPoints: cps); } cpss.Add(cps); From 1bc8c216320a363ce7a9e875218f1a38128d6a3f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 26 Nov 2023 01:29:47 +0100 Subject: [PATCH 06/26] Add control points to viz --- .../TestScenePiecewiseLinearToBSpline.cs | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs index b6a21c7459..cd4cad5631 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePiecewiseLinearToBSpline.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Framework.Testing; @@ -32,7 +34,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(0).AddRange(new[] { createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -40,7 +41,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(1).AddRange(new[] { createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -48,7 +48,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(2).AddRange(new[] { createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -56,7 +55,6 @@ public TestScenePiecewiseLinearToBSpline() Cell(3).AddRange(new[] { createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), - new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), doubleApproximatedPathTests[^1], }); @@ -134,26 +132,7 @@ private void updateTests() public delegate List ApproximatorFunc(ReadOnlySpan controlPoints); - private partial class ApproximatedPathTest : SmoothPath - { - public ApproximatedPathTest(ApproximatorFunc approximator) - { - Vector2[] points = new Vector2[5]; - points[0] = new Vector2(50, 250); - points[1] = new Vector2(150, 230); - points[2] = new Vector2(100, 150); - points[3] = new Vector2(200, 80); - points[4] = new Vector2(250, 50); - - AutoSizeAxes = Axes.None; - RelativeSizeAxes = Axes.Both; - PathRadius = 2; - Vertices = approximator(points); - Colour = Color4.White; - } - } - - private partial class DoubleApproximatedPathTest : SmoothPath + private partial class DoubleApproximatedPathTest : Container { private readonly Vector2[] inputPath; @@ -173,6 +152,10 @@ private partial class DoubleApproximatedPathTest : SmoothPath public bool OptimizePath { get; set; } + private readonly Path approximatedDrawnPath; + private readonly Path controlPointPath; + private readonly Container controlPointViz; + public DoubleApproximatedPathTest(ApproximatorFunc approximator) { Vector2[] points = new Vector2[5]; @@ -184,9 +167,33 @@ public DoubleApproximatedPathTest(ApproximatorFunc approximator) AutoSizeAxes = Axes.None; RelativeSizeAxes = Axes.Both; - PathRadius = 2; - Colour = Color4.Magenta; inputPath = approximator(points).ToArray(); + + Children = new Drawable[] + { + new Path + { + Colour = Color4.White, + PathRadius = 2, + Vertices = inputPath, + }, + approximatedDrawnPath = new Path + { + Colour = Color4.Magenta, + PathRadius = 2, + }, + controlPointPath = new Path + { + Colour = Color4.LightGreen, + PathRadius = 1, + Alpha = 0.5f, + }, + controlPointViz = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + }; } public void UpdatePath() @@ -194,7 +201,20 @@ public void UpdatePath() if (!OptimizePath) return; var controlPoints = PathApproximator.PiecewiseLinearToBSpline(inputPath, NumControlPoints, Degree, NumTestPoints, MaxIterations, LearningRate, B1, B2); - Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); + approximatedDrawnPath.Vertices = PathApproximator.BSplineToPiecewiseLinear(controlPoints.ToArray(), Degree); + controlPointPath.Vertices = controlPoints; + controlPointViz.Clear(); + + foreach (var cp in controlPoints) + { + controlPointViz.Add(new Box + { + Origin = Anchor.Centre, + Size = new Vector2(10), + Position = cp, + Colour = Color4.LightGreen, + }); + } } } } From 2d053c3dbea105998e2bf3adbf0f94f4af365d4b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 26 Nov 2023 01:30:28 +0100 Subject: [PATCH 07/26] Add minimum number of test points --- osu.Framework/Utils/PathApproximator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 9387f57592..99fe8ead6c 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -308,6 +308,7 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP int interpolatorResolution = 100, List? initialControlPoints = null) { + numTestPoints = Math.Max(numTestPoints, 3); return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); } @@ -323,6 +324,7 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input List? initialControlPoints = null) { degree = Math.Min(degree, numControlPoints - 1); + numTestPoints = Math.Max(numTestPoints, 3); return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); } From 6b2214ae77e093598443271da9fcbbe3fbdc0a4d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 26 Nov 2023 01:31:15 +0100 Subject: [PATCH 08/26] cache previous results and iterative optimization --- .../Utils/IncrementalBSplineBuilder.cs | 130 +++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 0c9eb3dcae..75dbd35464 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -79,6 +79,8 @@ private static float getAbsWindingAt(List path, List cumulativeD Value = new List>() }; + private bool controlPointsPartiallyInvalid; + private int degree; /// @@ -172,6 +174,9 @@ public IReadOnlyList> ControlPoints if (!controlPoints.IsValid) regenerateApproximatedPathControlPoints(); + if (controlPointsPartiallyInvalid) + updateApproximatedPathControlPoints(); + return controlPoints.Value; } } @@ -272,6 +277,120 @@ private List detectCorners(List vertices, List distances) return cornerT; } + private void updateApproximatedPathControlPoints() + { + if (!controlPoints.IsValid) + { + regenerateApproximatedPathControlPoints(); + return; + } + + var (vertices, distances) = computeSmoothedInputPath(); + + if (vertices.Count < 2) + { + controlPoints.Value = new List> { vertices }; + return; + } + + Debug.Assert(vertices.Count == distances.Count + 1); + var cornerTs = detectCorners(vertices, distances); + + var cpss = controlPoints.Value; + + // Make sure there are enough segments. + while (cpss.Count < cornerTs.Count - 1) + cpss.Add(new List()); + + // Make sure each segment has at least two control points which are the start and end points. + for (int j = 0; j < cpss.Count; ++j) + { + var cps = cpss[j]; + + if (cps.Count == 0) + { + cps.Add(getPathAt(vertices, distances, cornerTs[j])); + cps.Add(getPathAt(vertices, distances, cornerTs[j + 1])); + } + else if (cps.Count == 1) + { + cps[0] = getPathAt(vertices, distances, cornerTs[j]); + cps.Add(getPathAt(vertices, distances, cornerTs[j + 1])); + } + else + { + cps[0] = getPathAt(vertices, distances, cornerTs[j]); + cps[^1] = getPathAt(vertices, distances, cornerTs[j + 1]); + } + } + + // Populate the last segment with control points that have density proportional to the + // product of Tolerance and curvature. + const float step_size = FD_EPSILON; + + int i = cornerTs.Count - 1; + float totalWinding = 0; + + float t0 = cornerTs[i - 1] + step_size * 2; + float t1 = cornerTs[i] - step_size * 2; + + Vector2 c0 = getPathAt(vertices, distances, cornerTs[i - 1]); + Vector2 c1 = getPathAt(vertices, distances, cornerTs[i]); + Line linearConnection = new Line(c0, c1); + + var tmp = new List(); + var segmentPath = new List(); + bool allOnLine = true; + float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); + + if (t1 > t0) + { + int nSteps = (int)((t1 - t0) / step_size); + float currentWinding = 0.5f * Tolerance; + + for (int j = 0; j < nSteps; ++j) + { + float t = t0 + j * step_size; + float winding = getAbsWindingAt(vertices, distances, t); + totalWinding += winding; + currentWinding += winding; + + Vector2 p = getPathAt(vertices, distances, t); + segmentPath.Add(p); + + if (currentWinding < Tolerance) continue; + + if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) + allOnLine = false; + + tmp.Add(p); + currentWinding -= Tolerance; + } + } + + if (!allOnLine) + { + // Make sure the last segment has enough control points. + var cps = cpss[^1]; + int toAdd = tmp.Count + 2 - cps.Count; + + for (int j = 0; j < toAdd; j++) + { + cps.Insert(cps.Count - 1, tmp[j - toAdd + tmp.Count]); + } + + if (cps.Count > 2 && cps.Count < 100) + { + int res = (int)(totalWinding * 10); + cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, + res, 20, 5f, interpolatorResolution: res, initialControlPoints: cps); + cpss[^1] = cps; + } + } + + controlPointsPartiallyInvalid = false; + } + private void regenerateApproximatedPathControlPoints() { // Approximating a given input path with a BSpline has three stages: @@ -283,7 +402,9 @@ private void regenerateApproximatedPathControlPoints() // of Tolerance and curvature. // 4. Additionally, we special case linear segments: if the path does not deviate more // than some threshold from a straight line, we do not add additional control points. - var (vertices, distances) = computeSmoothedInputPath(); + // var (vertices, distances) = computeSmoothedInputPath(); + var vertices = inputPath; + var distances = cumulativeInputPathLength; if (vertices.Count < 2) { @@ -354,11 +475,14 @@ private void regenerateApproximatedPathControlPoints() if (cps.Count > 2 && cps.Count < 100) { int res = (int)(totalWinding * 10); - cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, res, 100, 5, interpolatorResolution: res, initialControlPoints: cps); + cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, + res, 100, 5, interpolatorResolution: res, initialControlPoints: cps); } cpss.Add(cps); } + + controlPointsPartiallyInvalid = false; } private void redrawApproximatedPath() @@ -390,7 +514,7 @@ public void Clear() public void AddLinearPoint(Vector2 v) { outputCache.Invalidate(); - controlPoints.Invalidate(); + controlPointsPartiallyInvalid = true; // Implementation detail: we would like to disregard input path detail that is smaller than // FD_EPSILON * 2 because it can otherwise mess with the winding calculations. However, we From b7aa29d0e671d9766ecdfec84e4b65195f2b6a4d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 26 Nov 2023 01:31:26 +0100 Subject: [PATCH 09/26] basic pathify regularizing --- osu.Framework/Utils/PathApproximator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 99fe8ead6c..3e4d54f8a3 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -393,7 +393,7 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Update labels to shift the distance distribution between points if (step % 11 == 0) { - getDistanceDistribution(points, distanceDistribution); + getDistanceDistribution(points, distanceDistribution, 0.1f); interpolator.Interpolate(distanceDistribution, labels); } @@ -521,7 +521,7 @@ private static float[] linspace(float start, float end, int count) return result; } - private static void getDistanceDistribution(float[,] points, float[] result) + private static void getDistanceDistribution(float[,] points, float[] result, float regularizingFactor = 0f) { int m = points.GetLength(1); float accumulator = 0; @@ -530,7 +530,7 @@ private static void getDistanceDistribution(float[,] points, float[] result) for (int i = 1; i < m; i++) { float dist = MathF.Sqrt(MathF.Pow(points[0, i] - points[0, i - 1], 2) + MathF.Pow(points[1, i] - points[1, i - 1], 2)); - accumulator += dist; + accumulator += dist + regularizingFactor; result[i] = accumulator; } From 9840883cdd8a41d67dcb1ea576ceb3e5e3a18ee2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 Nov 2023 13:04:42 +0100 Subject: [PATCH 10/26] fix bad initialization ofsegment path --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 75dbd35464..6edee16ab2 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -339,7 +339,7 @@ private void updateApproximatedPathControlPoints() Line linearConnection = new Line(c0, c1); var tmp = new List(); - var segmentPath = new List(); + var segmentPath = new List { c0 }; bool allOnLine = true; float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); @@ -381,9 +381,11 @@ private void updateApproximatedPathControlPoints() if (cps.Count > 2 && cps.Count < 100) { + segmentPath.Add(c1); + int res = (int)(totalWinding * 10); cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, - res, 20, 5f, interpolatorResolution: res, initialControlPoints: cps); + res, 50, 5f, interpolatorResolution: res, initialControlPoints: cps); cpss[^1] = cps; } } @@ -435,7 +437,7 @@ private void regenerateApproximatedPathControlPoints() Line linearConnection = new Line(c0, c1); var cps = new List { c0 }; - var segmentPath = new List(); + var segmentPath = new List { c0 }; bool allOnLine = true; float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); @@ -471,6 +473,7 @@ private void regenerateApproximatedPathControlPoints() } cps.Add(c1); + segmentPath.Add(c1); if (cps.Count > 2 && cps.Count < 100) { From 9ddc29c0eebc1dc460cc48dec2bc89944a654a53 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 Nov 2023 14:40:14 +0100 Subject: [PATCH 11/26] lock previous control points in place --- .../Utils/IncrementalBSplineBuilder.cs | 10 ++++++- osu.Framework/Utils/PathApproximator.cs | 27 ++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 6edee16ab2..b2aa282ed8 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -383,9 +383,17 @@ private void updateApproximatedPathControlPoints() { segmentPath.Add(c1); + float[,] learnableMask = new float[2, cps.Count]; + + for (int j = Math.Max(1, cps.Count - degree * 2); j < cps.Count - 1; j++) + { + learnableMask[0, j] = 1; + learnableMask[1, j] = 1; + } + int res = (int)(totalWinding * 10); cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, - res, 50, 5f, interpolatorResolution: res, initialControlPoints: cps); + res, 50, 5f, interpolatorResolution: res, initialControlPoints: cps, learnableMask: learnableMask); cpss[^1] = cps; } } diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 3e4d54f8a3..9adfe8d6d9 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -306,10 +306,12 @@ public static List PiecewiseLinearToBezier(ReadOnlySpan inputP float b1 = 0.8f, float b2 = 0.99f, int interpolatorResolution = 100, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { numTestPoints = Math.Max(numTestPoints, 3); - return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); + return piecewiseLinearToSpline(inputPath, generateBezierWeights(numControlPoints, numTestPoints), + maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints, learnableMask); } public static List PiecewiseLinearToBSpline(ReadOnlySpan inputPath, @@ -321,11 +323,13 @@ public static List PiecewiseLinearToBSpline(ReadOnlySpan input float b1 = 0.8f, float b2 = 0.99f, int interpolatorResolution = 100, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { degree = Math.Min(degree, numControlPoints - 1); numTestPoints = Math.Max(numTestPoints, 3); - return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints); + return piecewiseLinearToSpline(inputPath, generateBSplineWeights(numControlPoints, numTestPoints, degree), + maxIterations, learningRate, b1, b2, interpolatorResolution, initialControlPoints, learnableMask); } private static List piecewiseLinearToSpline(ReadOnlySpan inputPath, @@ -335,7 +339,8 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input float b1 = 0.8f, float b2 = 0.99f, int interpolatorResolution = 100, - List? initialControlPoints = null) + List? initialControlPoints = null, + float[,]? learnableMask = null) { int numControlPoints = weights.GetLength(1); int numTestPoints = weights.GetLength(0); @@ -373,12 +378,16 @@ private static List piecewiseLinearToSpline(ReadOnlySpan input // Initialize Adam optimizer variables float[,] m = new float[2, numControlPoints]; float[,] v = new float[2, numControlPoints]; - float[,] learnableMask = new float[2, numControlPoints]; - for (int i = 1; i < numControlPoints - 1; i++) + if (learnableMask is null) { - learnableMask[0, i] = 1; - learnableMask[1, i] = 1; + learnableMask = new float[2, numControlPoints]; + + for (int i = 1; i < numControlPoints - 1; i++) + { + learnableMask[0, i] = 1; + learnableMask[1, i] = 1; + } } // Initialize intermediate variables From 6a9b5e92e629d6dafd9e2d43329f376c9b8579ee Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 28 Nov 2023 15:51:41 +0100 Subject: [PATCH 12/26] add comment --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index b2aa282ed8..452f8efff2 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -383,6 +383,8 @@ private void updateApproximatedPathControlPoints() { segmentPath.Add(c1); + // Make a mask to prevent modifying the control points which have already been optimized enough. + // Also the end-points can not move. float[,] learnableMask = new float[2, cps.Count]; for (int j = Math.Max(1, cps.Count - degree * 2); j < cps.Count - 1; j++) From 1256fe632b9bb225b304678e38f02875d6f06be5 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 14:43:37 +0100 Subject: [PATCH 13/26] code cleanup --- .../Utils/IncrementalBSplineBuilder.cs | 159 ++++++------------ 1 file changed, 54 insertions(+), 105 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 452f8efff2..6342a3e59f 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -305,40 +305,68 @@ private void updateApproximatedPathControlPoints() // Make sure each segment has at least two control points which are the start and end points. for (int j = 0; j < cpss.Count; ++j) { - var cps = cpss[j]; + var tmp = cpss[j]; - if (cps.Count == 0) - { - cps.Add(getPathAt(vertices, distances, cornerTs[j])); - cps.Add(getPathAt(vertices, distances, cornerTs[j + 1])); - } - else if (cps.Count == 1) - { - cps[0] = getPathAt(vertices, distances, cornerTs[j]); - cps.Add(getPathAt(vertices, distances, cornerTs[j + 1])); - } + if (tmp.Count >= 1) + tmp[0] = getPathAt(vertices, distances, cornerTs[j]); else + tmp.Add(getPathAt(vertices, distances, cornerTs[j])); + + if (tmp.Count >= 2) + tmp[^1] = getPathAt(vertices, distances, cornerTs[j + 1]); + else + tmp.Add(getPathAt(vertices, distances, cornerTs[j + 1])); + } + + // Initialize control points for the last segment + int i = cornerTs.Count - 1; + var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i - 1], cornerTs[i]); + + // Make sure the last segment has enough control points. + var lastSegment = cpss[^1]; + int toAdd = cps.Count - lastSegment.Count; + + for (int j = 0; j < toAdd; j++) + { + lastSegment.Insert(lastSegment.Count - 1, cps[j + cps.Count - toAdd - 1]); + } + + if (lastSegment.Count > 2 && lastSegment.Count < 100) + { + // Make a mask to prevent modifying the control points which have already been optimized enough. + // Also the end-points can not move. + float[,] learnableMask = new float[2, lastSegment.Count]; + + for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) { - cps[0] = getPathAt(vertices, distances, cornerTs[j]); - cps[^1] = getPathAt(vertices, distances, cornerTs[j + 1]); + learnableMask[0, j] = 1; + learnableMask[1, j] = 1; } + + int res = (int)(totalWinding * 10); + cpss[^1] = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), lastSegment.Count, degree, + res, 50, 5f, interpolatorResolution: res, initialControlPoints: lastSegment, learnableMask: learnableMask); } - // Populate the last segment with control points that have density proportional to the + controlPointsPartiallyInvalid = false; + } + + private (List, List, float) initializeSegment(List vertices, List distances, float t0, float t1) + { + // Populate each segment between corners with control points that have density proportional to the // product of Tolerance and curvature. const float step_size = FD_EPSILON; - int i = cornerTs.Count - 1; float totalWinding = 0; - float t0 = cornerTs[i - 1] + step_size * 2; - float t1 = cornerTs[i] - step_size * 2; - - Vector2 c0 = getPathAt(vertices, distances, cornerTs[i - 1]); - Vector2 c1 = getPathAt(vertices, distances, cornerTs[i]); + Vector2 c0 = getPathAt(vertices, distances, t0); + Vector2 c1 = getPathAt(vertices, distances, t1); Line linearConnection = new Line(c0, c1); - var tmp = new List(); + t0 += step_size * 2; + t1 -= step_size * 2; + + var cps = new List { c0 }; var segmentPath = new List { c0 }; bool allOnLine = true; float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); @@ -363,44 +391,15 @@ private void updateApproximatedPathControlPoints() if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) allOnLine = false; - tmp.Add(p); + cps.Add(p); currentWinding -= Tolerance; } } - if (!allOnLine) - { - // Make sure the last segment has enough control points. - var cps = cpss[^1]; - int toAdd = tmp.Count + 2 - cps.Count; + cps.Add(c1); + segmentPath.Add(c1); - for (int j = 0; j < toAdd; j++) - { - cps.Insert(cps.Count - 1, tmp[j - toAdd + tmp.Count]); - } - - if (cps.Count > 2 && cps.Count < 100) - { - segmentPath.Add(c1); - - // Make a mask to prevent modifying the control points which have already been optimized enough. - // Also the end-points can not move. - float[,] learnableMask = new float[2, cps.Count]; - - for (int j = Math.Max(1, cps.Count - degree * 2); j < cps.Count - 1; j++) - { - learnableMask[0, j] = 1; - learnableMask[1, j] = 1; - } - - int res = (int)(totalWinding * 10); - cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, - res, 50, 5f, interpolatorResolution: res, initialControlPoints: cps, learnableMask: learnableMask); - cpss[^1] = cps; - } - } - - controlPointsPartiallyInvalid = false; + return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); } private void regenerateApproximatedPathControlPoints() @@ -431,59 +430,9 @@ private void regenerateApproximatedPathControlPoints() var cpss = controlPoints.Value; - // Populate each segment between corners with control points that have density proportional to the - // product of Tolerance and curvature. - const float step_size = FD_EPSILON; - for (int i = 1; i < cornerTs.Count; ++i) { - float totalWinding = 0; - - float t0 = cornerTs[i - 1] + step_size * 2; - float t1 = cornerTs[i] - step_size * 2; - - Vector2 c0 = getPathAt(vertices, distances, cornerTs[i - 1]); - Vector2 c1 = getPathAt(vertices, distances, cornerTs[i]); - Line linearConnection = new Line(c0, c1); - - var cps = new List { c0 }; - var segmentPath = new List { c0 }; - bool allOnLine = true; - float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); - - if (t1 > t0) - { - int nSteps = (int)((t1 - t0) / step_size); - float currentWinding = 0.5f * Tolerance; - - for (int j = 0; j < nSteps; ++j) - { - float t = t0 + j * step_size; - float winding = getAbsWindingAt(vertices, distances, t); - totalWinding += winding; - currentWinding += winding; - - Vector2 p = getPathAt(vertices, distances, t); - segmentPath.Add(p); - - if (currentWinding < Tolerance) continue; - - if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) - allOnLine = false; - - cps.Add(p); - currentWinding -= Tolerance; - } - } - - if (allOnLine) - { - cpss.Add(new List { c0, c1 }); - continue; - } - - cps.Add(c1); - segmentPath.Add(c1); + var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i - 1], cornerTs[i]); if (cps.Count > 2 && cps.Count < 100) { From 148f899ff95dd5748dcbd0469e68f008bef8c117 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 14:45:05 +0100 Subject: [PATCH 14/26] move method up --- .../Utils/IncrementalBSplineBuilder.cs | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 6342a3e59f..a67c07d9bd 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -277,6 +277,57 @@ private List detectCorners(List vertices, List distances) return cornerT; } + private (List, List, float) initializeSegment(List vertices, List distances, float t0, float t1) + { + // Populate each segment between corners with control points that have density proportional to the + // product of Tolerance and curvature. + const float step_size = FD_EPSILON; + + float totalWinding = 0; + + Vector2 c0 = getPathAt(vertices, distances, t0); + Vector2 c1 = getPathAt(vertices, distances, t1); + Line linearConnection = new Line(c0, c1); + + t0 += step_size * 2; + t1 -= step_size * 2; + + var cps = new List { c0 }; + var segmentPath = new List { c0 }; + bool allOnLine = true; + float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); + + if (t1 > t0) + { + int nSteps = (int)((t1 - t0) / step_size); + float currentWinding = 0.5f * Tolerance; + + for (int j = 0; j < nSteps; ++j) + { + float t = t0 + j * step_size; + float winding = getAbsWindingAt(vertices, distances, t); + totalWinding += winding; + currentWinding += winding; + + Vector2 p = getPathAt(vertices, distances, t); + segmentPath.Add(p); + + if (currentWinding < Tolerance) continue; + + if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) + allOnLine = false; + + cps.Add(p); + currentWinding -= Tolerance; + } + } + + cps.Add(c1); + segmentPath.Add(c1); + + return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); + } + private void updateApproximatedPathControlPoints() { if (!controlPoints.IsValid) @@ -351,57 +402,6 @@ private void updateApproximatedPathControlPoints() controlPointsPartiallyInvalid = false; } - private (List, List, float) initializeSegment(List vertices, List distances, float t0, float t1) - { - // Populate each segment between corners with control points that have density proportional to the - // product of Tolerance and curvature. - const float step_size = FD_EPSILON; - - float totalWinding = 0; - - Vector2 c0 = getPathAt(vertices, distances, t0); - Vector2 c1 = getPathAt(vertices, distances, t1); - Line linearConnection = new Line(c0, c1); - - t0 += step_size * 2; - t1 -= step_size * 2; - - var cps = new List { c0 }; - var segmentPath = new List { c0 }; - bool allOnLine = true; - float onLineThreshold = 0.02f * Tolerance * Vector2.Distance(c0, c1); - - if (t1 > t0) - { - int nSteps = (int)((t1 - t0) / step_size); - float currentWinding = 0.5f * Tolerance; - - for (int j = 0; j < nSteps; ++j) - { - float t = t0 + j * step_size; - float winding = getAbsWindingAt(vertices, distances, t); - totalWinding += winding; - currentWinding += winding; - - Vector2 p = getPathAt(vertices, distances, t); - segmentPath.Add(p); - - if (currentWinding < Tolerance) continue; - - if (linearConnection.DistanceSquaredToPoint(p) > onLineThreshold * onLineThreshold) - allOnLine = false; - - cps.Add(p); - currentWinding -= Tolerance; - } - } - - cps.Add(c1); - segmentPath.Add(c1); - - return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); - } - private void regenerateApproximatedPathControlPoints() { // Approximating a given input path with a BSpline has three stages: From ebe253a35c9ef4b788c36a1db5fa928be3131d6a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 15:14:10 +0100 Subject: [PATCH 15/26] fix corners --- .../Utils/IncrementalBSplineBuilder.cs | 114 ++++++++++-------- 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index a67c07d9bd..0dbb263d02 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -328,6 +328,59 @@ private List detectCorners(List vertices, List distances) return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); } + private void updateLastSegment(List vertices, List distances, List cornerTs, List> segments) + { + if (segments.Count == 0) return; + + // Initialize control points for the last segment + int i = segments.Count - 1; + var lastSegment = segments[i]; + var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i], cornerTs[i + 1]); + + // Make sure the last segment has the correct end-points + if (lastSegment.Count >= 1) + lastSegment[0] = cps[0]; + else + lastSegment.Add(cps[0]); + if (lastSegment.Count >= 2) + lastSegment[^1] = cps[^1]; + else + lastSegment.Add(cps[^1]); + + // Make sure the last segment has the correct number of control points. + if (cps.Count > lastSegment.Count) + { + int toAdd = cps.Count - lastSegment.Count; + + for (int j = 0; j < toAdd; j++) + { + lastSegment.Insert(lastSegment.Count - 1, cps[j + cps.Count - toAdd - 1]); + } + } + else if (cps.Count < lastSegment.Count) + { + int toRemove = lastSegment.Count - cps.Count; + lastSegment.RemoveRange(lastSegment.Count - toRemove - 1, toRemove); + } + + // Optimize the control point placement + if (lastSegment.Count is <= 2 or >= 100) return; + + // Make a mask to prevent modifying the control points which have already been optimized enough. + // Also the end-points can not move. + float[,] learnableMask = new float[2, lastSegment.Count]; + + for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) + { + learnableMask[0, j] = 1; + learnableMask[1, j] = 1; + } + + int res = (int)(totalWinding * 10); + segments[^1] = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), lastSegment.Count, degree, + res, 50, 5f, interpolatorResolution: res, initialControlPoints: lastSegment, learnableMask: learnableMask); + } + private void updateApproximatedPathControlPoints() { if (!controlPoints.IsValid) @@ -347,57 +400,18 @@ private void updateApproximatedPathControlPoints() Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cpss = controlPoints.Value; + var segments = controlPoints.Value; // Make sure there are enough segments. - while (cpss.Count < cornerTs.Count - 1) - cpss.Add(new List()); - - // Make sure each segment has at least two control points which are the start and end points. - for (int j = 0; j < cpss.Count; ++j) + while (segments.Count < cornerTs.Count - 1) { - var tmp = cpss[j]; - - if (tmp.Count >= 1) - tmp[0] = getPathAt(vertices, distances, cornerTs[j]); - else - tmp.Add(getPathAt(vertices, distances, cornerTs[j])); - - if (tmp.Count >= 2) - tmp[^1] = getPathAt(vertices, distances, cornerTs[j + 1]); - else - tmp.Add(getPathAt(vertices, distances, cornerTs[j + 1])); + // The previous segment may have been shortened by the addition of a corner. + // We have to remove the extra control points and re-optimize the path. + updateLastSegment(vertices, distances, cornerTs, segments); + segments.Add(new List()); } - // Initialize control points for the last segment - int i = cornerTs.Count - 1; - var (cps, segmentPath, totalWinding) = initializeSegment(vertices, distances, cornerTs[i - 1], cornerTs[i]); - - // Make sure the last segment has enough control points. - var lastSegment = cpss[^1]; - int toAdd = cps.Count - lastSegment.Count; - - for (int j = 0; j < toAdd; j++) - { - lastSegment.Insert(lastSegment.Count - 1, cps[j + cps.Count - toAdd - 1]); - } - - if (lastSegment.Count > 2 && lastSegment.Count < 100) - { - // Make a mask to prevent modifying the control points which have already been optimized enough. - // Also the end-points can not move. - float[,] learnableMask = new float[2, lastSegment.Count]; - - for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) - { - learnableMask[0, j] = 1; - learnableMask[1, j] = 1; - } - - int res = (int)(totalWinding * 10); - cpss[^1] = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), lastSegment.Count, degree, - res, 50, 5f, interpolatorResolution: res, initialControlPoints: lastSegment, learnableMask: learnableMask); - } + updateLastSegment(vertices, distances, cornerTs, segments); controlPointsPartiallyInvalid = false; } @@ -413,9 +427,7 @@ private void regenerateApproximatedPathControlPoints() // of Tolerance and curvature. // 4. Additionally, we special case linear segments: if the path does not deviate more // than some threshold from a straight line, we do not add additional control points. - // var (vertices, distances) = computeSmoothedInputPath(); - var vertices = inputPath; - var distances = cumulativeInputPathLength; + var (vertices, distances) = computeSmoothedInputPath(); if (vertices.Count < 2) { @@ -428,7 +440,7 @@ private void regenerateApproximatedPathControlPoints() Debug.Assert(vertices.Count == distances.Count + 1); var cornerTs = detectCorners(vertices, distances); - var cpss = controlPoints.Value; + var segments = controlPoints.Value; for (int i = 1; i < cornerTs.Count; ++i) { @@ -441,7 +453,7 @@ private void regenerateApproximatedPathControlPoints() res, 100, 5, interpolatorResolution: res, initialControlPoints: cps); } - cpss.Add(cps); + segments.Add(cps); } controlPointsPartiallyInvalid = false; From c3fce6c101cbd180b9b6465b1fd68eeedc73d81d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 15:35:01 +0100 Subject: [PATCH 16/26] improve post optimization --- .../Utils/IncrementalBSplineBuilder.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 0dbb263d02..37486ebdd8 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -328,7 +328,7 @@ private List detectCorners(List vertices, List distances) return allOnLine ? (new List { c0, c1 }, segmentPath, totalWinding) : (cps, segmentPath, totalWinding); } - private void updateLastSegment(List vertices, List distances, List cornerTs, List> segments) + private void updateLastSegment(List vertices, List distances, List cornerTs, List> segments, int iterations, bool mask) { if (segments.Count == 0) return; @@ -368,17 +368,22 @@ private void updateLastSegment(List vertices, List distances, Li // Make a mask to prevent modifying the control points which have already been optimized enough. // Also the end-points can not move. - float[,] learnableMask = new float[2, lastSegment.Count]; + float[,]? learnableMask = null; - for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) + if (mask) { - learnableMask[0, j] = 1; - learnableMask[1, j] = 1; + learnableMask = new float[2, lastSegment.Count]; + + for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) + { + learnableMask[0, j] = 1; + learnableMask[1, j] = 1; + } } int res = (int)(totalWinding * 10); segments[^1] = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), lastSegment.Count, degree, - res, 50, 5f, interpolatorResolution: res, initialControlPoints: lastSegment, learnableMask: learnableMask); + res, iterations, 4f, interpolatorResolution: res, initialControlPoints: lastSegment, learnableMask: learnableMask); } private void updateApproximatedPathControlPoints() @@ -407,11 +412,11 @@ private void updateApproximatedPathControlPoints() { // The previous segment may have been shortened by the addition of a corner. // We have to remove the extra control points and re-optimize the path. - updateLastSegment(vertices, distances, cornerTs, segments); + updateLastSegment(vertices, distances, cornerTs, segments, 100, false); segments.Add(new List()); } - updateLastSegment(vertices, distances, cornerTs, segments); + updateLastSegment(vertices, distances, cornerTs, segments, 10, true); controlPointsPartiallyInvalid = false; } From a0ae06d487d3ae435ef1c26f8857829b769dff70 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 15:45:51 +0100 Subject: [PATCH 17/26] Add Finish method for post-processing --- .../Drawables/TestSceneInteractivePathDrawing.cs | 8 ++++++++ osu.Framework/Utils/IncrementalBSplineBuilder.cs | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index c71b853080..9a5ae41246 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -121,5 +121,13 @@ protected override void OnDrag(DragEvent e) { bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition))); } + + protected override void OnDragEnd(DragEndEvent e) + { + if (e.Button == MouseButton.Left) + bSplineBuilder.Finish(); + + base.OnDragEnd(e); + } } } diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 37486ebdd8..52a7644e16 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -455,7 +455,7 @@ private void regenerateApproximatedPathControlPoints() { int res = (int)(totalWinding * 10); cps = PathApproximator.PiecewiseLinearToBSpline(segmentPath.ToArray(), cps.Count, degree, - res, 100, 5, interpolatorResolution: res, initialControlPoints: cps); + res, 200, 5, interpolatorResolution: res, initialControlPoints: cps); } segments.Add(cps); @@ -521,5 +521,18 @@ public void AddLinearPoint(Vector2 v) inputPath.Add(v); cumulativeInputPathLength.Add(cumulativeInputPathLength[^1]); } + + /// + /// Call this when you are done building the path. + /// This method applies the final step of post-processing. + /// + public void Finish() + { + if (!controlPoints.IsValid) return; + + var (vertices, distances) = computeSmoothedInputPath(); + var cornerTs = detectCorners(vertices, distances); + updateLastSegment(vertices, distances, cornerTs, controlPoints.Value, 100, false); + } } } From 61a7e20d969268f069ff7dba92e0997cdf0f381c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 16:39:21 +0100 Subject: [PATCH 18/26] fix output cache not being invalidated on finish --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 52a7644e16..1baef5254d 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -533,6 +533,8 @@ public void Finish() var (vertices, distances) = computeSmoothedInputPath(); var cornerTs = detectCorners(vertices, distances); updateLastSegment(vertices, distances, cornerTs, controlPoints.Value, 100, false); + + outputCache.Invalidate(); } } } From 86499b09f3f0f86c3e9f664b4be93d341f4247d4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 16:39:28 +0100 Subject: [PATCH 19/26] update default parameters in test --- .../Visual/Drawables/TestSceneInteractivePathDrawing.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index 9a5ae41246..ea688a65dc 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -63,11 +63,11 @@ public TestSceneInteractivePathDrawing() bSplineBuilder.Clear(); }); - AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 5, 3, v => + AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 4, 3, v => { bSplineBuilder.Degree = v; }); - AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 1.5f, v => + AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 2f, v => { bSplineBuilder.Tolerance = v; }); From 0bd8b41e60dadcb102fc309eb815fea3eb859306 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 29 Nov 2023 16:51:19 +0100 Subject: [PATCH 20/26] fix cache pattern --- .../Utils/IncrementalBSplineBuilder.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 1baef5254d..716dc684f3 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -81,6 +81,8 @@ private static float getAbsWindingAt(List path, List cumulativeD private bool controlPointsPartiallyInvalid; + private bool shouldFinishLastSegment; + private int degree; /// @@ -416,7 +418,10 @@ private void updateApproximatedPathControlPoints() segments.Add(new List()); } - updateLastSegment(vertices, distances, cornerTs, segments, 10, true); + if (shouldFinishLastSegment) + updateLastSegment(vertices, distances, cornerTs, segments, 100, false); + else + updateLastSegment(vertices, distances, cornerTs, segments, 10, true); controlPointsPartiallyInvalid = false; } @@ -484,6 +489,9 @@ public void Clear() controlPoints.Value = new List>(); outputCache.Value = new List(); + + controlPointsPartiallyInvalid = false; + shouldFinishLastSegment = false; } /// @@ -528,13 +536,9 @@ public void AddLinearPoint(Vector2 v) /// public void Finish() { - if (!controlPoints.IsValid) return; - - var (vertices, distances) = computeSmoothedInputPath(); - var cornerTs = detectCorners(vertices, distances); - updateLastSegment(vertices, distances, cornerTs, controlPoints.Value, 100, false); - outputCache.Invalidate(); + controlPointsPartiallyInvalid = true; + shouldFinishLastSegment = true; } } } From dde2d7ff9426a4e7c8e9e390dce76fee828f016d Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 6 Dec 2023 18:19:55 +0100 Subject: [PATCH 21/26] prevent potential index out of range exception if too many segments --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 716dc684f3..29f652fa41 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -332,7 +332,7 @@ private List detectCorners(List vertices, List distances) private void updateLastSegment(List vertices, List distances, List cornerTs, List> segments, int iterations, bool mask) { - if (segments.Count == 0) return; + if (segments.Count == 0 || segments.Count >= cornerTs.Count) return; // Initialize control points for the last segment int i = segments.Count - 1; From 8986a8d366ea492131f1cfa57ac304424cc08273 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 7 Dec 2023 13:42:35 +0100 Subject: [PATCH 22/26] fix edge case where cornerTs decreases count --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index efe7c639ff..44ab153b3d 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -409,6 +409,14 @@ private void updateApproximatedPathControlPoints() var segments = controlPoints.Value; + // In some extremely rare cases there can be less corners than on the previous frame, + // so we have to remove the last segments if that is the case. + if (segments.Count >= cornerTs.Count) + { + int toRemove = segments.Count + 1 - cornerTs.Count; + segments.RemoveRange(segments.Count - toRemove, toRemove); + } + // Make sure there are enough segments. while (segments.Count < cornerTs.Count - 1) { From 62d54e9b0d097c52623fb09d4de8de195de7cae1 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 7 Dec 2023 14:08:05 +0100 Subject: [PATCH 23/26] Improve comment about learnableMask --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 44ab153b3d..3cc746bd0d 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -368,14 +368,18 @@ private void updateLastSegment(List vertices, List distances, Li // Optimize the control point placement if (lastSegment.Count is <= 2 or >= 100) return; - // Make a mask to prevent modifying the control points which have already been optimized enough. - // Also the end-points can not move. float[,]? learnableMask = null; if (mask) { + // When live-drawing, only the end of the segment gets extended in which case we use this mask to make updates less wobbly. + // Make a mask to prevent modifying the control points which have barely any impact on the end of the segment. + // Also the end-points can not move. learnableMask = new float[2, lastSegment.Count]; + // Only the 2 * degree last control points are not fixed in place. + // This number was chosen because manual testing showed that control points outside this range barely get moved + // by the optimization when the end of the segment gets extended. for (int j = Math.Max(1, lastSegment.Count - degree * 2); j < lastSegment.Count - 1; j++) { learnableMask[0, j] = 1; From 0e3ad2946b5b00d1cbcc800cfddd839a7bf26d8b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 7 Dec 2023 14:08:16 +0100 Subject: [PATCH 24/26] dont use 'or' keyword --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 3cc746bd0d..8adf324de6 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -366,7 +366,7 @@ private void updateLastSegment(List vertices, List distances, Li } // Optimize the control point placement - if (lastSegment.Count is <= 2 or >= 100) return; + if (lastSegment.Count <= 2 || lastSegment.Count >= 100) return; float[,]? learnableMask = null; From bfb75b43c1d3c542ec97b6f97ea92577907e0826 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 7 Dec 2023 14:19:32 +0100 Subject: [PATCH 25/26] add comment in Finish() --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 8adf324de6..ae5c957be9 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -548,6 +548,8 @@ public void AddLinearPoint(Vector2 v) /// public void Finish() { + // Do additional optimization steps on the entire last segment. + // This improves results after drawing, so the performance stays fast and control points dont wobble too much while drawing. outputCache.Invalidate(); controlPointsPartiallyInvalid = true; shouldFinishLastSegment = true; From e7c7397291c8cea889e1cd10e237a101e81808b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 7 Dec 2023 20:53:04 +0100 Subject: [PATCH 26/26] Rename some members for understanding --- .../Utils/IncrementalBSplineBuilder.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index ae5c957be9..ad773bc69d 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -79,9 +79,9 @@ private static float getAbsWindingAt(List path, List cumulativeD Value = new List>() }; - private bool controlPointsPartiallyInvalid; + private bool shouldOptimiseLastSegment; - private bool shouldFinishLastSegment; + private bool finishedDrawing; private int degree; @@ -174,10 +174,10 @@ public IReadOnlyList> ControlPoints get { if (!controlPoints.IsValid) - regenerateApproximatedPathControlPoints(); + regenerateFullApproximatedPath(); - if (controlPointsPartiallyInvalid) - updateApproximatedPathControlPoints(); + if (shouldOptimiseLastSegment) + regenerateLastApproximatedSegment(); return controlPoints.Value; } @@ -392,11 +392,11 @@ private void updateLastSegment(List vertices, List distances, Li res, iterations, 4f, initialControlPoints: lastSegment, learnableMask: learnableMask); } - private void updateApproximatedPathControlPoints() + private void regenerateLastApproximatedSegment() { if (!controlPoints.IsValid) { - regenerateApproximatedPathControlPoints(); + regenerateFullApproximatedPath(); return; } @@ -430,15 +430,15 @@ private void updateApproximatedPathControlPoints() segments.Add(new List()); } - if (shouldFinishLastSegment) + if (finishedDrawing) updateLastSegment(vertices, distances, cornerTs, segments, 100, false); else updateLastSegment(vertices, distances, cornerTs, segments, 10, true); - controlPointsPartiallyInvalid = false; + shouldOptimiseLastSegment = false; } - private void regenerateApproximatedPathControlPoints() + private void regenerateFullApproximatedPath() { // Approximating a given input path with a BSpline has three stages: // 1. Fit a dense-ish BSpline (with one control point in FdEpsilon-sized intervals) to the input path. @@ -478,7 +478,7 @@ private void regenerateApproximatedPathControlPoints() segments.Add(cps); } - controlPointsPartiallyInvalid = false; + shouldOptimiseLastSegment = false; } private void redrawApproximatedPath() @@ -502,8 +502,8 @@ public void Clear() controlPoints.Value = new List>(); outputCache.Value = new List(); - controlPointsPartiallyInvalid = false; - shouldFinishLastSegment = false; + shouldOptimiseLastSegment = false; + finishedDrawing = false; } /// @@ -513,7 +513,7 @@ public void Clear() public void AddLinearPoint(Vector2 v) { outputCache.Invalidate(); - controlPointsPartiallyInvalid = true; + shouldOptimiseLastSegment = true; // Implementation detail: we would like to disregard input path detail that is smaller than // FD_EPSILON * 2 because it can otherwise mess with the winding calculations. However, we @@ -551,8 +551,8 @@ public void Finish() // Do additional optimization steps on the entire last segment. // This improves results after drawing, so the performance stays fast and control points dont wobble too much while drawing. outputCache.Invalidate(); - controlPointsPartiallyInvalid = true; - shouldFinishLastSegment = true; + shouldOptimiseLastSegment = true; + finishedDrawing = true; } } }