65.9K
CodeProject 正在变化。 阅读更多。
Home

绘制齿轮 - 圆形和非圆形

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (55投票s)

2015 年 10 月 8 日

CPOL

11分钟阅读

viewsIcon

76288

downloadIcon

3402

了解齿轮,并通过使用 jpg 文件来切割木材和其他材料制成的可用齿轮

Sample Image - maximum width is 600 pixels

引言

DrawInvolute 可用于为特定齿轮找到最佳“废料切口”
探索用于形成齿轮齿廓的渐开线细节
通过使用程序并查看源代码来了解更多关于齿轮的知识
获取切割齿轮毛坯(外圆)、基圆... 的值

在切割渐开线之前,使用本文方法去除齿轮毛坯(圆盘)中的废料:

《大众科学》 - 1961 年 11 月,第 144-148 页
制作木制齿轮?当然,Edwin W. Love 的《制作木制齿轮?方法在这里》已被改编。

用黄色标记的废料切口是渐开线曲线的最佳近似值。
YouTube:使用路由器制作木制齿轮展示了另一种方法。
数据文件和保存的图像可用作插图和齿轮切割模板。
使用超椭圆来制作一些有趣的非圆形齿轮。

背景

我为一个小爱好项目需要几个木制齿轮。在网上搜索,我找到了大量关于齿轮的信息,但关于如何绘制/制作齿轮的代码片段很少,而且没有使用 C# 的,所以我开始收集信息并制作了这个程序 DrawInvolute

为了解决使用一种方法发现的问题,又添加了更多算法,在完成了圆形齿轮之后,又添加了标准的椭圆形状齿轮,最后是超椭圆非圆形齿轮。

Using the Code

您可以按原样使用该程序来调整齿轮参数并在屏幕上查看结果。
将保存的图像用作插图或木材和其他材料齿轮切割的模板。
如果您想更好地理解某个主题,建议查看源代码。

类名

将 MooreNeighborTracing 算法从 CPP 移植到 C# 以满足我的特殊需求 - 仅内部形状 - 用于清理绘图
添加了 LockBitMap 以提高速度,并添加了 ArrayList 来保存找到点的极坐标表示。
该算法称为 Moore 邻域跟踪。

算法的解释可以在这里找到:http://www.thebigblob.com/moore-neighbor-tracing-algorithm-in-c Erik Smistad

详细解释可以在这里找到:http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/moore.html

  • GearTools,用于转换 rad2deginch2mmpixel2mm 以及计算渐开线,处理 polarPoint 等。
  • GearParams,用于保存和进行基本的齿轮参数计算。
  • LockBitmap使用 C# 更快地处理位图 添加了对此程序所需的 16 位支持
  • MooreNeighborTracing
  • Ellipse,用于绘制和计算椭圆和超椭圆的函数。
  • BiSection,一个未使用的尝试,用于查找两个齿轮之间的最佳中心距
  • DrawInvoluteTake2,主类,包含 drawGearDrawCenterXdrawMultipageAlignmentGriddrawToothdrawRackDrivedrawRackDrivendrawRawToothdrawIndexMarkNumbersdrawPerfectGearcalcBestMatingGearCenterDistancedrawGearFromArrayfastDrawGearFromArraydrawXaxisdrawCicleMarkmakeGearAnimationFromArrayLists... 等函数。

圆形齿轮

GearTools 的渐开线函数用于绘制齿轮齿廓

/// 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;
}

使用函数 rotatePoint 来旋转齿廓到其在齿轮上的正确位置

/// 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; 
} 

使用渐开线绘制一个齿廓分两步进行 - 首先是齿廓的左侧,然后是右侧。
此处使用 gearParms 类中的齿轮参数。

...
// 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;
	}
}
...

挑战 1 - 低齿数下的根切

根据互联网上的实验和信息,这仅适用于齿数超过 18 的齿轮,如果齿数少于 18,我们会遇到齿轮制造者称为根切的现象。
根切是指一个齿的尖端切入相邻齿轮的齿根。

为了获得正确的齿廓形状,可以通过将一个直齿条齿轮绕节圆旋转来获得正确的带根切的齿廓。drawRawTooth 通过仅绘制一个齿条齿廓并使用函数 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);
    }
}

drawSingleToothRackdrawRawTooth 中使用。

// 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);
        }
...

drawRawTooth 的使用方法如下,用于绘制多个齿廓。

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

结果如下:

标准椭圆

使用超椭圆且 n=2,我们可以绘制标准椭圆。

/// 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;
    }
...

MDF 切割出的椭圆齿轮示例

两个标准椭圆驱动齿轮可以与中心销对齐,中心距为 a+b

两个标准椭圆驱动齿轮和一个中间的从动齿轮可以与焦点销对齐,焦点距为 a+a

DrawInvolute 如何制作这个?

再次,将一个齿条围绕形状旋转,这次不是围绕圆形,而是围绕椭圆或超椭圆。
为了形成驱动/从动标准椭圆,使用了两个绘制齿条的函数,从动齿条相对于驱动齿条偏移一个齿距。

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));
            }
        }
    }
}

从动齿轮的工作方式如下

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));
        }
    }
}

标准椭圆只是围绕形状旋转齿条,使用 Ellipse 类中的 angleTangentAtAnglepointAtAngle 函数来定位齿条。

/// 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));
			}
		}
	}
}
...

挑战 2 - 超椭圆

然而,这种方法对于 n>2 的超椭圆会产生问题,值会跳跃,因此,如果您将角度除以超椭圆公式给出的值,两个点之间的距离可能会很大,这会破坏齿轮的绘制。

解决方案是绘制形状,使用直线填充这些间隙/跳跃,并利用 Moore 邻域跟踪逐像素获取形状,使用这些数据来移动/旋转齿条,我们又能得到一个完美的齿轮。
我尚未解决的一个问题是,如果 n 小于 1,形状是凸形的,齿条会撞到形状中不应该接触的部分。此程序中未实现的解决方案是取一个圆形齿轮,并将其围绕形状滚动。其直径应小于凸形曲率,并进行调整,以便在形状上获得均匀分布的齿。

第一步是获取超椭圆的形状 - 非圆形齿轮

...
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");                            
    }
                    
...

下一步是围绕找到的形状旋转一个驱动/从动齿条,以添加齿廓,再次使用 Ellipse 类中的函数。
这次使用这里找到的算法 radiusAtAngleCenter
http://math.stackexchange.com/questions/76099/polar-form-of-a-superellipse
lengthBetweenPoints - 使用勾股定理
tangentAngleAtRadiusAtAngleCenter - 通过找到靠近中点的两个点的割线来计算切线角度。这之前的/之后的导数将更好地近似真实切线,因为一个低估了值,另一个高估了值。

/// 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;
}
...

通过第二次 Moore 邻域跟踪,我们可以看到从中心或焦点看到的带齿廓的齿轮形状。

...
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);
...

现在可以根据 Moore 邻域跟踪找到的 arrGear 来绘制齿轮,使用函数 drawGearFromArrayListWithCenterAt 且不进行旋转。
现在,它将矢量坐标保存为 SVG 格式,存储在名为 filename 的 html 文件中。还添加了一个小的 JavaScript,用于以不同尺寸显示齿轮。
SVG 部分的数据可以用作 3D/CNC 的输入来生成齿轮。还生成了一些 ruby 脚本文件,可用作 Google Sketchup 的扩展。

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();
    }
}

SVG 看起来像这样

<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>

JavaScript 部分如下

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

请自行承担使用风险,见下文。
Google Sketchup 的 ruby 脚本是根据此处代码制作的。
将其中一个文件以 .rb 扩展名保存在 Sketchup 的插件文件夹中。
启动 Sketchup 并选择扩展 draw_gear
所有 .rb 文件都使用相同的扩展名,因此如果您想在一次演示中使用更多文件,则需要在文件中修改名称。
函数中的点数已受限,因为如果扩展有 10000 多个点,Sketchup 就会崩溃。
因此,请确保在尝试使用 draw_gear 扩展之前保存您的工作。

    ...
    	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();
}

挑战 3 - 计算啮合齿轮

第一步是使用不带齿廓的形状,并将其围绕形状外部的新点进行旋转。
如果旋转,一个完整的圆周和围绕新点的形状周长长度与不带齿廓的形状的周长长度相同,我们就会获得与普通圆形齿轮相同的行为 - 它们在节圆/形状上无滑动地旋转。
这可以看作是无齿廓的形状是一个凸轮,而啮合形状是凸轮从动件点的位置。
drawBestMatingGearCenterDistance2 为我们执行此操作并返回一个新 arrGear 来表示啮合齿轮。

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

计算最佳中心距

我们首先找到非圆形形状无滑动旋转的最佳新点。
这里重要的一点是闭合形状,否则 perimeterLength 总是相同的,最后一个跳跃是必需的。

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;
		}
	}
	...

为啮合齿轮添加齿廓

有了这个中心距,很容易将原始带齿廓的齿轮围绕啮合齿轮旋转以给它添加齿廓,fastDrawGearFromaArrayListWithCenterAt 只绘制了原始齿轮与啮合齿轮接触的部分,速度提高了 6 倍。
仅绘制数据量的六分之一可以再提高 6 倍的速度,我看不出区别,而且产生的齿轮对我来说可以正常工作。

...
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;

}
...

结果齿轮的动画

为了更好地展示结果齿轮,添加了一个动画,结果的 html 保存在一个名为 animation.html 的文件中,并在程序中使用浏览器控件显示,我发现这默认使用 IE 7,但通过将此行插入 html 的 head 部分,它可以在我的计算机上运行 SVG 和 JavaScript。

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

完整的函数如下

   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();
            }
        }

动画屏幕看起来像这样

使用“隐藏动画”按钮后,会显示如下屏幕(图片使用了不同的值)。

最终超椭圆结果

进行 Moore 邻域跟踪,并绘制找到的 arrGear,我们就有两个有趣的齿轮可以玩了。

MDF 切割的方齿轮

程序保存的文件

按钮 calc

带有废料切口数据的文件的文件:DrawInvoluteValues.txt 和 DrawInvoluteValuesBest.txt

生成许多文件名类似于:gear_2.66 _9.548_77.88_93.57_63.51_18.jpg 的文件
具有 3-96 个齿。

  • gear_
  • 模数
  • 模块
  • 基圆半径
  • 外圆半径
  • 起始滚动位置
  • 齿数

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

按钮 Ellipse

  • Moore 邻域 traceres_ellipse.jpg - 椭圆之后
  • ellipse_drive.jpg - 带有中心/焦点标记和椭圆的最终驱动椭圆
  • ellipse_driven.jpg - 带有中心/焦点标记和椭圆的最终从动椭圆

按钮 Superellipse

super_ellipse.jpg - 仅超椭圆

res_before_moore.jpg - 齿条围绕超椭圆旋转后

res_non_circular_shape.jpg - Moore 邻域跟踪后的超椭圆,用于获取 polarPoint 的 arrayList

res_super_ellipse.jpg - Moore 邻域跟踪后

super_ellipse_drive.jpg - 添加了中心/焦点标记、参数详细信息和超椭圆后的最终驱动超椭圆

super_ellipse_driven.jpg - 添加了中心/焦点标记、参数详细信息和超椭圆后的最终从动超椭圆

rawMatingGear.jpg - 围绕最佳中心距旋转 res_super_ellipse 后

res_mating_super_ellipse.jpg - 带有中心标记和参数详细信息的最终啮合超椭圆

历史

第一个版本...

第二个版本 v2

更多评论提到 GDI+ 和内存问题,程序错误地尝试使用 dispose 和 GC.collect 来修复,可以通过以下方式解决:
所有临时的动态分配的 GDI 相关对象,如 BrushPenGraphicsPathMatrix,都应通过 using 语句进行控制,例如:

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

感谢提供的宝贵建议 - 效果很好。

另一个要求是提供矢量输出以供 3D 打印机/CNC 使用
现在已通过 SVG 输出和小型 JavaScript 实现,该 JavaScript 可将齿轮缩小以显示为不同尺寸,请查看运行程序时所在的 html 文件。

修复了一些小错误。

第三个版本 v3

“Super ellipse”按钮现在在计算超椭圆和啮合齿轮后显示动画。使用“Hide animation”按钮停止/关闭动画。
还添加了一个实验性的 draw_gear ruby 脚本扩展/插件,用于 Google Sketchup。在加载大型齿轮后,我的 Sketchup 崩溃了,我尝试限制点数,但为安全起见:在您自行承担风险尝试之前,请保存您的工作!

剩余挑战 - 待办

加载一个具有中心点(作为文件名的一部分)的闭合形状图像的函数,围绕此点滚动一个圆形齿轮,并使啮合齿轮围绕中心点旋转

在形状内旋转齿轮,如行星齿轮,可能通过反向旋转齿轮来生成啮合齿轮来实现

此实验代码包含在源代码中,它似乎适用于圆形齿轮,也应该适用于一些椭圆。
在将其在程序中启用之前,需要进行更多调查。
有关数学问题和解决方案的更多详细信息,请参阅书籍
Litvin、Fuetes-Aznar、Gonzalez-Perez 和 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;
} 
© . All rights reserved.