-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 03e5ff4
Showing
47 changed files
with
2,564 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.vs/ | ||
obj/ | ||
bin/ | ||
BiArcTutorial.sln |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
using System; | ||
using System.Numerics; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
|
||
namespace BiArcTutorial | ||
{ | ||
public class Algorithm | ||
{ | ||
|
||
/// <summary> | ||
/// Algorithm to approximate a bezier curve with biarcs | ||
/// Based on: M.A. Sabin, The use of piecewise forms for the numerical representation of shape (1977) | ||
/// </summary> | ||
/// <param name="bezier">The bezier curve to be approximated.</param> | ||
/// <param name="nrPointsToCheck">The number of points used for calculating the approximation error.</param> | ||
/// <param name="tolerance">The approximation is accepted if the maximum devation at the sampling points is smaller than this number.</param> | ||
/// <returns></returns> | ||
public static List<BiArc> ApproxCubicBezier(CubicBezier bezier, int nrPointsToCheck, float tolerance) | ||
{ | ||
// The result will be put here | ||
List<BiArc> biarcs = new List<BiArc>(); | ||
|
||
// The bezier curves to approximate | ||
var curves = new Stack<CubicBezier>(); | ||
curves.Push(bezier); | ||
|
||
// --------------------------------------------------------------------------- | ||
// First, calculate the inflexion points and split the bezier at them (if any) | ||
|
||
var toSplit = curves.Pop(); | ||
|
||
// Edge case: P1 == P2 -> Split bezier | ||
if (bezier.P1 == bezier.P2) | ||
{ | ||
var bs = bezier.Split(0.5f); | ||
curves.Push(bs.Item2); | ||
curves.Push(bs.Item1); | ||
} | ||
// Edge case -> no inflexion points | ||
else if (toSplit.P1 == toSplit.C1 || toSplit.P2 == toSplit.C2) | ||
{ | ||
curves.Push(toSplit); | ||
} | ||
else | ||
{ | ||
var inflex = toSplit.InflexionPoints; | ||
|
||
var i1 = inflex.Count > 0; | ||
var i2 = inflex.Count > 1; | ||
|
||
if (i1 && !i2) | ||
{ | ||
var splited = toSplit.Split((float)inflex[0].Real); | ||
curves.Push(splited.Item2); | ||
curves.Push(splited.Item1); | ||
} | ||
else if (!i1 && i2) | ||
{ | ||
var splited = toSplit.Split((float)inflex[1].Real); | ||
curves.Push(splited.Item2); | ||
curves.Push(splited.Item1); | ||
} | ||
else if (i1 && i2) | ||
{ | ||
var t1 = (float)inflex[0].Real; | ||
var t2 = (float)inflex[1].Real; | ||
|
||
// I'm not sure if I need, but it does not hurt to order them | ||
if (t1 > t2) | ||
{ | ||
var tmp = t1; | ||
t1 = t2; | ||
t2 = tmp; | ||
} | ||
|
||
// Make the first split and save the first new curve. The second one has to be splitted again | ||
// at the recalculated t2 (it is on a new curve) | ||
|
||
var splited1 = toSplit.Split(t1); | ||
|
||
t2 = (1 - t1) * t2; | ||
|
||
toSplit = splited1.Item2; | ||
var splited2 = toSplit.Split(t2); | ||
|
||
curves.Push(splited2.Item2); | ||
curves.Push(splited2.Item1); | ||
curves.Push(splited1.Item1); | ||
} | ||
else | ||
{ | ||
curves.Push(toSplit); | ||
} | ||
} | ||
|
||
// --------------------------------------------------------------------------- | ||
// Second, approximate the curves until we run out of them | ||
|
||
while (curves.Count > 0) | ||
{ | ||
bezier = curves.Pop(); | ||
|
||
// --------------------------------------------------------------------------- | ||
// Calculate the transition point for the BiArc | ||
|
||
// V: Intersection point of tangent lines | ||
var C1 = bezier.P1 == bezier.C1 ? bezier.C2 : bezier.C1; | ||
var C2 = bezier.P2 == bezier.C2 ? bezier.C1 : bezier.C2; | ||
|
||
var T1 = new Line(bezier.P1, C1); | ||
var T2 = new Line(bezier.P2, C2); | ||
|
||
// Edge case: control lines are parallel | ||
if(T1.m == T2.m) | ||
{ | ||
var bs = bezier.Split(0.5f); | ||
curves.Push(bs.Item2); | ||
curves.Push(bs.Item1); | ||
continue; | ||
} | ||
|
||
var V = T1.Intersection(T2); | ||
|
||
// G: incenter point of the triangle (P1, V, P2) | ||
// http://www.mathopenref.com/coordincenter.html | ||
var dP2V = Vector2.Distance(bezier.P2, V); | ||
var dP1V = Vector2.Distance(bezier.P1, V); | ||
var dP1P2 = Vector2.Distance(bezier.P1, bezier.P2); | ||
var G = (dP2V * bezier.P1 + dP1V * bezier.P2 + dP1P2 * V) / (dP2V + dP1V + dP1P2); | ||
|
||
// --------------------------------------------------------------------------- | ||
// Calculate the BiArc | ||
|
||
BiArc biarc = new BiArc(bezier.P1, (bezier.P1 - C1), bezier.P2, (bezier.P2 - C2), G); | ||
|
||
// --------------------------------------------------------------------------- | ||
// Calculate the maximum error | ||
// TODO: D.J. Walton*, D.S. Meek, Approximation of a planar cubic B6zier spiral by circular arcs (1996) | ||
|
||
var maxDistance = 0f; | ||
var maxDistanceAt = 0f; | ||
|
||
var parameterStep = 1f / nrPointsToCheck; | ||
|
||
for (int i = 0; i <= nrPointsToCheck; i++) | ||
{ | ||
var t = parameterStep * i; | ||
var u1 = biarc.PointAt(t); | ||
var u2 = bezier.PointAt(t); | ||
var distance = (u1 - u2).Length(); | ||
|
||
if (distance > maxDistance) | ||
{ | ||
maxDistance = distance; | ||
maxDistanceAt = t; | ||
} | ||
} | ||
|
||
// Check if the two curves are close enough | ||
if (maxDistance > tolerance) | ||
{ | ||
// If not, split the bezier curve the point where the distance is the maximum | ||
// and try again with the two halfs | ||
var bs = bezier.Split(maxDistanceAt); | ||
curves.Push(bs.Item2); | ||
curves.Push(bs.Item1); | ||
} | ||
else | ||
{ | ||
// Otherwise we are done with the current bezier | ||
biarcs.Add(biarc); | ||
} | ||
} | ||
|
||
return biarcs; | ||
} | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<configuration> | ||
<startup> | ||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/> | ||
</startup> | ||
</configuration> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?xml version = "1.0" encoding = "UTF-8" ?> | ||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui" | ||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | ||
xmlns:local="clr-namespace:BiArcTutorial" | ||
x:Class="BiArcTutorial.App"> | ||
<Application.Resources> | ||
<ResourceDictionary> | ||
<ResourceDictionary.MergedDictionaries> | ||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" /> | ||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" /> | ||
</ResourceDictionary.MergedDictionaries> | ||
</ResourceDictionary> | ||
</Application.Resources> | ||
</Application> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
namespace BiArcTutorial; | ||
|
||
public partial class App : Application | ||
{ | ||
public App() | ||
{ | ||
InitializeComponent(); | ||
|
||
Application.Current.UserAppTheme = AppTheme.Light; | ||
|
||
MainPage = new MainPage(); | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
using System; | ||
using System.Numerics; | ||
|
||
namespace BiArcTutorial | ||
{ | ||
/// <summary> | ||
/// Definition of an Arc. It contains redundant information. | ||
/// Positive angle direction is clockwise as required by System.Drawing | ||
/// </summary> | ||
public struct Arc | ||
{ | ||
/// <summary> | ||
/// Center point | ||
/// </summary> | ||
public readonly Vector2 C; | ||
/// <summary> | ||
/// Radius | ||
/// </summary> | ||
public readonly float r; | ||
/// <summary> | ||
/// Start angle in radian | ||
/// </summary> | ||
public readonly float startAngle; | ||
/// <summary> | ||
/// Sweep angle in radian | ||
/// </summary> | ||
public readonly float sweepAngle; | ||
/// <summary> | ||
/// Start point of the arc | ||
/// </summary> | ||
public readonly Vector2 P1; | ||
/// <summary> | ||
/// End point of the arc | ||
/// </summary> | ||
public readonly Vector2 P2; | ||
|
||
public Arc(Vector2 C, float r, float startAngle, float sweepAngle, Vector2 P1, Vector2 P2) | ||
{ | ||
this.C = C; | ||
this.r = r; | ||
this.startAngle = startAngle; | ||
this.sweepAngle = sweepAngle; | ||
this.P1 = P1; | ||
this.P2 = P2; | ||
} | ||
|
||
/// <summary> | ||
/// Orientation of the arc. | ||
/// </summary> | ||
public bool IsClockwise | ||
{ | ||
get { return sweepAngle > 0; } | ||
} | ||
|
||
/// <summary> | ||
/// Implement the parametric equation. | ||
/// </summary> | ||
/// <param name="t">Parameter of the curve. Must be in [0,1]</param> | ||
/// <returns></returns> | ||
public Vector2 PointAt(float t) | ||
{ | ||
var x = C.X + r * Math.Cos(startAngle + t * sweepAngle); | ||
var y = C.Y + r * Math.Sin(startAngle + t * sweepAngle); | ||
return new Vector2((float)x, (float)y); | ||
} | ||
|
||
public float Length | ||
{ | ||
get { return r * Math.Abs(sweepAngle); } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System; | ||
using System.Numerics; | ||
|
||
namespace BiArcTutorial | ||
{ | ||
public struct BiArc | ||
{ | ||
public readonly Arc A1; | ||
public readonly Arc A2; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="P1">Start point</param> | ||
/// <param name="T1">Tangent vector at P1</param> | ||
/// <param name="P2">End point</param> | ||
/// <param name="T2">Tangent vector at P2</param> | ||
/// <param name="T">Transition point</param> | ||
public BiArc(Vector2 P1, Vector2 T1, Vector2 P2, Vector2 T2, Vector2 T) | ||
{ | ||
// Calculate the orientation | ||
// https://en.wikipedia.org/wiki/Curve_orientation | ||
|
||
var sum = 0d; | ||
sum += (T.X - P1.X) * (T.Y + P1.Y); | ||
sum += (P2.X - T.X) * (P2.Y + T.Y); | ||
sum += (P1.X - P2.X) * (P1.Y + P2.Y); | ||
var cw = sum < 0; | ||
|
||
// Calculate perpendicular lines to the tangent at P1 and P2 | ||
var tl1 = Line.CreatePerpendicularAt(P1, P1 + T1); | ||
var tl2 = Line.CreatePerpendicularAt(P2, P2 + T2); | ||
|
||
// Calculate the perpendicular bisector of P1T and P2T | ||
var P1T2 = (P1 + T) / 2; | ||
var pbP1T = Line.CreatePerpendicularAt(P1T2, T); | ||
|
||
var P2T2 = (P2 + T) / 2; | ||
var pbP2T = Line.CreatePerpendicularAt(P2T2, T); | ||
|
||
// The origo of the circles are at the intersection points | ||
var C1 = tl1.Intersection(pbP1T); | ||
var C2 = tl2.Intersection(pbP2T); | ||
|
||
// Calculate the radii | ||
var r1 = (C1 - P1).Length(); | ||
var r2 = (C2 - P2).Length(); | ||
|
||
// Calculate start and sweep angles | ||
var startVector1 = P1 - C1; | ||
var endVector1 = T - C1; | ||
var startAngle1 = Math.Atan2(startVector1.Y, startVector1.X); | ||
var sweepAngle1 = Math.Atan2(endVector1.Y, endVector1.X) - startAngle1; | ||
|
||
var startVector2 = T - C2; | ||
var endVector2 = P2 - C2; | ||
var startAngle2 = Math.Atan2(startVector2.Y, startVector2.X); | ||
var sweepAngle2 = Math.Atan2(endVector2.Y, endVector2.X) - startAngle2; | ||
|
||
// Adjust angles according to the orientation of the curve | ||
if (cw && sweepAngle1 < 0) sweepAngle1 = 2 * Math.PI + sweepAngle1; | ||
if (!cw && sweepAngle1 > 0) sweepAngle1 = sweepAngle1 - 2 * Math.PI; | ||
if (cw && sweepAngle2 < 0) sweepAngle2 = 2 * Math.PI + sweepAngle2; | ||
if (!cw && sweepAngle2 > 0) sweepAngle2 = sweepAngle2 - 2 * Math.PI; | ||
|
||
A1 = new Arc(C1, r1, (float)startAngle1, (float)sweepAngle1, P1, T); | ||
A2 = new Arc(C2, r2, (float)startAngle2, (float)sweepAngle2, T, P2); | ||
} | ||
|
||
/// <summary> | ||
/// Implement the parametric equation. | ||
/// </summary> | ||
/// <param name="t">Parameter of the curve. Must be in [0,1]</param> | ||
/// <returns></returns> | ||
public Vector2 PointAt(float t) | ||
{ | ||
var s = A1.Length / (A1.Length + A2.Length); | ||
|
||
if (t <= s) | ||
{ | ||
return A1.PointAt(t / s); | ||
} | ||
else | ||
{ | ||
return A2.PointAt((t - s) / (1 - s)); | ||
} | ||
} | ||
|
||
public float Length | ||
{ | ||
get { return A1.Length + A2.Length; } | ||
} | ||
} | ||
} |
Oops, something went wrong.