Author Topic: Circle tangents  (Read 1812 times)

0 Members and 1 Guest are viewing this topic.

Offline (gile)

  • C#
  • *
  • Posts: 87
  • Karma: +8/-0
  • Gender: Male
    • prefered language: F
    • Prog expertise: Good
    • View Profile
Circle tangents
« on: September 14, 2013, 06:13:46 PM »
Hi,

Two methods which extents the CircularArc3d type to find tangents between to a point or another instance of CircularArc3d.

Code: [Select]
    using System;
    using Autodesk.AutoCAD.Geometry;
     
    namespace GeometryExtensions
    {
       /// <summary>
       /// Tangent type enum
       /// </summary>
       [Flags]
       public enum TangentType { Inner = 1, Outer = 2 }
     
       /// <summary>
       /// Provides extension methods for the CircularArc3d class.
       /// </summary>
       public static class CircularArc3dExtensions
       {
           /// <summary>
           /// Returns the tangents between the active CircularArc3d instance complete circle and another one.
           /// </summary>
           /// <remarks>
           /// Tangents start points are on the object to which this method applies, end points on the one passed as argument.
           /// Tangents are always returned in the same order: outer tangents before inner tangents, and for both,
           /// the tangent on the left side of the line from this circular arc center to the other one before the one on the right side.
           /// </remarks>
           /// <param name="arc">The object to which this method applies.</param>
           /// <param name="other">The CircularArc3d to which searched for tangents.</param>
           /// <param name="flags">An enum value specifying which type of tangent is returned.</param>
           /// <returns>An array of LineSegment3d representing the tangents (maybe 2 or 4) or null if there is none.</returns>
           /// <exception cref="Autodesk.AutoCAD.Runtime.exception">eNonCoplanarGeometry is thrown if the objects do not lies on the same plane.</exception>
           public static LineSegment3d[] GetTangentsTo(this CircularArc3d arc, CircularArc3d other, TangentType flags)
           {
               // check if circles lies on the same plane
               Vector3d normal = arc.Normal;
               double elev1 = arc.Center.TransformBy(Matrix3d.WorldToPlane(normal)).Z;
               double elev2 = other.Center.TransformBy(Matrix3d.WorldToPlane(normal)).Z;
               if (!(normal.IsParallelTo(other.Normal) &&
                   Math.Abs(elev1 - elev2) < Tolerance.Global.EqualPoint))
                   throw new Autodesk.AutoCAD.Runtime.Exception(
                       Autodesk.AutoCAD.Runtime.ErrorStatus.NonCoplanarGeometry);
     
               // check if a circle is inside the other
               double dist = arc.Center.DistanceTo(other.Center);
               if (dist - Math.Abs(arc.Radius - other.Radius) <= Tolerance.Global.EqualPoint)
                   return null;
     
               // check if circles overlap
               bool overlap = arc.Radius + other.Radius >= dist;
               if (overlap && flags == TangentType.Inner)
                   return null;
     
               CircularArc3d tmp1, tmp2;
               Point3d[] inters;
               Vector3d vec1, vec2, vec = other.Center - arc.Center;
               int i, j;
               LineSegment3d[] result = new LineSegment3d[(int)flags == 3 && !overlap ? 4 : 2];
     
               // outer tangents
               if (flags.HasFlag(TangentType.Outer))
               {
                   if (arc.Radius == other.Radius)
                   {
                       Line3d perp = new Line3d(arc.Center, vec.CrossProduct(normal));
                       inters = arc.IntersectWith(perp);
                       vec1 = (inters[0] - arc.Center).GetNormal();
                       vec2 = (inters[1] - arc.Center).GetNormal();
                       i = vec.GetAngleTo(vec1, normal) < vec.GetAngleTo(vec2, normal) ? 0 : 1;
                       j = i ^ 1;
                       result[i] = new LineSegment3d(inters[0], inters[0] + vec);
                       result[j] = new LineSegment3d(inters[1], inters[1] + vec);
                   }
                   else
                   {
                       Point3d center = arc.Radius < other.Radius ? other.Center : arc.Center;
                       tmp1 = new CircularArc3d(center, normal, Math.Abs(arc.Radius - other.Radius));
                       tmp2 = new CircularArc3d(arc.Center + vec / 2.0, normal, dist / 2.0);
                       inters = tmp1.IntersectWith(tmp2);
                       vec1 = (inters[0] - center).GetNormal();
                       vec2 = (inters[1] - center).GetNormal();
                       i = vec.GetAngleTo(vec1, normal) < vec.GetAngleTo(vec2, normal) ? 0 : 1;
                       j = i ^ 1;
                       result[i] = new LineSegment3d(arc.Center + vec1 * arc.Radius, other.Center + vec1 * other.Radius);
                       result[j] = new LineSegment3d(arc.Center + vec2 * arc.Radius, other.Center + vec2 * other.Radius);
                   }
               }
     
               // inner tangents
               if (flags.HasFlag(TangentType.Inner) && !overlap)
               {
                   double ratio = (arc.Radius / (arc.Radius + other.Radius)) / 2.0;
                   tmp1 = new CircularArc3d(arc.Center + vec * ratio, normal, dist * ratio);
                   inters = arc.IntersectWith(tmp1);
                   vec1 = (inters[0] - arc.Center).GetNormal();
                   vec2 = (inters[1] - arc.Center).GetNormal();
                   i = vec.GetAngleTo(vec1, normal) < vec.GetAngleTo(vec2, normal) ? 2 : 3;
                   j = i == 2 ? 3 : 2;
                   result[i] = new LineSegment3d(arc.Center + vec1 * arc.Radius, other.Center + vec1.Negate() * other.Radius);
                   result[j] = new LineSegment3d(arc.Center + vec2 * arc.Radius, other.Center + vec2.Negate() * other.Radius);
               }
               return result;
           }
     
           /// <summary>
           /// Returns the tangents between the active CircularArc3d instance complete circle and a point.
           /// </summary>
           /// <remarks>
           /// Tangents start points are on the object to which this method applies, end points on the point passed as argument.
           /// Tangents are always returned in the same order: the tangent on the left side of the line from the circular arc center
           /// to the point before the one on the right side.
           /// </remarks>
           /// <param name="arc">The object to which this method applies.</param>
           /// <param name="pt">The Point3d to which tangents are searched</param>
           /// <returns>An array of LineSegement3d representing the tangents (2) or null if there is none.</returns>
           /// <exception cref="Autodesk.AutoCAD.Runtime.exception">eNonCoplanarGeometry is thrown if the objects do not lies on the same plane.</exception>
           public static LineSegment3d[] GetTangentsTo(this CircularArc3d arc, Point3d pt)
           {
               // check if circle and point lies on the plane
               Vector3d normal = arc.Normal;
               double elev1 = arc.Center.TransformBy(Matrix3d.WorldToPlane(normal)).Z;
               double elev2 = pt.TransformBy(Matrix3d.WorldToPlane(normal)).Z;
               if (Math.Abs(elev1 - elev2) < Tolerance.Global.EqualPoint)
                   throw new Autodesk.AutoCAD.Runtime.Exception(
                       Autodesk.AutoCAD.Runtime.ErrorStatus.NonCoplanarGeometry);
     
               // check if the point is inside the circle
               Point3d center = arc.Center;
               if (pt.DistanceTo(center) <= arc.Radius)
                   return null;
     
               Vector3d vec = pt.GetVectorTo(center) / 2.0;
               CircularArc3d tmp = new CircularArc3d(pt + vec, arc.Normal, vec.Length);
               Point3d[] inters = arc.IntersectWith(tmp);
               LineSegment3d[] result = new LineSegment3d[2];
               int i = vec.GetAngleTo(inters[0] - center, normal) < vec.GetAngleTo(inters[1] - center, normal) ? 0 : 1;
               int j = i ^ 1;
               result[i] = new LineSegment3d(inters[0], pt);
               result[j] = new LineSegment3d(inters[1], pt);
               return result;
           }
       }
    }
     

A (maybe usefull) command example which draws a closed polyline along the outer tangents and the trimmed selected circles.

Code: [Select]
            [CommandMethod("JoinCircles", CommandFlags.Modal)]
           public void JoinCircles()
           {
               Document doc = Application.DocumentManager.MdiActiveDocument;
               Database db = doc.Database;
               Editor ed = doc.Editor;
     
               PromptEntityOptions peo = new PromptEntityOptions("\nSelect a circle: ");
               peo.SetRejectMessage("Only a circle.");
               peo.AddAllowedClass(typeof(Circle), true);
               PromptEntityResult per = ed.GetEntity(peo);
               if (per.Status != PromptStatus.OK)
                   return;
               ObjectId id1 = per.ObjectId;
     
               peo.Message = "\nSelect another circle: ";
               ObjectId id2;
               while (true)
               {
                   per = ed.GetEntity(peo);
                   if (per.Status != PromptStatus.OK)
                       return;
                   id2 = per.ObjectId;
                   if (id1 == id2)
                       ed.WriteMessage("\nThe second circle is the same as the first one.");
                   else break;
               }
     
               try
               {
                   using (Transaction tr = db.TransactionManager.StartTransaction())
                   {
                       Circle c1 = (Circle)tr.GetObject(id1, OpenMode.ForRead);
                       Circle c2 = (Circle)tr.GetObject(id2, OpenMode.ForRead);
                       CircularArc3d ca1 = new CircularArc3d(c1.Center, c1.Normal, c1.Radius);
                       CircularArc3d ca2 = new CircularArc3d(c2.Center, c2.Normal, c2.Radius);
                       LineSegment3d[] lines = ca1.GetTangentsTo(ca2, TangentType.Outer);
                       if (lines != null)
                       {
                           BlockTableRecord btr =
                               (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                           Vector3d vec = c1.Center.GetVectorTo(c2.Center);
                           Plane plane = new Plane(Point3d.Origin, c1.Normal);
                           double a1 = vec.GetAngleTo(ca1.Center.GetVectorTo(lines[1].StartPoint), ca1.Normal) -
                               vec.GetAngleTo(ca1.Center.GetVectorTo(lines[0].StartPoint), ca1.Normal);
                           double a2 = vec.Negate().GetAngleTo(ca2.Center.GetVectorTo(lines[0].EndPoint), ca1.Normal) -
                               vec.Negate().GetAngleTo(ca2.Center.GetVectorTo(lines[1].EndPoint), ca1.Normal);
                           Polyline pline = new Polyline(4);
                           pline.AddVertexAt(0, lines[0].StartPoint.Convert2d(plane), Math.Tan(a1 / 4.0), 0.0, 0.0);
                           pline.AddVertexAt(1, lines[1].StartPoint.Convert2d(plane), 0.0, 0.0, 0.0);
                           pline.AddVertexAt(2, lines[1].EndPoint.Convert2d(plane), Math.Tan(a2 / 4.0), 0.0, 0.0);
                           pline.AddVertexAt(3, lines[0].EndPoint.Convert2d(plane), 0.0, 0.0, 0.0);
                           pline.Closed = true;
                           pline.Normal = c1.Normal;
                           pline.Elevation = c1.Center.TransformBy(Matrix3d.WorldToPlane(c1.Normal)).Z;
                           btr.AppendEntity(pline);
                           tr.AddNewlyCreatedDBObject(pline, true);
                           c1.UpgradeOpen();
                           c2.UpgradeOpen();
                           c1.Erase();
                           c2.Erase();
                       }
                       tr.Commit();
                   }
               }
               catch (System.Exception exn)
               {
                   ed.WriteMessage("\nError: " + exn.Message);
               }
           }

Offline (gile)

  • C#
  • *
  • Posts: 87
  • Karma: +8/-0
  • Gender: Male
    • prefered language: F
    • Prog expertise: Good
    • View Profile
Re: Circle tangents
« Reply #1 on: September 15, 2013, 04:31:56 PM »
As i encountered some issues with the CircularArc3d.IntersectWith() method in some randomly 3d rotated planes, I decided to try the CircularArc2d route it seems to work fine.

The two methods shown upper now call equivalent extension methods for the CircularArc2d type after converting the CircularArc3d instance into CircularArc2d ones.
The Point2d.Convert3d() extension method is extracted from the GeometryExtensions library.

Code: [Select]
using System;
using Autodesk.AutoCAD.Geometry;

namespace GeometryExtensions
{
    /// <summary>
    /// Tangent type enum
    /// </summary>
    [Flags]
    public enum TangentType { Inner = 1, Outer = 2 }

    /// <summary>
    /// Provides extension methods for the Point2d structure.
    /// </summary>
    public static class Point2dExtensions
    {       
        /// <summary>
        /// Converts a 2d point into a 3d point according to the plane defined by
        /// the specified normal vector and elevation.
        /// </summary>
        /// <param name="pt">The instance to which the method applies.</param>
        /// <param name="normal">The normal vector of the plane which the point lies on.</param>
        /// <param name="elevation">The elevation of the plane which the point lies on.</param>
        /// <returns>The corresponding 3d point</returns>
        public static Point3d Convert3d(this Point2d pt, Vector3d normal, double elevation)
        {
            return new Point3d(pt.X, pt.Y, elevation).TransformBy(Matrix3d.PlaneToWorld(normal));
        }
    }

    /// <summary>
    /// Provides extension methods for the CircularArc3d class.
    /// </summary>
    public static class CircularArc3dExtensions
    {
        /// <summary>
        /// Returns the tangents between the active CircularArc3d instance complete circle and a point.
        /// </summary>
        /// <remarks>
        /// Tangents start points are on the object to which this method applies, end points on the point passed as argument.
        /// Tangents are always returned in the same order: the tangent on the left side of the line from the circular arc center
        /// to the point before the one on the right side.
        /// </remarks>
        /// <param name="arc">The object to which this method applies.</param>
        /// <param name="pt">The Point3d to which tangents are searched</param>
        /// <returns>An array of LineSegement3d representing the tangents (2) or null if there is none.</returns>
        /// <exception cref="Autodesk.AutoCAD.Runtime.exception">eNonCoplanarGeometry is thrown if the objects do not lies on the same plane.</exception>
        public static LineSegment3d[] GetTangentsTo(this CircularArc3d arc, Point3d pt)
        {
            // check if arc and point lies on the plane
            Vector3d normal = arc.Normal;
            Matrix3d WCS2OCS = Matrix3d.WorldToPlane(normal);
            double elevation = arc.Center.TransformBy(WCS2OCS).Z;
            if (Math.Abs(elevation - pt.TransformBy(WCS2OCS).Z) < Tolerance.Global.EqualPoint)
                throw new Autodesk.AutoCAD.Runtime.Exception(
                    Autodesk.AutoCAD.Runtime.ErrorStatus.NonCoplanarGeometry);

            Plane plane = new Plane(Point3d.Origin, normal);
            Matrix3d OCS2WCS = Matrix3d.PlaneToWorld(plane);
            CircularArc2d ca2d = new CircularArc2d(arc.Center.Convert2d(plane), arc.Radius);
            LineSegment2d[] lines2d = ca2d.GetTangentsTo(pt.Convert2d(plane));

            if (lines2d == null)
                return null;

            LineSegment3d[] result = new LineSegment3d[lines2d.Length];
            for (int i = 0; i < lines2d.Length; i++)
            {
                LineSegment2d ls2d = lines2d[i];
                //Point3d p1 = new Point3d(ls2d.StartPoint.X, ls2d.StartPoint.Y, elevation);
                //Point3d p2 = new Point3d(ls2d.EndPoint.X, ls2d.EndPoint.Y, elevation);
                //result[i] = new LineSegment3d(p1.TransformBy(OCS2WCS), p2.TransformBy(OCS2WCS));
                result[i] = new LineSegment3d(ls2d.StartPoint.Convert3d(normal, elevation), ls2d.EndPoint.Convert3d(normal, elevation));
            }
            return result;
        }

        /// <summary>
        /// Returns the tangents between the active CircularArc3d instance complete circle and another one.
        /// </summary>
        /// <remarks>
        /// Tangents start points are on the object to which this method applies, end points on the one passed as argument.
        /// Tangents are always returned in the same order: outer tangents before inner tangents, and for both,
        /// the tangent on the left side of the line from this circular arc center to the other one before the one on the right side.
        /// </remarks>
        /// <param name="arc">The object to which this method applies.</param>
        /// <param name="other">The CircularArc3d to which searched for tangents.</param>
        /// <param name="flags">An enum value specifying which type of tangent is returned.</param>
        /// <returns>An array of LineSegment3d representing the tangents (maybe 2 or 4) or null if there is none.</returns>
        /// <exception cref="Autodesk.AutoCAD.Runtime.exception">eNonCoplanarGeometry is thrown if the objects do not lies on the same plane.</exception>
        public static LineSegment3d[] GetTangentsTo(this CircularArc3d arc, CircularArc3d other, TangentType flags)
        {
            // check if circles lies on the same plane
            Vector3d normal = arc.Normal;
            Matrix3d WCS2OCS = Matrix3d.WorldToPlane(normal);
            double elevation = arc.Center.TransformBy(WCS2OCS).Z;
            if (!(normal.IsParallelTo(other.Normal) &&
                Math.Abs(elevation - other.Center.TransformBy(WCS2OCS).Z) < Tolerance.Global.EqualPoint))
                throw new Autodesk.AutoCAD.Runtime.Exception(
                    Autodesk.AutoCAD.Runtime.ErrorStatus.NonCoplanarGeometry);

            Plane plane = new Plane(Point3d.Origin, normal);
            Matrix3d OCS2WCS = Matrix3d.PlaneToWorld(plane);
            CircularArc2d ca2d1 = new CircularArc2d(arc.Center.Convert2d(plane), arc.Radius);
            CircularArc2d ca2d2 = new CircularArc2d(other.Center.Convert2d(plane), other.Radius);
            LineSegment2d[] lines2d = ca2d1.GetTangentsTo(ca2d2, flags);

            if (lines2d == null)
                return null;

            LineSegment3d[] result = new LineSegment3d[lines2d.Length];
            for (int i = 0; i < lines2d.Length; i++)
            {
                LineSegment2d ls2d = lines2d[i];
                //Point3d p1 = new Point3d(ls2d.StartPoint.X, ls2d.StartPoint.Y, elevation);
                //Point3d p2 = new Point3d(ls2d.EndPoint.X, ls2d.EndPoint.Y, elevation);
                //result[i] = new LineSegment3d(p1.TransformBy(OCS2WCS), p2.TransformBy(OCS2WCS));
                result[i] = new LineSegment3d(ls2d.StartPoint.Convert3d(normal, elevation), ls2d.EndPoint.Convert3d(normal, elevation));
            }
            return result;
        }
    }

    /// <summary>
    /// Provides extension methods for the CircularArc2d class.
    /// </summary>
    public static class CircularArc2dExtensions
    {
        /// <summary>
        /// Returns the tangents between the active CircularArc2d instance complete circle and a point.
        /// </summary>
        /// <remarks>
        /// Tangents start points are on the object to which this method applies, end points on the point passed as argument.
        /// Tangents are always returned in the same order: the tangent on the left side of the line from the circular arc center
        /// to the point before the one on the right side.
        /// </remarks>
        /// <param name="arc">The object to which this method applies.</param>
        /// <param name="pt">The Point2d to which tangents are searched</param>
        /// <returns>An array of LineSegement2d representing the tangents (2) or null if there is none.</returns>
        public static LineSegment2d[] GetTangentsTo(this CircularArc2d arc, Point2d pt)
        {
            // check if the point is inside the circle
            Point2d center = arc.Center;
            if (pt.GetDistanceTo(center) <= arc.Radius)
                return null;

            Vector2d vec = center.GetVectorTo(pt) / 2.0;
            CircularArc2d tmp = new CircularArc2d(center + vec, vec.Length);
            Point2d[] inters = arc.IntersectWith(tmp);
            if (inters == null)
                return null;
            LineSegment2d[] result = new LineSegment2d[2];
            Vector2d v1 = inters[0] - center;
            Vector2d v2 = inters[1] - center;
            int i = vec.X * v1.Y - vec.Y - v1.X > 0 ? 0 : 1;
            int j = i ^ 1;
            result[i] = new LineSegment2d(inters[0], pt);
            result[j] = new LineSegment2d(inters[1], pt);
            return result;
        }

        /// <summary>
        /// Returns the tangents between the active CircularArc2d instance complete circle and another one.
        /// </summary>
        /// <remarks>
        /// Tangents start points are on the object to which this method applies, end points on the one passed as argument.
        /// Tangents are always returned in the same order: outer tangents before inner tangents, and for both,
        /// the tangent on the left side of the line from this circular arc center to the other one before the one on the right side.
        /// </remarks>
        /// <param name="arc">The object to which this method applies.</param>
        /// <param name="other">The CircularArc2d to which searched for tangents.</param>
        /// <param name="flags">An enum value specifying which type of tangent is returned.</param>
        /// <returns>An array of LineSegment2d representing the tangents (maybe 2 or 4) or null if there is none.</returns>
        public static LineSegment2d[] GetTangentsTo(this CircularArc2d arc, CircularArc2d other, TangentType flags)
        {
            // check if a circle is inside the other
            double dist = arc.Center.GetDistanceTo(other.Center);
            if (dist - Math.Abs(arc.Radius - other.Radius) <= Tolerance.Global.EqualPoint)
                return null;

            // check if circles overlap
            bool overlap = arc.Radius + other.Radius >= dist;
            if (overlap && flags == TangentType.Inner)
                return null;

            CircularArc2d tmp1, tmp2;
            Point2d[] inters;
            Vector2d vec1, vec2, vec = other.Center - arc.Center;
            int i, j;
            LineSegment2d[] result = new LineSegment2d[(int)flags == 3 && !overlap ? 4 : 2];

            // outer tangents
            if (flags.HasFlag(TangentType.Outer))
            {
                if (arc.Radius == other.Radius)
                {
                    Line2d perp = new Line2d(arc.Center, vec.GetPerpendicularVector());
                    inters = arc.IntersectWith(perp);
                    if (inters == null)
                        return null;
                    vec1 = (inters[0] - arc.Center).GetNormal();
                    vec2 = (inters[1] - arc.Center).GetNormal();
                    i = vec.X * vec1.Y - vec.Y - vec1.X > 0 ? 0 : 1;
                    j = i ^ 1;
                    result[i] = new LineSegment2d(inters[0], inters[0] + vec);
                    result[j] = new LineSegment2d(inters[1], inters[1] + vec);
                }
                else
                {
                    Point2d center = arc.Radius < other.Radius ? other.Center : arc.Center;
                    tmp1 = new CircularArc2d(center, Math.Abs(arc.Radius - other.Radius));
                    tmp2 = new CircularArc2d(arc.Center + vec / 2.0, dist / 2.0);
                    inters = tmp1.IntersectWith(tmp2);
                    if (inters == null)
                        return null;
                    vec1 = (inters[0] - center).GetNormal();
                    vec2 = (inters[1] - center).GetNormal();
                    i = vec.X * vec1.Y - vec.Y - vec1.X > 0 ? 0 : 1;
                    j = i ^ 1;
                    result[i] = new LineSegment2d(arc.Center + vec1 * arc.Radius, other.Center + vec1 * other.Radius);
                    result[j] = new LineSegment2d(arc.Center + vec2 * arc.Radius, other.Center + vec2 * other.Radius);
                }
            }

            // inner tangents
            if (flags.HasFlag(TangentType.Inner) && !overlap)
            {
                double ratio = (arc.Radius / (arc.Radius + other.Radius)) / 2.0;
                tmp1 = new CircularArc2d(arc.Center + vec * ratio, dist * ratio);
                inters = arc.IntersectWith(tmp1);
                if (inters == null)
                    return null;
                vec1 = (inters[0] - arc.Center).GetNormal();
                vec2 = (inters[1] - arc.Center).GetNormal();
                i = vec.X * vec1.Y - vec.Y - vec1.X > 0 ? 2 : 3;
                j = i == 2 ? 3 : 2;
                result[i] = new LineSegment2d(arc.Center + vec1 * arc.Radius, other.Center + vec1.Negate() * other.Radius);
                result[j] = new LineSegment2d(arc.Center + vec2 * arc.Radius, other.Center + vec2.Negate() * other.Radius);
            }
            return result;
        }
    }
}

A Test command which draws all tangents between the two selected circles. the lines are colored according to their order in the LineSegment3d array returned by GetTangentsTo() (1 red, 2 yellow, 3 green, 4 cyan).

Code: [Select]
        [CommandMethod("Test")]
        public void Test()
        {
            Document doc = Application.DocumentManager.MdiActiveDocument;
            Database db = doc.Database;
            Editor ed = doc.Editor;

            PromptEntityOptions peo = new PromptEntityOptions("\nSelect a circle: ");
            peo.SetRejectMessage("Only a circle.");
            peo.AddAllowedClass(typeof(Circle), true);
            PromptEntityResult per = ed.GetEntity(peo);
            if (per.Status != PromptStatus.OK)
                return;
            ObjectId id1 = per.ObjectId;

            peo.Message = "\nSelect another circle: ";
            ObjectId id2;
            while (true)
            {
                per = ed.GetEntity(peo);
                if (per.Status != PromptStatus.OK)
                    return;
                id2 = per.ObjectId;
                if (id1 == id2)
                    ed.WriteMessage("\nThe second circle is the same as the first one.");
                else break;
            }

            try
            {
                using (Transaction tr = db.TransactionManager.StartTransaction())
                {
                    Circle c1 = (Circle)tr.GetObject(id1, OpenMode.ForRead);
                    Circle c2 = (Circle)tr.GetObject(id2, OpenMode.ForRead);

                    CircularArc3d ca1 = new CircularArc3d(c1.Center, c1.Normal, c1.Radius);
                    CircularArc3d ca2 = new CircularArc3d(c2.Center, c2.Normal, c2.Radius);
                    LineSegment3d[] lines = ca1.GetTangentsTo(ca2, TangentType.Inner | TangentType.Outer);
                    if (lines != null)
                    {
                        BlockTableRecord btr =
                            (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
                        for (int i = 0; i < lines.Length; i++)
                        {
                            Line line = new Line(lines[i].StartPoint, lines[i].EndPoint);
                            line.ColorIndex = i + 1;
                            btr.AppendEntity(line);
                            tr.AddNewlyCreatedDBObject(line, true);
                        }
                    }
                    tr.Commit();
                }
            }
            catch (System.Exception exn)
            {
                ed.WriteMessage("\n" + exn.Message);
            }
        }