Drawing Gears - Circular and Non Circular






4.98/5 (55 votes)
Learn about gears and by using the jpg's be able to cut working gears in wood and other materials
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, handlepolarPoint
, 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 programMooreNeighborTracing
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 gearsDrawInvoluteTake2
, the main class with functions todrawGear
,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:
/// 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:
/// 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.
...
// 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.
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
.
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
.
// 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.
...
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:
Standard Ellipse
Using the superellipse with n=2
, we can draw the standard ellipse.
/// 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
.
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
.
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.
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:
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.
/// 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);
}
/// 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));
}
...
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:
...
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.
/// 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;
}
/// 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);
}
/// 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));
}
...
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.
...
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.
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:
<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.
...
sb_ruby.AppendLine("#On Windows, the plugins are installed here:\n" +
"#C:\\Users\\\\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();
}
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.
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.
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.
...
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.
sb.AppendLine(" <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\"");
The full function goes like this:
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:
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.
Square Gears in MDF
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
gear_5 _5.08 _39.13_47.24_33.71_17 .jpg
gear_5 _5.08 _41.43_49.78_33.78_18 .jpg
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
res_before_moore.jpg - After rotation of the rack around the superellipse
res_non_circular_shape.jpg - The superellipse after a Moore neghbor trace, to get arrayList of polarPoint
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
rawMatingGear.jpg - After rotation of the res_super_ellipse around the best center distance
res_mating_super_ellipse.jpg - The final mating superellipse with center mark and parameter details
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.:
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
/// <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;
}