Monday, March 26, 2012

C# Spirograph Point Generators

Spirograph's  are cool.  See here and here.



I put together three ways to generate points for a Spirograph, first using a Brute Force straight generate the points, second using a Parallel.For and third using LINQ.



Two important classes first.  First is a static DoubleExtension class to help determine when two doubles are about equal:

[csharp]
using System;

namespace DC.SpiroGraph.Core
{
public static class DoubleExtension
{
public static bool AboutEqual(double x, double y)
{
double epsilon = Math.Max(Math.Abs(x), Math.Abs(y)) * 1E-15;

var variance = x > y ? x - y : y - x;
return Math.Abs(variance)
}
}
}
[/csharp]

Second, a simple Point class. Yeah, I probably could have used the Drawing Point class.

[csharp]
using System;
using System.Diagnostics;

namespace DC.SpiroGraph.Core
{
[DebuggerDisplay("X = {X}, Y = {Y}")]
public class Point
{
public double X { get; set; }
public double Y { get; set; }

public bool NotAboutEqualTo(Point point)
{
return !DoubleExtension.AboutEqual(X, point.X) && !DoubleExtension.AboutEqual(Y, point.Y);
}
}
}
[/csharp]

Note the use of DebuggerDisplay to help with the debugging.
The SpiroGraphGenerator:

[csharp]
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace DC.SpiroGraph.Core
{
public class SpiroGraphGenerator
{
/// <summary>
/// Radius, R, of Circle (equator) centered at the origin
/// </summary>
public double Radius1 { get; set; }

/// <summary>
/// Radius, r, of circle (bicyle wheel) cetered at (R + r, 0)
/// </summary>
public double Radius2 { get; set; }

/// <summary>
/// Distance of Point (reflector) from the center of Circle 2 (the circle of Radius2)
/// </summary>
public double Position { get; set; }

/// <summary>
/// Controls how precise the SpiroGraph is drawn.
/// Controls t of f(t).
/// Resolution = 360, t = 0, 1, 2, 3, ...,360
/// Resolution = 180, t = 0, 2, 4, 6, ... 360
///
/// </summary>
public double Resolution { get; set; }

public IEnumerable<Point> GetSpiroGraphPoints()
{

var t = 0d;
var sumOfRadius = Radius1 + Radius2;

var firstPoint = new Point { X = Xt(sumOfRadius, Radius2, Position, t), Y = Yt(sumOfRadius, Radius2, Position, t) };
var currentPoint = new Point { X = firstPoint.X, Y = firstPoint.Y };

// Convert to Radians
var increment = (360 / Resolution) * Math.PI / 180;

do
{
for (var i = 0; i < Resolution; i++)
{
yield return currentPoint;
t += increment;
currentPoint = new Point { X = Xt(sumOfRadius, Radius2, Position, t), Y = Yt(sumOfRadius, Radius2, Position, t) };
}
} while (currentPoint.NotAboutEqualTo(firstPoint));

yield return currentPoint;
}

public IEnumerable<Point> GetSpiroGraphPoints2()
{
var endPoints = FindAllEndPoints();
var numberOfEndPonits = endPoints.Count();
var numberOfPoints = Convert.ToInt32((numberOfEndPonits - 1)*Resolution + 1);
var points = new Point[numberOfPoints];
var resolution = Convert.ToInt32(Resolution);

var sumOfRadius = Radius1 + Radius2;

// Convert to Radians
var increment = (360/Resolution)*Math.PI/180;
points[0] = GetPoint(sumOfRadius, 0);

Parallel.For(1, numberOfEndPonits, i =>
{
for (var j = 0; j < resolution; j++)
{
var pointIndex = (i - 1)*resolution + j + 1;
var t = pointIndex*increment;
points[pointIndex] = GetPoint(sumOfRadius, t);
}
});
return points;
}

public IEnumerable<Point> GetSpiroGraphPoints3()
{
var endPoints = FindAllEndPoints();
var numberOfEndPonits = endPoints.Count();
var numberOfPoints = Convert.ToInt32((numberOfEndPonits - 1)*Resolution + 1);

var sumOfRadius = Radius1 + Radius2;

// Convert to Radians
var increment = (360/Resolution)*Math.PI/180;

var points2 = Enumerable.Range(0, numberOfPoints)
.Select(i => new {Index = i, t = i*increment})
.Select(it => new {it.Index, point = GetPoint(sumOfRadius, it.t)})
.OrderBy(ip => ip.Index)
.Select(ip => ip.point);

return points2;
}

public IEnumerable<Point> FindAllEndPoints()
{
var t = 0d;
var sumOfRadius = CalculateSumOfRadius();

var firstPoint = GetPoint(sumOfRadius, t);
var currentPoint = new Point { X = firstPoint.X, Y = firstPoint.Y };
// Convert to Radians
var increment = CalculateIncrement();

do
{
yield return currentPoint;
t += (increment*Resolution);
currentPoint = GetPoint(sumOfRadius, t);
} while (currentPoint.NotAboutEqualTo(firstPoint));

yield return currentPoint;
}

private double CalculateIncrement()
{
return (360/Resolution)*Math.PI/180;
}

private double CalculateSumOfRadius()
{
return Radius1 + Radius2;
}

private static double Xt(double sumOfRadius, double radius2, double position , double t)
{
return sumOfRadius * Math.Cos(t) + position * Math.Cos(sumOfRadius * t / radius2);
}

private static double Yt(double sumOfRadius, double radius2, double position, double t)
{
return sumOfRadius * Math.Sin(t) + position * Math.Sin(sumOfRadius * t / radius2);
}

private Point GetPoint(double sumOfRadius, double t)
{
return new Point { X = Xt(sumOfRadius, Radius2, Position, t), Y = Yt(sumOfRadius, Radius2, Position, t) };
}
}
}
[/csharp]

I created a simple WinForm app using DevExpress controls, this is how I populated the graph with points:

[csharp highlight="28,31"]
using System;
using System.Linq;
using System.Windows.Forms;

namespace DC.SpiroGraph.WinForm
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

public void CreateData(bool useParallel)
{
try
{
Cursor.Current = Cursors.WaitCursor;

var sg = new Core.SpiroGraphGenerator
{
Radius1 = Convert.ToDouble(radius1TextEdit.Text),
Radius2 = Convert.ToDouble(radius2TextEdit.Text),
Position = Convert.ToDouble(positionTextEdit.Text),
Resolution = Convert.ToDouble(resolutionTextEdit.Text)
};

var endPoints = sg.FindAllEndPoints();
endPointCountLabel.Text = string.Format("# of End Points: {0}", endPoints.Count());

var graphPoints = useParallel? sg.GetSpiroGraphPoints2() : sg.GetSpiroGraphPoints3();
pointCountLabel.Text = string.Format("# of Points: {0}", graphPoints.Count());

chartControl1.Series[0].DataSource = graphPoints;

}
catch (Exception)
{

// Eat It
}
finally
{
Cursor.Current = Cursors.Default;
}
}

private void Form1_Load(object sender, EventArgs e)
{
var series = chartControl1.Series[0];
series.Label.Visible = false;
series.ArgumentDataMember = "X";
series.ValueDataMembers.AddRange(new string[] {"Y"});

radius1TextEdit.Text = "60";
radius2TextEdit.Text = "60";
positionTextEdit.Text = "60";
resolutionTextEdit.Text = "270";

CreateData(true);
}

private void simpleButton1_Click(object sender, EventArgs e)
{
CreateData(true);
}

private void simpleButton2_Click(object sender, EventArgs e)
{
CreateData(false);
}
}
}
[/csharp]

Future changes will be to make a series for each rotation about the main circle, this way we can start adding different colors to the graph.

No comments: