Skip to content

Commit

Permalink
MAUI rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
domoszlai committed Nov 11, 2022
0 parents commit 03e5ff4
Show file tree
Hide file tree
Showing 47 changed files with 2,564 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vs/
obj/
bin/
BiArcTutorial.sln
180 changes: 180 additions & 0 deletions Algorithm.cs
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;
}

}
}
6 changes: 6 additions & 0 deletions App.config
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>
15 changes: 15 additions & 0 deletions App.xaml
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>

14 changes: 14 additions & 0 deletions App.xaml.cs
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();
}
}

72 changes: 72 additions & 0 deletions Arc.cs
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); }
}
}
}
94 changes: 94 additions & 0 deletions BiArc.cs
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; }
}
}
}
Loading

0 comments on commit 03e5ff4

Please sign in to comment.