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:
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:
public void rotatePoint(PointF offset, double angle, ref PointF p)
{
float s = (float)Math.Sin(angle);
float c = (float)Math.Cos(angle);
p.X -= offset.X;
p.Y -= offset.Y;
float xnew = p.X * c - p.Y * s;
float ynew = p.X * s + p.Y * c;
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.
...
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;
}
}
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;
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))
{
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));
from_l.X -= (float)pitchCircleRotateDistance;
to_addendum_l.X -= (float)pitchCircleRotateDistance;
gt.rotatePoint(offset, rotationAngle, ref from_l);
gt.rotatePoint(offset, rotationAngle, ref to_addendum_l);
gr.DrawLine(pp, from_l, to_addendum_l);
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));
to_r.X -= (float)pitchCircleRotateDistance;
to_addendum_r.X -= (float)pitchCircleRotateDistance;
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
.
void drawRawTooth(ref Graphics gr, double at_angle, ref gearParams gp, PointF Orig_offset)
{
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)
{
double g = Math.PI / 2 + at_angle +
(Math.PI * 2) / gp.pitch_circle * n;
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.
public void drawEllipse_n(Graphics gr, PointF offset, double rotateAngleRadians)
{
gr.PageUnit = GraphicsUnit.Millimeter;
float length_perimeter = 0;
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;
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);
from += size;
to.X = (float)(0);
to.Y = (float)(-radius);
to += size;
gt.rotatePoint(offset, rotateAngleRadians, ref from);
gt.rotatePoint(offset, rotateAngleRadians, ref to);
gr.DrawLine(pp, from, to);
}
using (Pen pp = new Pen(Color.Green, 0.25f))
{
radius = radiusAtAngleCenter(0.0, ref circular_angle);
from.X = (float)(radius);
from.Y = (float)(0);
from += size;
to.X = (float)(-radius);
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);
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);
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)
{
if ((n + 4) * ptw < move_rack)
continue;
if (cnt++ > 3)
break;
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)
{
if ((n + 2) * ptw < move_rack)
continue;
if (cnt++ > 3)
break;
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.
double angleTangentAtAngle(double angleRadians)
{
double slope_y;
double x = a * Math.Cos(angleRadians);
double y = b * Math.Sin(angleRadians);
if (y != 0)
slope_y = (b * b * x) / (a * a * y);
else
slope_y = double.MaxValue;
return Math.Atan(-slope_y);
}
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);
{
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.
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);
circularAngleRadians = Math.Sign(s) * Math.Acos(x / radi);
if (angleRadians > Math.PI)
circularAngleRadians = 2 * Math.PI + circularAngleRadians;
return radi;
}
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);
}
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;
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\\<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.
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)
{
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)
{
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++)
{
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)
{
new_radius = E - polP.radius;
deltaAngle = Math.Abs(prevAngle - polP.angle);
rotateMatingGear = deltaAngle *
(polP.radius / new_radius);
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));
if ((perimeterLength < checkLength) || (currentMatingAngle > (Math.PI * 2)))
{
if (last_hit == 1)
E_step *= 1.2;
else
E_step /= 3;
E_offset += E_step;
last_hit = 1;
}
else
{
if (last_hit == -1)
E_step *= 1.2;
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)
{
{
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));
currentMatingAngle += rotateMatingGear;
centerNonCircularShape.X = (float)(Math.Cos(currentMatingAngle) * E);
centerNonCircularShape.Y = (float)(Math.Sin(currentMatingAngle) * E);
centerNonCircularShape += moveToOffset;
if (cnt % 6 == 0)
{
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];
int cnt = 0;
foreach (polarPoint pol in arrGear)
{
to.X = (float)pol.x;
to.Y = (float)pol.y;
origPol[cnt++] = to;
minmax.setMinMax(to);
}
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();
sb.AppendLine("<HTML>");
sb.AppendLine("<head>");
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>>" );
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)));
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>");
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>");
sb.AppendLine("<script language=\"javescript\"
type=\"text/javascript\">");
sb.AppendLine("<!-- Hide javascript");
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();");
sb.Append(" steps = [ ");
sbGear.Append(" stepsGear = [ ");
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));
sb.Append(matingDegree.ToString().Replace(',','.') + ", ");
sbGear.Append(deltaAngle.ToString().Replace(',', '.') + ", ");
prevMatingDegree = pol.angle_degree;
}
}
sb.AppendLine(" 0];" );
sbGear.AppendLine(" 0];");
sb.AppendLine(sbGear.ToString());
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(" }");
sb.AppendLine(" rotate -= steps[cnt];");
sb.AppendLine(" value += stepsGear[cnt++];");
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);");
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>");
System.IO.StreamWriter file = new System.IO.StreamWriter(filename);
file.WriteLine(sb.ToString());
file.Close();
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();
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())
{
}
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
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();
Ellipse el = new Ellipse(Convert.ToDouble(tbEllipse_a.Text),
Convert.ToDouble(tbEllipse_b.Text), Convert.ToDouble(tbEllipse_n.Text));
if (rbFoci2.Checked == true)
{
offset.X -= (float)el.f1;
}
E = calculateBestCenterDistance(ref arrNonCircularShape, el.a, el.e, el.n);
rtbCalcBestCenter.AppendText("Using E: " + E.ToString().PadRight(10).Substring(0, 10) + "\n");
Bitmap bmp;
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)
{
{
newRadius = E + polP.radius;
deltaAngle = Math.Abs(prevAngle - calcAngle);
rotateMatingGear = ((deltaAngle) *
(newRadius / prevRadius));
currentMatingAngle -= rotateMatingGear;
centerNonCircularShape.X = (float)(Math.Cos
(currentMatingAngle) * E);
centerNonCircularShape.Y = (float)(Math.Sin
(currentMatingAngle) * E);
centerNonCircularShape += moveToOffset;
if (cnt % 6 == 0)
{
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;
}