Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Drawing Gears - Circular and Non Circular

4.98/5 (55 votes)
25 Feb 2016CPOL11 min read 74.5K   3.3K  
Learn about gears and by using the jpg's be able to cut working gears in wood and other materials

Sample Image - maximum width is 600 pixels

Introduction

DrawInvolute can be used to find best "Waste cut" for a specific gear
Explore the details of the involute used to form the gear tooth
Learn more about gears by using the program and looking into the source code
Get values to cut the gear blank (outside circle), base circle ...

To remove waste material in gear blanks (disc) before cutting the involute curve, using the method in:

Popular Science – November 1961 page 144-148
Make Wooden Gears? Sure, Here's how by Edwin W. Love, have been adapted

The waste cut marked with yellow is the best approximation to the involute curve.
Youtube: Making Wooden Gears with a Router shows another way of doing this.
The data files and the images saved can be used for illustrations and templates for cutting gears.
The Superellipse to make some fun looking non circular gears.

Background

I needed a couple of wooden gears for a small hobby project. Searching the internet, I found a lot of information about gears, but only a few snips of code on how to draw/make them but none using C#, so I started collecting information and made this program DrawInvolute.

To solve issues found using one method, more algorithms was added, after doing the circular gears, standard ellipse shaped gears were added and final superellipse non circular gears.

Using the Code

You can use the program as is to play with the gear parameters and see the result on screen.
Use the saved images as illustrations or as templates for gear cutting in wood and other materials.
If you want a better understanding of a subject, it's adviced to take a look at the source code.

Class Names

Adapted the algorithm MooreNeighborTracing from CPP to C# for my special need - only the inside shape - to clean up a drawing
Added LockBitMap to make it faster and an ArrayList to hold the polar representation of the points found.
This algorithm is called Moore Neighbor Tracing.

An explanation of the algorithm can be found here: http://www.thebigblob.com/moore-neighbor-tracing-algorithm-in-c by Erik Smistad

Detailed explanation can be found here: http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/moore.html

  • GearTools, used to convert rad2deg, inch2mm, pixel2mm and calculating involute, handle polarPoint, etc.
  • GearParams, used to hold and do basic gear parameter calculations.
  • LockBitmap, Work-with-bitmap-faster-with-Csharp Added support for 16 bit needed by this program
  • MooreNeighborTracing
  • Ellipse, functions to draw and calculate ellipse and super ellipse.
  • BiSection, an unused attempt to be used for finding the best center distance between too gears
  • DrawInvoluteTake2, the main class with functions to drawGear, DrawCenterX, drawMultipageAlignmentGrid, drawTooth, drawRackDrive, drawRackDriven, drawRawTooth, drawIndexMarkNumbers, drawPerfectGear, calcBestMatingGearCenterDistance, drawGearFromArray, fastDrawGearFromArray, drawXaxis, drawCicleMark, makeGearAnimationFromArrayLists...

Circular Gears

GearTools involute function used to draw the gear teeth:

C++
/// Calculate the involute for a given radius at a given angle adjusting the center to the offset
/// The pf is set to the result 
public void involute(bool leftsideoftooth, 
	double radius, double rad_angle, PointF offset, ref PointF pf) 
{ 
    pf.X = (float)(radius * (Math.Cos(rad_angle) + (rad_angle * Math.Sin(rad_angle)))); 
    pf.Y = ((leftsideoftooth == true) ? 1.0f : -1.0f) * (float)(radius * 
		(Math.Sin(rad_angle) - (rad_angle * Math.Cos(rad_angle)))); 
    pf.X += offset.X; pf.Y += offset.Y;
}

To rotate the tooth to its correct position on the gear, function rotatePoint is used:

C#
/// Rotate a point around it's offset 
public void rotatePoint(PointF offset, double angle, ref PointF p) 
{ 
    float s = (float)Math.Sin(angle); 
    float c = (float)Math.Cos(angle); 
    // translate point back to origin: 
    p.X -= offset.X; 
    p.Y -= offset.Y; 
    float xnew = p.X * c - p.Y * s; 
    float ynew = p.X * s + p.Y * c; 
    // translate point back:
     p.X = xnew + offset.X; 
     p.Y = ynew + offset.Y; 
} 

Drawing a tooth using the involute is done in two steps - first the left side of tooth, second the right side.
Gear parameters from the gearParms class is used here.

C#
...
// Draw toolpath left side of tooth
pp.Color = Color.Black;
pp.Width = 0.5f;
for (double angle = -(gp.angle_one_tooth / 4) - 
	gp.angle_pitch_tangent; angle < Math.PI * 2; angle += (2 * Math.PI / gp.number_of_teeth))
{
	alpha = 0;
	gt.involute(true, gp.base_radius, alpha, offset, ref from);
	gt.rotatePoint(offset, angle, ref from);
	to = new PointF(0f, 0f);
	for (alpha = 0; alpha < Math.PI / 4; alpha += (Math.PI / 200))
	{
		gt.involute(true, gp.base_radius, alpha, offset, ref to);
		gt.rotatePoint(offset, angle, ref to);
		gr.DrawLine(pp, from, to);
		from = to;
		to.X -= offset.X;
		to.Y -= offset.Y;
		if (Math.Sqrt(to.X * to.X + to.Y * to.Y) > gp.outside_radius)
			break;
	}
}

 // Draw toolpath right side of tooth
for (double angle = (gp.angle_one_tooth / 4) + gp.angle_pitch_tangent; 
	angle < Math.PI * 2; angle += (2 * Math.PI / gp.number_of_teeth))
{
	alpha = 0;
	gt.involute(false, gp.base_radius, alpha, offset, ref from);
	gt.rotatePoint(offset, angle, ref from);
	to = new PointF(0f, 0f);
	for (alpha = 0; alpha < Math.PI / 4; alpha += (Math.PI / 200))
	{
		gt.involute(false, gp.base_radius, alpha, offset, ref to);
		gt.rotatePoint(offset, angle, ref to);
		gr.DrawLine(pp, from, to);
		from = to;
		to.X -= offset.X;   // Stop if we passed outside_radius
		to.Y -= offset.Y;
		if (Math.Sqrt(to.X * to.X + to.Y * to.Y) > gp.outside_radius)
			break;
	}
}
...

Challenge 1 - Undercut with Low Teeth Count

Experiments and information on the internet show this only works for gears with more than 18 teeth, if less teeth we experience something gear makers call undercut.
Undercut is the tip of one tooth cutting into the bottom of the tooth on the mating gear.

Image 2

To get the right shape of the tooth, a straight rack gear can be rotated around the pitch circle giving the correct tooth form with undercut. drawRawTooth does exactly that by only drawing one rack tooth using function drawSingleToothRack.

C#
void drawSingleToothRack(ref Graphics gr, PointF Orig_offset, PointF offset, 
	ref gearParams gp, double pitchCircleRotateDistance, double rotationAngle)
{
    gearTools gt = new gearTools();
    using (Pen pp = new Pen(Color.Black, 0.25f))
    {
        // Left side of single tooth rack addendum deep
        PointF from_l = new PointF(Orig_offset.X - (float)gp.backlash - 
		(float)(gp.addendum * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2)), 
		Orig_offset.Y - (float)(gp.addendum));
        PointF to_addendum_l = new PointF(from_l.X + (float)((gp.dedendum + gp.addendum) * 
	Math.Cos(gp.pressure_angle + Math.PI * 3 / 2)), from_l.Y + (float)(gp.dedendum + gp.addendum));

        // Move the rack tooth the same distance the pitch circle has rotated
        from_l.X -= (float)pitchCircleRotateDistance;
        to_addendum_l.X -= (float)pitchCircleRotateDistance;

        // Rotate the points found so it keep the right position on the gear
        gt.rotatePoint(offset, rotationAngle, ref from_l);
        gt.rotatePoint(offset, rotationAngle, ref to_addendum_l);

        gr.DrawLine(pp, from_l, to_addendum_l);

        // Right side of single tooth rack addendum deep
        PointF to_r = new PointF(Orig_offset.X + (float)gp.pitch_tooth_width + 
	(float)gp.backlash + (float)(gp.addendum * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2)), 
	Orig_offset.Y - (float)(gp.addendum));
        PointF to_addendum_r = new PointF(to_r.X - (float)gp.backlash - 
	(float)((gp.dedendum + gp.addendum) * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2)), 
	to_r.Y + (float)(gp.dedendum + gp.addendum));

        // Move the rack tooth the same distance the pitch circle has rotated
        to_r.X -= (float)pitchCircleRotateDistance;
        to_addendum_r.X -= (float)pitchCircleRotateDistance;

        // Rotate the points found so it keep the right position on the gear
        gt.rotatePoint(offset, rotationAngle, ref to_r);
        gt.rotatePoint(offset, rotationAngle, ref to_addendum_r);

        gr.DrawLine(pp, to_addendum_l, to_addendum_r);
        gr.DrawLine(pp, to_addendum_r, to_r);
    }
}

The drawSingleToothRack is used in drawRawTooth.

C#
// Using a rack with one tooth we can form/remove material between teeth, 
// leaving the perfect tooth form
void drawRawTooth(ref Graphics gr, double at_angle, ref gearParams gp, PointF Orig_offset)
{
    // Center the drawing of the rack tooth at the pitch_radius in the midle of the tooth
    PointF offset = new PointF(Orig_offset.X + (float)(gp.pitch_tooth_width / 2), 
			Orig_offset.Y + (float)(gp.pitch_radius));
    using (Pen pp = new Pen(Color.Black, 0.1f))
    {
        gearTools gt = new gearTools();
        for (double n = -gp.pitch_tooth_width * 2; n < gp.pitch_tooth_width * 2; 
		n += gp.pitch_tooth_width * 4 / 400) // Movement of rack in mm
        {
            double g = Math.PI / 2 + at_angle + 
		(Math.PI * 2) / gp.pitch_circle * n; // Movement of the gear
            drawSingleToothRack(ref gr, Orig_offset, offset, ref gp, n, g);
        }
...

The drawRawTooth is used like this to draw a number of teeth.

C#
...
for (double angle = 0.0; angle < Math.PI * 2; angle += Math.PI * 2 / gp.number_of_teeth)
	drawRawTooth(ref gr, angle, ref gp, offset);
...

The result looks like this:

Image 3

Standard Ellipse

Using the superellipse with n=2, we can draw the standard ellipse.

C#
/// Draw Raw Super Ellipse with offset in center
public void drawEllipse_n(Graphics gr, PointF offset, double rotateAngleRadians)
{
    gr.PageUnit = GraphicsUnit.Millimeter;
    float length_perimeter = 0;
    //gr.Clear(Color.White);
    PointF from = new PointF(0, 0);
    PointF to = new PointF(0, 0);
    SizeF size = new SizeF(offset);
    gearTools gt = new gearTools();
    double circular_angle = 0.0;
    // Draw minor axis
    double radius = radiusAtAngleCenter(Math.PI / 2, ref circular_angle);
            
    using (Pen pp = new Pen(Color.Blue, 0.25f))
    {
                
        from.X = (float)(0);
        from.Y = (float)(radius);   // At angle PI/2 sin is 1
        from += size;
        to.X = (float)(0);
        to.Y = (float)(-radius);    // At angle 3*PI/2 sin is -1
        to += size;
        gt.rotatePoint(offset, rotateAngleRadians, ref from);
        gt.rotatePoint(offset, rotateAngleRadians, ref to);
        gr.DrawLine(pp, from, to);
    }
        // Draw major axis
    using (Pen pp = new Pen(Color.Green, 0.25f))
    {
        radius = radiusAtAngleCenter(0.0, ref circular_angle);
        from.X = (float)(radius);    // At angle 0.0 cos is 1
        from.Y = (float)(0);
        from += size;
        to.X = (float)(-radius);    // At angle PI cos is -1
        to.Y = (float)(0);
        to += size;
        gt.rotatePoint(offset, rotateAngleRadians, ref from);
        gt.rotatePoint(offset, rotateAngleRadians, ref to);
        gr.DrawLine(pp, from, to);
    }
    double c = Math.Cos(rotateAngleRadians);    // Do rotation calculation without 
						// calling a function to speed up
    double s = Math.Sin(rotateAngleRadians);
    double x = 0.0;
    double y = 0.0;
    using (Pen pp = new Pen(Color.Black, 0.25f))
    {
        for (double angle = 0; angle < Math.PI * 2; angle += Math.PI / 1000)
        {
            radius = radiusAtAngleCenter(angle, ref circular_angle);
            x = radius * Math.Cos(circular_angle);      // Use the circular angle - not the warped 
						// ellipse angle used to find the length of the radius
            y = radius * Math.Sin(circular_angle);
            to.X = (float)(x * c - y * s);
            to.Y = (float)(x * s + y * c);
            to += size;
            gr.DrawLine(pp, from, to);
            length_perimeter += lengthBetweenPoints(from, to);
            from = to;
        }
        this.perimeter = length_perimeter;
    }
...

Examples of Ellipse Gears Cut Out of MDF

Two standard ellipse drive gears will fit with pins in the centers and the distance between centers at a+b.

Image 4

Two standard ellipse drive gears and one driven in the middle will fit with a pins in the foci and the distance between foci at a+a.

Image 5

Image 6

How is this Made by the DrawInvolute?

Again, a rack is rotated around the shape, this time it's not a circle but an ellipse or superellipse.
To form the drive/driven standard ellipse, two draw rack functions are used, the driven is shifted a tooth width in relation to the drive.

C#
private void drawRackDrive(Graphics gr, ref gearParams gp, PointF offset, 
	int number_of_teeth_in_rack, double rotate_angle, double move_rack)
{
    double ptw = gp.pitch_tooth_width;
    double add = gp.addendum;
    double ded = gp.dedendum;
    PointF from = new PointF();
    PointF to = new PointF();
    PointF s_from = new PointF();
    PointF s_to = new PointF();
    PointF s2_from = new PointF();
    PointF s2_to = new PointF();
    using (Pen pp = new Pen(Color.Black, 0.25f))
    {
        bool first = true;
        int num = number_of_teeth_in_rack / 2;
        float centerX = (float)(offset.X - move_rack + (ptw / 2));
        int cnt = 0;
        for (int n = -num; n < 2; n += 2)
        {
            // Try to minimize number of iteration
            if ((n + 4) * ptw < move_rack)
                continue;
            if (cnt++ > 3)
                break;
            // Draw one tooth
            from.X = (float)(n * ptw + centerX);
            from.Y = offset.Y;
            to.X = from.X - (float)(add * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
            to.Y = from.Y - (float)add;
            pp.Color = Color.Black;
            pp.Width = 0.5f;
            {
                gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
                s2_from = to;

                if (first == true)
                {
                    first = false;
                }
                else
                {
                    gr.DrawLine(pp, rotatePoint(s2_from, offset, rotate_angle), 
				rotatePoint(s2_to, offset, rotate_angle));
                }

                to.X = from.X + (float)(ded * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
                to.Y = from.Y + (float)ded;
                gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
                s_from = to;

                from.X += (float)ptw;
                to.X = from.X + (float)(add * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
                to.Y = from.Y - (float)add;
                gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
                s2_to = to;

                to.X = from.X - (float)(ded * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
                to.Y = from.Y + (float)ded;
                gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
                s_to = to;

                gr.DrawLine(pp, rotatePoint(s_from, offset, rotate_angle), 
				rotatePoint(s_to, offset, rotate_angle));
            }
        }
    }
}

The driven goes like this:

C#
private void drawRackDriven(Graphics gr, ref gearParams gp, PointF offset, 
	int number_of_teeth_in_rack, double rotate_angle, double move_rack)
{
    double ptw = gp.pitch_tooth_width;
    double add = gp.addendum;
    double ded = gp.dedendum;
    PointF from = new PointF();
    PointF to = new PointF();
    PointF s_from = new PointF();
    PointF s_to = new PointF();
    PointF s2_from = new PointF();
    PointF s2_to = new PointF();
    using (Pen pp = new Pen(Color.Black, 0.25f))
    {
        bool first = true;
        int num = number_of_teeth_in_rack / 2;
        float centerX = (float)(offset.X - move_rack - ptw / 2);
        int cnt = 0;
        for (int n = -num; n < 2; n += 2)
        {
            // Try to minimize number of iteration
            if ((n + 2) * ptw < move_rack)
                continue;
            if (cnt++ > 3)
                break;
            // Draw one tooth
            from.X = (float)(n * ptw + centerX);
            from.Y = offset.Y;
            to.X = from.X - (float)(add * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
            to.Y = from.Y - (float)add;
            pp.Color = Color.Black;
            pp.Width = 0.5f;
            gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
            s2_from = to;

            if (first == true)
            {
                first = false;
            }
            else
            {
                gr.DrawLine(pp, rotatePoint(s2_from, offset, rotate_angle), 
				rotatePoint(s2_to, offset, rotate_angle));
            }

            to.X = from.X + (float)(ded * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
            to.Y = from.Y + (float)ded;
            gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
            s_from = to;

            from.X += (float)ptw;
            to.X = from.X + (float)(add * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
            to.Y = from.Y - (float)add;
            gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
            s2_to = to;

            to.X = from.X - (float)(ded * Math.Cos(gp.pressure_angle + Math.PI * 3 / 2));
            to.Y = from.Y + (float)ded;
            gr.DrawLine(pp, rotatePoint(from, offset, rotate_angle), 
				rotatePoint(to, offset, rotate_angle));
            s_to = to;

            gr.DrawLine(pp, rotatePoint(s_from, offset, rotate_angle), 
				rotatePoint(s_to, offset, rotate_angle));
        }
    }
}

The standard ellipse just rotates the rack around the shape, using functions in the Ellipse class, angleTangentAtAngle and pointAtAngle to position the rack.

C#
/// The tangent angle on the perimeter at an angle seen from the center
double angleTangentAtAngle(double angleRadians)
{
	double slope_y;
	double x = a * Math.Cos(angleRadians);
	double y = b * Math.Sin(angleRadians);
	// ellipse tangent slope y' = -((b*b*X)/(a*a*Y))
	if (y != 0)      // Avoid divide by zero error
		slope_y = (b * b * x) / (a * a * y);
	else
		slope_y = double.MaxValue;
	return Math.Atan(-slope_y);
}
C#
 /// Point on perimeter at angle seen from center adding a offset
PointF pointAtAngle(double angleRadians, PointF offset)
{
	return new PointF((float)(a * Math.Cos(angleRadians) + offset.X), 
		(float)(b * Math.Sin(angleRadians) + offset.Y));
}
C#
...
int cnt = 0;
for (double ang = 0; ang < Math.PI * 2; ang += Math.PI / working_dpi)
{
	cnt++;
	double tangent_angle = el.angleTangentAtAngle(ang);
	center_current_tooth = el.pointAtAngle(ang, Orig_offset);
	// Draw the rack
	//if (cnt <= 600)
	{
		if (ang <= Math.PI)
		{
			if (rbDrive.Checked == true)
			{
				drawRackDrive(gx, ref gp, center_current_tooth, 
				(int)(2 * el.perimeter / gp.pitch_tooth_width), 
				tangent_angle, -el.lengthAtAngle(ang));
			}
			else
			{
				drawRackDriven(gx, ref gp, center_current_tooth, 
				(int)(2 * el.perimeter / gp.pitch_tooth_width), 
				tangent_angle, -el.lengthAtAngle(ang));
			}
		}
		else
		{
			if (rbDrive.Checked == true)
			{
				drawRackDrive(gx, ref gp, center_current_tooth, 
				(int)(2 * el.perimeter / gp.pitch_tooth_width), 
				Math.PI + tangent_angle, -el.lengthAtAngle(ang));
			}
			else
			{
				drawRackDriven(gx, ref gp, center_current_tooth, 
				(int)(2 * el.perimeter / gp.pitch_tooth_width), 
				Math.PI + tangent_angle, -el.lengthAtAngle(ang));
			}
		}
	}
}
...

Challenge 2 - Superellipse

However, this method present a problem for superellipse with n>2, the values jump so if you divide the angle given to the superellipse formula, the distance between two points can be quite large and this ruins the drawing of the gear.

The solution is to draw the shape using straight lines filling the gaps/jumps and make use of Moore neighbor tracing to get the shape pixel by pixel, using this data to move/rotate the rack and we get a perfect gear again.
One problem I haven't solved yet, is if n less than 1, the shape is convex and the rack bump into parts of the shape it shouldn't, the solution not made in this program is to take a circular gear and roll this around the shape. The diameter of this should be less than the curvature of the convex shape and be adjusted so we get an even distributation of teeth on the shape.

The first step is to get the shape of the superellipse - non circular gear:

C#
...
using (Graphics gx = Graphics.FromImage((Image)bmp))
{
    gx.Clear(Color.White);
    el.drawRawEllipse_n(gx, Orig_offset, 0.0);
    bmp.Save("super_ellipse.jpg");

    using (tmpbmp = (Bitmap)bmp.Clone())
    {
        Bitmap res_bmp2;
        MooreNeighborTracing mnt = new MooreNeighborTracing();
        if (rbFoci2.Checked == true)
        {
            Orig_offset.X += (float)el.f2;
        }
        res_bmp2 = mnt.doMooreNeighborTracing(ref tmpbmp, Orig_offset, true, 
			Orig_offset, ref arrNonCircularShape, working_dpi);
        if (rbFoci2.Checked == true)
        {
            Orig_offset.X -= (float)el.f2;
        }
        res_bmp2.Save("res_non_circular_shape.jpg");                            
    }
                    
...

The next step is to rotate one of the racks drive/driven around the found shape to add the teeth again functions in Ellipse class is used.
This time radiusAtAngleCenter using algorithm found here:
http://math.stackexchange.com/questions/76099/polar-form-of-a-superellipse
lengthBetweenPoints - Using Pythagorean theorem
tangentAngleAtRadiusAtAngleCenter - Calculate tangent angle finding the secant of too points close to the middle point. This derivate before/after will make/give a better approximation of the real tangent as one underestimates the value and the other overestimates.

C#
/// Radius of Super Ellipse at angle seen from center
/// http://math.stackexchange.com/questions/76099/polar-form-of-a-superellipse

double radiusAtAngleCenter(double angleRadians, ref double circularAngleRadians)
{
	double c = Math.Cos(angleRadians);
	double s = Math.Sin(angleRadians);
	double cPow = Math.Pow(Math.Abs(c), 2 / n);
	double sPow = Math.Pow(Math.Abs(s), 2 / n);
	double x = a * Math.Sign(c) * cPow;
	double y = b * Math.Sign(s) * sPow;
	double radi = Math.Sqrt(x * x + y * y);
	// See the this as a vector a (x,y) and a std vector (x, 0) the angle 
	// between is defined as cos(angle) = a.b/|a||b|
	circularAngleRadians =  Math.Sign(s) * Math.Acos(x / radi);
	if (angleRadians > Math.PI)
		circularAngleRadians = 2 * Math.PI + circularAngleRadians;
	return radi;
}
C#
/// Calculate the length between to points - Using Pythagorean theorem.
float lengthBetweenPoints(PointF from, PointF to)
{
	float x_diff = to.X - from.X;
	float y_diff = to.Y - from.Y;
	return (float)Math.Sqrt(x_diff * x_diff + y_diff * y_diff);
}
C#
/// Calculate tangent angle finding the secant of too points close to the midle point
/// This derivate before/after will make give a better approximation of the 
/// real tangent as one underestimate the value and the other overestimate
public double tangentAngleAtRadiusAtAngleCenter(double angleRadians)
{
	double delta_angleRadians = Math.PI / 1000;
	double circle_angle_minus = 0.0;
	double radius_minus = radiusAtAngleCenter(angleRadians - delta_angleRadians, 
				ref circle_angle_minus);
	double circle_angle_plus = 0.0;
	double radius_plus = radiusAtAngleCenter(angleRadians + delta_angleRadians, 
				ref circle_angle_plus);

	double x_minus = Math.Cos(circle_angle_minus) * radius_minus;
	double y_minus = Math.Sin(circle_angle_minus) * radius_minus;
	double x_plus = Math.Cos(circle_angle_plus) * radius_plus;
	double y_plus = Math.Sin(circle_angle_plus) * radius_plus;
	// The -(Math.PI -  just to adjust to the way i draw the rack at the tangent  
	return -(Math.PI - (Math.PI * 4 + Math.Atan2(y_plus - y_minus, x_plus - x_minus)) % 
		(Math.PI * 2));
}
C#
...
for (double ang = 0.0; ang < Math.PI * 2; ang += Math.PI / working_dpi)
{
	new_radius = el.radiusAtAngleCenter(ang, ref circleAngle);
	to.X = (float)(new_radius * Math.Cos(circleAngle)) + Orig_offset.X;
	to.Y = (float)(new_radius * Math.Sin(circleAngle)) + Orig_offset.Y;

	center_current_tooth = to;
	len_perimeter += el.lengthBetweenPoints(from, to);
	double tangent_angle = el.tangentAngleAtRadiusAtAngleCenter(ang);
	if (rbDrive.Checked == true)
	{
		drawRackDrive(gx, ref gp, center_current_tooth, 
		(int)(2 * el.perimeter / gp.pitch_tooth_width), tangent_angle, -len_perimeter);
	}
	else
	{
		drawRackDriven(gx, ref gp, center_current_tooth, 
		(int)(2 * el.perimeter / gp.pitch_tooth_width), tangent_angle, -len_perimeter);
	}
	from = to;
}
...

With a second Moore neighbor trace, we get the shape of the gear with teeth seen from either center or foci.

C#
...
if (rbFoci2.Checked == true)
{
	Orig_offset.X += (float)el.f2;
}
res_bmp = mnt.doMooreNeighborTracing(ref tmpbmp, Orig_offset, true, Orig_offset, ref arrGear, working_dpi);
...

The gear can now be drawn based on the arrGear found with the Moore neighbor trace, the function drawGearFromArrayListWithCenterAt with no rotation is used for this.
Now, it saves the vector coordinates as SVG in an html named by filename. A small JavaScript has been added too, to show the gear in different sizes.
Data from the SVG part could be used as input to a 3D/CNC to generate a gear. Some ruby script files are generated too, can be used as extensions in Google's Skechup.

C#
private void drawGearFromArrayListWithCenterAt(Graphics gr, ref ArrayList arrGear, 
	double rotateGearAroundCenterRadians, PointF offset, Pen pp, String filename)
{
    gr.PageUnit = GraphicsUnit.Millimeter;
    float minX = float.MaxValue;
    float maxX = float.MinValue;
    float minY = float.MaxValue;
    float maxY = float.MinValue;
    SizeF move_center = new SizeF(offset);
    PointF centerGear = new PointF(0, 0);
    PointF to = new PointF(0, 0);
    PointF [] newPol = new PointF[arrGear.Count];
    PointF[] rawPol = new PointF[arrGear.Count];
    int cnt = 0;
    if (rotateGearAroundCenterRadians == 0.0)
    {
        foreach (polarPoint pol in arrGear)
        {
            to.X = (float)pol.x;
            to.Y = (float)pol.y;
            rawPol[cnt] = to;
            minX = Math.Min(to.X, minX);
            minY = Math.Min(to.Y, minY);
            maxX = Math.Max(to.X, maxX);
            maxY = Math.Max(to.Y, maxY);
            to += move_center;
            newPol[cnt++] = to;  
        }
    }
    else
    {
        double c = Math.Cos(rotateGearAroundCenterRadians);
        double s = Math.Sin(rotateGearAroundCenterRadians);
            
        foreach (polarPoint pol in arrGear)
        {
            to.X = (float)(pol.x * c - pol.y * s);
            to.Y = (float)(pol.x * s + pol.y * c);
            rawPol[cnt] = to;
            minX = Math.Min(to.X, minX);
            minY = Math.Min(to.Y, minY);
            maxX = Math.Max(to.X, maxX);
            maxY = Math.Max(to.Y, maxY);
            to += move_center;
            newPol[cnt++] = to;   
        }
    }
    if (newPol.Count() > 2)
    {
        gr.DrawLines(pp, newPol);
        float width = maxX - minX + 1;
        float height = maxY - minY + 1;
                
        StringBuilder sb = new StringBuilder();
        gearTools gt = new gearTools();
        ... SVG and java script is added to the sb with appendline here ...
        System.IO.StreamWriter file = new System.IO.StreamWriter(filename);
        file.WriteLine(sb.ToString());
        file.Close();
    }
}

The SVG looks like this:

<svg width="1209px" height="1184px" viewBox="-598 -586 1209 1184">
<g id="superellipse" style="stroke: black; fill: none;">
<path d="M 0 0
L 0 0
L 599 0
L 599 -1 
...
L 599 2
L 599 1
L 599 0
"/>
</g>
<!--<use xlink:href="#superellipse" transform="scale(0.03)"/>-->
</svg>

The JavaScript part is as follows:

JavaScript
<script language="javescript" type="text/javascript">
<!-- Hide javascript
    var myVar = setInterval(myTimer, 10);
    var value = 1.0;
    function myTimer()
    {
        var d = new Date();
        value -= 0.001;
        var my_str = "value: " + value + "<br/>";
        if (value < 0.11)
        {
           clearInterval(myVar); 
        } 
        var scale_str = "scale(" + String(value) + ")"; 
        document.getElementById("superellipse").setAttribute("transform", scale_str);
     }
 -->
 </script>
 <noscript>
 <h3>JavaScript needed</h3>
 </noscript>

Use at your own risk, see below.
The ruby script for Google's Sketchup is made by the code here.
Save one of the files with extension .rb in Sketchup's plugins folder.
Start Sketchup and select the extension draw_gear.
All the .rb files use the same extension name, so you will have to modify the name in the file if you want to use more in one presentation.
Number of points have been limited in the function as Sketchup crashes if the extension has 10000+ points in an extension.
So make sure you save your work before trying to use draw_gear extension.

C#
    ...
    	sb_ruby.AppendLine("#On Windows, the plugins are installed here:\n" +
					   "#C:\\Users\\<your_windows_user_name>\\AppData\\Roaming\\SketchUp\\
						SketchUp [n]\\SketchUp\\Plugins\n" +
					   "#Save this file as draw_gear.rb here... 
						start sketchup and select extension draw_gear\n\n" +
					   "# First we pull in the standard API hooks.\n" +
						"require 'sketchup.rb'\n" +

						"# Show the Ruby Console at startup so we can\n" +
						"# see any programming errors we may make.\n" +
						"SKETCHUP_CONSOLE.show\n" +

						"# Add a menu item to launch our plugin.\n" +
						"UI.menu(\"Plugins\").add_item(\"Draw gear\") {\n" +
						"  UI.messagebox(\"I'm about to draw a gear in mm!\") \n" +

						"  # Call our new method.\n" +
						"  draw_gear\n" +
						"}\n" +
						"def draw_gear\n" +
						"# Get \"handles\" to our model and the 
						Entities collection it contains.\n" +
						"model = Sketchup.active_model\n" +
						"entities = model.entities\n" +
						"gpt = []\n");
	foreach (PointF polElem in rawPol)
	{
		if (idx++ >= -1)
			if (idx%10 == 0)
				sb_ruby.AppendLine(String.Format("gpt[{0}] = [{1}, {2}, 0 ]", 
				idx/10 , (polElem.X/25.4f).ToString(CultureInfo.GetCultureInfo("en-GB")), 
				(polElem.Y/25.4f).ToString(CultureInfo.GetCultureInfo("en-GB"))));
	}
	sb_ruby.AppendLine("  # Add the face to the entities in the model\n" +
					   "  face = entities.add_face(gpt)\n\n" +
					   "  # Draw a circle on the ground plane around the origin.\n" +
					   "  center_point = Geom::Point3d.new(0,0,0)\n" +
					   "  normal_vector = Geom::Vector3d.new(0,0,1)\n" +
					   "  radius = 0.1574803\n" +
					   "  edgearray = entities.add_circle center_point, 
						normal_vector, radius\n" +
					   "  first_edge = edgearray[0]\n" +
					   "  arccurve = first_edge.curve\n" +
					   "  face.pushpull 0.3937\n" +
					   "  view = Sketchup.active_model.active_view\n" +
					   "  new_view = view.zoom_extents\n");
	sb_ruby.AppendLine("end");

	
	System.IO.StreamWriter file2 = new System.IO.StreamWriter(filename.Replace(".html", ".rb"));
	file2.WriteLine(sb_ruby.ToString());
	file2.Close();
}</your_windows_user_name>

Challenge 3 - Calculating the Mating Gear

The first step here is to use the shape found without teeth and rotate this around a new point outside the shape.
If rotated, a full circle and the length of the perimeter of the shape around the new point is the same as the perimeter length of the shape with no teeth, we have the same behavior as a normal circular gear - they rotate without slip on the pitch circle/shape.
This can be seen as the shape without teeth is a CAM, and the mating shape is the position of a point CAM follower.
drawBestMatingGearCenterDistance2 does this for us and returns a new arrGear of the mating gear.

C#
drawBestMatingGearCenterDistance2(ref arrNonCircularShape, ref arrGear, ref E);

Calculate the Best Center Distance

We start by finding the best new point where the non circular shape rotates without slip.
The important part here is to close the shape, else the perimeterLength is always the same, the last jump is needed.

C#
private double calculateBestCenterDistance(ref ArrayList arrNonCircularShape, 
	double a, double e, double n)
{
	//rtbCalcBestCenter.Text = "";
	int number_of_revolutions = Convert.ToInt32(tbNoRevolutions.Text);
	double centerOffset = Convert.ToDouble(tbCenterOffset.Text);
	double E = a * (1 + Math.Sqrt(1 + ((n * n) - 1) * (1 - e * e)));
	rtbCalcBestCenter.AppendText("E calculated: " + E.ToString());
	double new_radius = 0.0;
	double deltaAngle = 0.0;
	double prevAngle = 0.0;
	double rotateMatingGear = 0.0;
	double currentMatingAngle = 0.0;
	double E_offset = 10.0;
	double E_step = 4;
	int last_hit = 0;

	int cnt = 0;
	polarPoint pol = (polarPoint)arrNonCircularShape[1];
	double lastX = pol.x;
	double lastY = pol.y;
	double perimeterLength = 0.0;
	double checkLength = 0.0;
	foreach (polarPoint polP in arrNonCircularShape)
	{
		if (++cnt > 2) // Skip first point in array it has no correct radius
		{
			perimeterLength += Math.Sqrt((polP.x - lastX) * (polP.x - lastX) + 
						(polP.y - lastY) * (polP.y - lastY));
			lastX = polP.x;
			lastY = polP.y;
		}
	}
	checkLength = perimeterLength * number_of_revolutions;
	double nextX = 0;
	double nextY = 0;
	double firstX = 0;
	double firstY = 0;
	for (int loop_cnt = 0; loop_cnt < 100; loop_cnt++)  // Brute force way of getting best value..
	{
		currentMatingAngle = 0.0;
		perimeterLength = 0.0;

		E = a * (1 + Math.Sqrt(1 + ((n * n) - 1) * (1 - e * e))) + E_offset;
		if (E > 0)
		{
		   
			for (int rev = 0; rev < number_of_revolutions; rev++)
			{
				cnt = 0;
				pol = (polarPoint)arrNonCircularShape[1];
				new_radius = E - pol.radius;
				firstX = lastX = (Math.Cos(currentMatingAngle) * new_radius);
				firstY = lastY = (Math.Sin(currentMatingAngle) * new_radius);
				foreach (polarPoint polP in arrNonCircularShape)
				{
					if (++cnt > 2) 	// Skip first point in array it has 
							//no correct radius
					{
						new_radius = E - polP.radius;
						deltaAngle = Math.Abs(prevAngle - polP.angle);
						rotateMatingGear = deltaAngle * 
						(polP.radius / new_radius);    // rotate other way the 
								// minus, on the other side PI, 
								// number of revolution times
						currentMatingAngle += rotateMatingGear;

						nextX = (Math.Cos(currentMatingAngle) * new_radius);
						nextY = (Math.Sin(currentMatingAngle) * new_radius);
						perimeterLength += Math.Sqrt((nextX - lastX) * 
						(nextX - lastX) + (nextY - lastY) * (nextY - lastY));
						lastX = nextX;
						lastY = nextY;
					}
					prevAngle = polP.angle;
				}
			}			
		}
		perimeterLength += Math.Sqrt((firstX - lastX) * (firstX - lastX) + (firstY - lastY) * 
			(firstY - lastY));    // Close the shape..
		// Suspect a better way of finding the best value can be done 
		// (binary search like - recurcive) but this works and don't take long time 
		// so for now it's the way I'm doing it.       
		if ((perimeterLength < checkLength) || (currentMatingAngle > (Math.PI * 2)))
		{
			if (last_hit == 1)
				E_step *= 1.2;      // Works better than 2.0 for some reason
			else
				E_step /= 3;
			E_offset += E_step;
			last_hit = 1;
		}
		else
		{
			if (last_hit == -1)
				E_step *= 1.2;      // Works better than 2.0 for some reason
			else
				E_step /= 5;
			E_offset -= E_step;
			last_hit = -1;
		}
	}
	...

Adding Teeth to the Mating Gear

With this distance between centers, it's easy to rotate the original gear with teeth around the mating gear to give it its teeth, the fastDrawGearFromaArrayListWithCenterAt used only draw the part of the original gear that have contact with the mating gear, speeding things up by a factor 6.
Another 6 times faster is obtained by only drawing one sixth of the data, I can't see the difference and the gears produced work for my purpose.

C#
...
foreach (polarPoint polP in arrNonCircularShape)
{
	calcAngle = polP.angle;
	if (++cnt > 2) // Skip first point in array it has no correct radius
	{
		{
			new_radius = E - polP.radius;
			deltaAngle = Math.Abs(prevAngle - calcAngle);
			rotateNonCircularShapeRadians = (calcAngle * (new_radius / polP.radius)) % 
							(Math.PI * 2);
			rotateMatingGear = ((deltaAngle) * (polP.radius / new_radius));    // rotate 
				// other way the minus, on the other side PI, number of revolution times
			currentMatingAngle += rotateMatingGear;
			centerNonCircularShape.X = (float)(Math.Cos(currentMatingAngle) * E);
			centerNonCircularShape.Y = (float)(Math.Sin(currentMatingAngle) * E);
			centerNonCircularShape += moveToOffset;
			if (cnt % 6 == 0)       // Minimize the number of drawings
			{
				//drawGearFromaArrayListWithCenterAt(ref gr, ref arrGear, 
				//(3 * Math.PI + (-polP.angle + currentMatingAngle)) % (Math.PI * 2), 
				//centerNonCircularShape, pp);
				fastDrawGearFromaArrayListWithCenterAt(ref gr, ref arrGear, 
				(3 * Math.PI + (-polP.angle + currentMatingAngle)) % (Math.PI * 2), 
				-polP.angle, centerNonCircularShape, pp);
			}
		}
	}
	prevAngle = calcAngle;

}
...

Animation of the Resulting Gears

To give a better view of the resulting gear, an anmation has been added, the resulting html is saved in a file called animation.html and shown in the program using webbrowser control, I found this defaults to IE 7, but by inserting this line in the head section of the html, it works on my computer with SVG and JavaScript running.

C#
sb.AppendLine("     <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\"");

The full function goes like this:

C#
private void makeGearAnimationFromArrayLists( ref ArrayList arrGearNonCircular,
 ref ArrayList arrGear, ref ArrayList arrGearMating, ref double E, String filename,
 ref Ellipse el, ref gearParams gp)
     {
         MinMax minmax = new MinMax();
         MinMax minmaxMating = new MinMax();
         PointF centerGear = new PointF(0, 0);
         PointF to = new PointF(0, 0);
         PointF[] matingPol = new PointF[arrGearMating.Count];
         PointF[] origPol = new PointF[arrGear.Count];
         // Find min and max X and Y values to optimize height/width and viewport of animation
         int cnt = 0;
         foreach (polarPoint pol in arrGear)
         {
             to.X = (float)pol.x;
             to.Y = (float)pol.y;
             origPol[cnt++] = to;
             minmax.setMinMax(to);
         }
         // Both gears min and max are needed
         cnt = 0;
         foreach (polarPoint pol in arrGearMating)
         {
             to.X = (float)pol.x;
             to.Y = (float)pol.y;
             matingPol[cnt++] = to;
             minmaxMating.setMinMax(to);
         }
         if (origPol.Count() > 2)
         {
             float width = (float)(Math.Max(minmax.maxX, minmaxMating.maxX) -
             Math.Min(minmax.minX, minmaxMating.minX)) * 2 + 1;
             float height = (float)(Math.Max(Math.Max(minmax.maxX, minmaxMating.maxX),
             Math.Max(minmax.maxY, minmaxMating.maxY)) -
             Math.Min(Math.Min(minmax.minX, minmaxMating.minX),
             Math.Min(minmax.minY, minmaxMating.minY)))  + 1;

             StringBuilder sb = new StringBuilder();
             StringBuilder sbGear = new StringBuilder();
             gearTools gt = new gearTools();
             // Insert html header and make it show gear parameters
             sb.AppendLine("<HTML>");
             sb.AppendLine("<head>");
             // webbrowser used to display the animation, defaults to IE7,
             // this tell it to use a never version with support for the SVG and Java Script used
             sb.AppendLine("
             <meta http-equiv=\"X-UA-Compatible\"
             content=\"IE=edge\">\"");
             sb.AppendLine("</head>");
             sb.AppendLine("<H2><B>" + "a=" +
             el.a.ToString().PadRight(6).Substring(0, 6) +
                                     " b=" +
                                     el.b.ToString().PadRight(6).Substring(0, 6) +
                                     " n=" +
                                     el.n.ToString().PadRight(6).Substring(0, 6) +
                                     " revs=" +
                                     tbNoRevolutions.Text.PadRight(3).Substring(0, 3) +
                                     " ptw=" +
                                     gp.pitch_tooth_width.ToString().
                     PadRight(6).Substring(0, 6) +
                                     " foci=" + rbFoci2.Checked.ToString());
             sb.AppendLine(" add=" + gp.addendum.ToString().PadRight(10).Substring(0, 10) +
                           " ded=" + gp.dedendum.ToString().PadRight(10).Substring(0, 10) +
                           " E=" + E.ToString().PadRight(10).Substring(0, 10) +
                           ((rbDrive.Checked == true) ? " Drive" :
                           " Driven") + "</H2></B>>" );
             // The SVG stuff used to draw gear and mating gear
             sb.Append(String.Format("<svg width=\"{0}px\" height=\"{1}px\"
         viewBox=\"{2} {3} {4} {5}\">\n",
                 gt.mm2Pixel(width / 4, working_dpi),
                 gt.mm2Pixel(height / 4, working_dpi),
                 gt.mm2Pixel(Math.Min(minmax.minX, minmaxMating.minX) / 4, working_dpi),
                 gt.mm2Pixel(Math.Min(Math.Min(minmax.minX, minmaxMating.minX),
         Math.Min(minmax.minY, minmaxMating.minY)) / 4, working_dpi),
                 gt.mm2Pixel((width + (float)E) / 4, working_dpi),
                 gt.mm2Pixel(height / 4, working_dpi)));
             // Here goes the first gear
             sb.Append("<g id=\"superellipse\"
             style=\"stroke: black; fill: red;\">\n");
             sb.AppendLine(String.Format("<path d=\"M {0} {1} ",
     gt.mm2Pixel(origPol[0].X, working_dpi), gt.mm2Pixel(origPol[0].Y, working_dpi)));
             foreach (PointF polElem in origPol)
             {
                 sb.AppendLine(String.Format("L {0} {1} ",
                 gt.mm2Pixel(polElem.X, working_dpi),
         gt.mm2Pixel(polElem.Y, working_dpi)));
             }
             sb.AppendLine("\"/>\n</g>");

             // Here goet the mating gear
             sb.Append("<g id=\"mating\"
             style=\"stroke: black; fill: blue;\">\n");
             sb.AppendLine(String.Format("<path d=\"M {0} {1} ",
             gt.mm2Pixel(origPol[0].X,
         working_dpi), gt.mm2Pixel(origPol[0].Y, working_dpi)));
             foreach (PointF polElem in matingPol)
             {
                 sb.AppendLine(String.Format("L {0} {1} ",
                 gt.mm2Pixel(polElem.X, working_dpi),
         gt.mm2Pixel(polElem.Y, working_dpi)));
             }
             sb.AppendLine("\"/>\n</g>");

             sb.AppendLine("<!--<use xlink:href=\"#superellipse\"
             transform=\"scale(0.03)\"/>-->");
             sb.AppendLine("</svg>");
             // End of the SVG and start of the JavaScript to animate the gears
             sb.AppendLine("<script language=\"javescript\"
             type=\"text/javascript\">");
             sb.AppendLine("<!-- Hide javascript");
             // If more revolutions -> more data points so I speed up the animation
     // to get an even speed for the all animations
             sb.AppendLine(String.Format("var myVar = setInterval(myTimer, {0});",
         30 / Convert.ToDouble(tbNoRevolutions.Text)));
             sb.AppendLine("var value = 0.0;");
             sb.AppendLine("var rotate = 0.0;");
             sb.AppendLine("var cnt = 0;");
             sb.AppendLine("function myTimer() {");
             sb.AppendLine("    var d = new Date();");
             // Array of rotation step values for the mating gear
             sb.Append("    steps = [ ");
             // To keep things in sync a second array controls the original gear
             sbGear.Append("    stepsGear = [ ");
             // MooreNeighbor start at 360 and goes to 0 degree
             double startDegree = 360;
             double prevMatingDegree = startDegree;
             double matingDegree = 0.0;

             cnt = 0;
             int cntDegreeEntrys = 0;
             double deltaAngle = 0.0;

             foreach (polarPoint pol in arrGearNonCircular)
             {
                 if (cnt++ > 2)
                     if (pol.angle_degree < startDegree)
                     {
                         cntDegreeEntrys++;
                         deltaAngle = Math.Abs(prevMatingDegree - pol.angle_degree);
                         startDegree -=  1 / Convert.ToDouble(tbNoRevolutions.Text);
                         matingDegree = deltaAngle * (pol.radius / (E - pol.radius));
                         // Rotate the mating gear this increment
                         sb.Append(matingDegree.ToString().Replace(',','.') + ", ");
                         // and the original gear this and the gear run in sync.
                         sbGear.Append(deltaAngle.ToString().Replace(',', '.') + ", ");
                         prevMatingDegree = pol.angle_degree;
                     }
             }
             // End the arrays with a value not used..
             sb.AppendLine(" 0];" );
             sbGear.AppendLine(" 0];");
             // Insert second array into script
             sb.AppendLine(sbGear.ToString());
             // Reset the animation after all entrys have been shown once,
     // keeps things in sync to avoid a small error to add up to a bigger one
             sb.AppendLine(String.Format("    if (cnt > {0})", cntDegreeEntrys));
             sb.AppendLine("    {");
             sb.AppendLine("       cnt = 0;");
             sb.AppendLine("       value = 0.0;");
             sb.AppendLine("       rotate = 0.0;");
             sb.AppendLine("    }");
             // Mating rotate one way the original the other -/+
             sb.AppendLine("    rotate -= steps[cnt];");
             sb.AppendLine("    value += stepsGear[cnt++];");
             // Changing the scale factor the translate has to be changed too
     // ex. 312 with 0.25 will be 561 with 0.45 scale
             // We have to start the animation at 180 degree if foci is used - Move the
     // mating gear to it's correct position at E/(1/scalefactor)
             if (rbFoci2.Checked == false)
                 sb.AppendLine(String.Format("      var scale_str = \"translate({0},0)
         scale(0.25) rotate(\" + String(rotate) + \")\";",
         gt.mm2Pixel((float)E / 4, working_dpi)));
             else
                 sb.AppendLine(String.Format("      var scale_str = \"translate({0},0)
         scale(0.25) rotate(\" + String(rotate+180) + \")\";",
         gt.mm2Pixel((float)E / 4, working_dpi)));
             sb.AppendLine("    document.getElementById(\"superellipse\").setAttribute
             (\"transform\", scale_str);");
             // Remember to scale this too
             sb.AppendLine("    var scale_str2 = \"scale(0.25)
             rotate(\" + String(value) + \")\";");
             sb.AppendLine("    document.getElementById
             (\"mating\").setAttribute(\"transform\",
         scale_str2);");

             sb.AppendLine("}");
             sb.AppendLine("-->");
             sb.AppendLine("</script>");
             sb.AppendLine("<noscript>");
             sb.AppendLine(" <h3>JavaScript needed</h3>");
             sb.AppendLine("</noscript>");
             sb.AppendLine("</HTML>");

             // Save the animation html file - so it can be used outside the program
             System.IO.StreamWriter file = new System.IO.StreamWriter(filename);
             file.WriteLine(sb.ToString());
             file.Close();

             // Get the fullpath location of the amination html to be shown using webbrowser
             var myAssembly = System.Reflection.Assembly.GetEntryAssembly();
             var myAssemblyLocation = System.IO.Path.GetDirectoryName(myAssembly.Location);
             var myHtmlPath = Path.Combine(myAssemblyLocation, filename);
             Uri uri = new Uri(myHtmlPath);
             webBrowser1.ScriptErrorsSuppressed = false;
             webBrowser1.Navigate(uri);
             webBrowser1.Focus();
             webBrowser1.SetBounds(0, 0, 1400, 1000);
             webBrowser1.Show();
             // Show a button to stop the webbrowser and return to programs normal mode
             bHideAnimation.Show();
         }
     }

The animation screen looks like this:

Image 7

After using the button "Hide animation", a screen like this is shown (different values used for the pictures).

The Final Superellipse Result

A Moore neighbor trace, and drawing of the found arrGear and we have two funny looking gears to play with.

Image 8

Square Gears in MDF

Image 9

Files Saved by the Program

Button calc

Files with waste cut data: DrawInvoluteValues.txt and DrawInvoluteValuesBest.txt

Make a lot of files with names like: gear_2.66 _9.548_77.88_93.57_63.51_18.jpg
With 3-96 teeth.

  • gear_
  • Diametral_pitch
  • Module
  • Base radius
  • Outside radius
  • Start pos roll
  • Number of teeth

gear_5 _5.08 _6.906_11.68_18.28_3 .jpg

Image 10

gear_5 _5.08 _39.13_47.24_33.71_17 .jpg

Image 11

gear_5 _5.08 _41.43_49.78_33.78_18 .jpg

Image 12

Button Ellipse

  • Moore neighbor traceres_ellipse.jpg - The ellipse after
  • ellipse_drive.jpg - The final drive ellipse with center/foci marks and ellipse
  • ellipse_driven.jpg - The final driven ellipse with center/foci marks and ellipse

Button Superellipse

super_ellipse.jpg - Just the superellipse

Image 13

res_before_moore.jpg - After rotation of the rack around the superellipse

Image 14

res_non_circular_shape.jpg - The superellipse after a Moore neghbor trace, to get arrayList of polarPoint

Image 15

res_super_ellipse.jpg - After Moore neigbor trace

super_ellipse_drive.jpg - The final drive after adding center/foci marks, parameter details and superellipse

super_ellipse_driven.jpg - The final driven after adding center/foci marks, parameter details and superellipse

Image 16

rawMatingGear.jpg - After rotation of the res_super_ellipse around the best center distance

Image 17

res_mating_super_ellipse.jpg - The final mating superellipse with center mark and parameter details

Image 18

History

First Version...

Second version v2

More comments mentioned the GDI+ and memory problems the progam wrongly tried to fix with dispose and GC.collect could be solved by doing this:
All temporary dynamically allocated gdi related stuff, such as a Brush, a Pen, a GraphicsPath, a Matrix, should be controlled by a using statement, e.g.:

C#
using(var path = new GraphicsPath())
{
// Do your stuff
}

Thanks for the good advice - it works great.

Another request was vector output to be used with a 3D printer/CNC
This is now done as SVG output and a small JavaScript scaling the gear down, to show it in different sizes, look for the html files where you run the program.

Fixed some minor errors.

Third Version v3

The "Super ellipse" button now shows an animation after the calculation of the superellipse and the mating gear. Use the "Hide animation" button to stop/close the animation.
A experimental draw_gear ruby script extension/plugin for Google's Sketchup have been added too, I had my Sketchup crash after loading a large gear, have tried to limit number of points but just to be safe: Save work before you try at your own risk!

Challenges Left - ToDo

Function to load a image of a closed shape with a center point as part of the filename, roll a circular gear around this and make the mating gear rotating around the center point

Rotate the gear inside a shape like planetary gears, might be obtained by rotating the gear the other way generating the mating gear

This experimental code is included in the source, it seems to work with circular gears and should work with some ellipses too.
More investigation needed before I enable it in the program.
More details about the math problems and solution can be found in the book:
Noncircular Gears Design and Generation by Litvin, Fuetes-Aznar, Gonzalez-Perez and Hayasaka

C#
/// <summary>
/// Experimental works with ordinary circular gears - planatary gears see rawMatingGear.jpg
/// Ellipse with center - you get pointy teeth
/// Ellipse with foci - not working - Noncircular Gears Design and Generation by Litvin, 
/// Fuetes-Aznar, Gonzalez-Perez and Hayasaka mention you will need 3 ellipses with 
/// center at foci stacked to do this.
/// </summary>
/// <param name="arrNonCircularShape"></param>
/// <param name="arrGear"></param>
/// <param name="E"></param>
/// <returns></returns>
private double drawBestMatingGearCenterDistanceOutside(ref ArrayList arrNonCircularShape, 
	ref ArrayList arrGear, ref double E)
{
	int pixelHeight = 0;
	int pixelWidth = 0;
	Stopwatch sw = new Stopwatch();
	PointF offset = new PointF(0, 0);
	gearParams gp = new gearParams();
	gp.setInitValues(Convert.ToDouble(tbDiametralPitch.Text), 
	Convert.ToDouble(tbTeeth.Text), (float)Convert.ToDouble(tbPinSize.Text), 
	(float)Convert.ToDouble(tbToolWidth.Text), Convert.ToDouble(tbPressureAngle.Text), 
	Convert.ToDouble(tbBacklash.Text), cbColor.Checked);
	gp.calc();

	// Draw mating shape we got from new center
	Ellipse el = new Ellipse(Convert.ToDouble(tbEllipse_a.Text), 
		Convert.ToDouble(tbEllipse_b.Text), Convert.ToDouble(tbEllipse_n.Text));
	//double Enear = calculateBestCenterDistanceNearCalculated(ref arrNonCircularShape, 
	//el.a, el.e, el.n);
	if (rbFoci2.Checked == true)
	{
		offset.X -= (float)el.f1;
	}
	E = calculateBestCenterDistance(ref arrNonCircularShape, el.a, el.e, el.n);
	
	//double Eold = calculateBestCenterDistance();

	//rtbCalcBestCenter.AppendText("E: " + E.ToString().PadRight(10).Substring(0, 10) + 
	//" Eold: " + Eold.ToString().PadRight(10).Substring(0, 10) + "\n");
	//E = Math.Min(E, Eold);
	//E -= 0.5;
	rtbCalcBestCenter.AppendText("Using E: " + E.ToString().PadRight(10).Substring(0, 10) + "\n");
	Bitmap bmp; // Must have same number of pixels in x,y for drawing multipage alignment 
			// grid correct
	// MooreNeighborTrace only work on even pixels cnt
	int pixels = gp.mm2Pixel((float)((E + 10 + gp.addendum + gp.dedendum) * 4), working_dpi);
	if (pixels % 2 == 1)
		pixels++;
	bmp = new Bitmap(pixels, pixels, System.Drawing.Imaging.PixelFormat.Format16bppRgb555);
	bmp.SetResolution(working_dpi, working_dpi);
	Graphics gr = Graphics.FromImage((Image)bmp);

	gr.PageUnit = GraphicsUnit.Millimeter;
	gr.Clear(Color.White);
	int number_of_revolutions = Convert.ToInt32(tbNoRevolutions.Text);
	double newRadius = 0.0;
	double rotateMatingGear = 0.0;
	PointF centerNonCircularShape = new PointF(0, 0);
	PointF centerMatingGear = offset;
	offset.X = (float)(E + 10 + gp.addendum + gp.dedendum) *2;
	offset.Y = offset.X;
	SizeF moveToOffset = new SizeF(offset);
	double calcAngle = 0.0;
	PointF from = new PointF(0, 0);
	using (Pen pp = new Pen(Color.Black, 0.5f))
	{
		double prevAngle = 0.0;
		double prevRadius = 0.0;
		double currentMatingAngle = 0.0;
		double deltaAngle = 0.0;


		int cnt = 0;
		sw.Start();
		for (double rev = 0; rev < number_of_revolutions; rev++)
		{
			prevAngle = 0.0;
			currentMatingAngle = (rev * 2 * Math.PI) / (double)number_of_revolutions;
			cnt = 0;
			foreach (polarPoint polP in arrNonCircularShape)
			{
				calcAngle = polP.angle;

				if (++cnt > 2) // Skip first point in array it has no correct radius
				{
					//if (cnt % 50 == 0) // Limit number of drawings
					{
						newRadius = E + polP.radius;
						deltaAngle = Math.Abs(prevAngle - calcAngle);
						rotateMatingGear = ((deltaAngle) * 
						(newRadius / prevRadius));    // rotate other way the 
						//minus, on the other side PI, number of revolution times
						currentMatingAngle -= rotateMatingGear;
						centerNonCircularShape.X = (float)(Math.Cos
								(currentMatingAngle) * E);
						centerNonCircularShape.Y = (float)(Math.Sin
								(currentMatingAngle) * E);
						centerNonCircularShape += moveToOffset;
						if (cnt % 6 == 0)       // Minimize the number of 
									// drawings
						{
							
							//drawGearFromaArrayListWithCenterAt
							//(ref gr, ref arrGear, (3 * Math.PI + 
							//(-polP.angle + currentMatingAngle)) % 
							//(Math.PI * 2), centerNonCircularShape, pp);
							fastDrawGearFromaArrayListWithCenterAt(ref gr, 
							ref arrGear, (3 * Math.PI + (-polP.angle - 
							currentMatingAngle)) % (Math.PI * 2), 
							-polP.angle, centerNonCircularShape, pp);
						}
					}
				}
				prevAngle = calcAngle;
				prevRadius = E + polP.radius;
			}
		}
	}
	sw.Stop();
	rtbCalcBestCenter.AppendText("Draw Mating:  " + sw.ElapsedMilliseconds.ToString().PadLeft(4) + 
		" ms\n");
	bmp.Save("1037482/rawMatingGear.jpg");

	Bitmap res_bmp;
	MooreNeighborTracing mnt = new MooreNeighborTracing();
	arrGear.Clear();

	sw.Restart();
	using (res_bmp = mnt.doMooreNeighborTracing(ref bmp, offset, true, offset, 
				ref arrGear, working_dpi))
	{
		sw.Stop();
		rtbCalcBestCenter.AppendText("Second Moore: " + 
			sw.ElapsedMilliseconds.ToString().PadLeft(4) + " ms\n");
		gp.getPixelHeightWidthOffsetFromPolarPoints(ref arrGear, working_dpi, 
				ref pixelHeight, ref pixelWidth, ref offset);
		using (Bitmap bmpSecond = new Bitmap(pixelWidth, pixelHeight, 
				System.Drawing.Imaging.PixelFormat.Format16bppRgb555))
		{
			bmpSecond.SetResolution(working_dpi, working_dpi);
			using (Graphics gx = Graphics.FromImage((Image)bmpSecond))
			{

				gx.PageUnit = GraphicsUnit.Millimeter;

				using (Pen pp = new Pen(Color.Black, 0.5f))
				{
					if (rbFoci2.Checked == true)
					{
						offset.X += (float)(el.f1);
						if (Convert.ToDouble(tbNoRevolutions.Text) == 2)
						{
							offset.X += (float)(el.f2);
						}
					}
					gx.Clear(Color.White);
					drawGearFromArrayListWithCenterAt(gx, ref arrGear, 
						0.0, offset, pp, "res_mating_super_ellipse.html");
					drawCenterX(gx, offset);

					gx.DrawString("a=" + el.a.ToString().PadRight(6).Substring(0, 6) +
						" b=" + el.b.ToString().PadRight(6).Substring(0, 6) +
						" n=" + el.n.ToString().PadRight(6).Substring(0, 6) +
						" revs=" + tbNoRevolutions.Text.PadRight(3).
						Substring(0, 3) +
						" ptw=" + gp.pitch_tooth_width.ToString().
						PadRight(6).Substring(0, 6) +
						" foci=" + rbFoci2.Checked.ToString()
						, new Font("Tahoma", 8), Brushes.Black, 
						new PointF(10, 0));
					gx.DrawString("E=" + E.ToString().PadRight(10).
						Substring(0, 10) +
						" DP=" + gp.diametral_pitch.ToString().
						PadRight(10).Substring(0, 10) +
						" add=" + gp.addendum.ToString().
						PadRight(10).Substring(0, 10) +
						" ded=" + gp.dedendum.ToString().
						PadRight(10).Substring(0, 10)
						, new Font("Tahoma", 8), Brushes.Black, 
							new PointF(10, 4));
					bmpSecond.Save("1037482/res_mating_super_ellipse.jpg");
				}
			}
		}
	}
	return 0.0;
} 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)