Saturday, September 30, 2023

Find Tangential Touch Points of Entities (Beyond ARC/Circle)

A while ago, I post an article on finding tangent points on a Circle or an Arc. Recently, a question was posted in the AutoCAD .NET API discussion, asking how to find 2 edge points if a ray projected from a point towards an entity (in the original question, the entity is a BlockReference, but in general, it could be any entity, Line, Circle, Polyline, Text, Hatch...),as the picture is shown below:


The case is similar to finding tangent point on Circle, but only the target entity could be in any shape (Circle and Arc could be included, of course), so, let me call them tangential points of an Entity from a point away from it. 

When I read the question from the forum, I gave it some thought, but did not find time to solve it with code then, but finally, I have some code running that gives fairly satisfying result. Because of my limited time available, I limited the target entities only to be ARC/CIRCLE/LINE/POLYLINE.

Before diving into the code, here is the video clip showing the result of the code, so that my reader could see if they are interested in the scenarios shown in the video, if yes, then going deep into the code.


After watching the video, the code would be much easier to follow, as shown below.

First, the help class that actually does the work of finding tangential points:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using System;
using System.Collections.Generic;
 
namespace FindTangentialPoints
{
    public class CadUtils
    {
        public static (Point3d? tanPt1, Point3d? tanPt2) GetTangentialPoints(
            Curve curve, Point3d tanLineOrigin)
        {
            List<Point3d> points = null;
            
            try 
            { 
                if (curve is Line)
                {
                    points = GetTangentPointsOnLine(
                        curve as Line, tanLineOrigin);
                }
                else if (curve is Arc)
                {
                    points = GetTangentPointsOnArc(
                        curve as Arc, tanLineOrigin);
                }
                else if (curve is Circle)
                {
                    points = GetTangentPointsOnCircle(
                        curve as Circle, tanLineOrigin);
                }
                else if (curve is Polyline)
                {
                    points = GetTangentPointsOnPolyline(
                        curve as Polyline, tanLineOrigin);
                }
                else
                {
                    throw new NotImplementedException(
                        "The target entity must be LINE/ARC/CIRCLE/POLYLINE!");
                }
            }
            catch
            {
                points = null;
            }
                
            if (points != null && points.Count == 2)
            {
                return (points[0], points[1]);
            }
            else
            {
                return (nullnull);
            }
        }
 
        public static List<Point3d> GetTangentPointsOnArc(
            Arc arc, Point3d originPoint)
        {
            var geArc = arc.GetGeCurve() as CircularArc3d;
            return GetTangentPointsOnArc(geArc, originPoint);
        }
 
        public static List<Point3d> GetTangentPointsOnArc(
            CircularArc3d geArc, Point3d originPoint)
        {
            CalculateTangentPointOfCircularArc(
                geArc, originPoint, out Point3d? tan1out Point3d? tan2);
            if (tan1.HasValue && tan2.HasValue)
            {
                return new List<Point3d> { tan1.Value, tan2.Value };
            }
            if (!tan1.HasValue && !tan2.HasValue)
            {
                return new List<Point3d> { geArc.StartPoint, geArc.EndPoint };
            }
            else
            {
                Point3d pt = tan1.HasValue ? tan1.Value : tan2.Value;
                using (var curve = Curve.CreateFromGeCurve(geArc))
                {
                    if (IsTangetiallyTouched(curve, geArc.StartPoint, originPoint))
                    {
                        return new List<Point3d> { pt, geArc.StartPoint };
                    }
                    else
                    {
                        return new List<Point3d> { pt, geArc.EndPoint };
                    }
                }
            }
        }
 
        public static List<Point3d> GetTangentPointsOnCircle(
            Circle circle, Point3d originPoint)
        {
            if (IsInside(circle, originPoint)) return null;
            CalculateTangentPointOfCircularArc(
                circle.GetGeCurve() as CircularArc3d, 
                originPoint, out Point3d? tan1out Point3d? tan2);
 
            return new List<Point3d> { tan1.Value, tan2.Value };
        }
 
        public static List<Point3d> GetTangentPointsOnLine(
            Line line, Point3d originPoint)
        {
            return new List<Point3d> { line.StartPoint, line.EndPoint };
        }
 
        public static List<Point3d> GetTangentPointsOnPolyline(
            Polyline polyline, Point3d originPoint)
        {
            if (polyline.Closed)
            {
                if (IsInside(polyline, originPoint)) return null;
            }
            else
            {
                // If the point is inside the polyline's area
                using (var poly = polyline.Clone() as Polyline)
                {
                    poly.Closed = true;
                    if (IsInside(poly, originPoint))
                    {
                        return new List<Point3d> 
                        { 
                            polyline.StartPoint, 
                            polyline.EndPoint 
                        };
                    }
                }
            }
 
            // If the point is beyond the polyline's area
            Point3d? tan1 = null;
            Point3d? tan2 = null;
 
            var points=GetPossibleTangentialPoints(polyline, originPoint);
 
            using (var poly = polyline.Clone() as Polyline)
            {
                if (!poly.Closed) poly.Closed = true;
 
                foreach (var point in points)
                {
                    if (IsTangetiallyTouched(poly, point, originPoint))
                    {
                        if (!tan1.HasValue)
                        {
                            tan1 = point;
                        }
                        else
                        {
                            if (!IsTheSamePoint(point, tan1.Value))
                            {
                                tan2 = point;
                            }
                        }
                    }
 
                    if (tan1.HasValue && tan2.HasValue) break;
                }
            }
 
            if (tan1.HasValue && tan2.HasValue)
            {
                return new List<Point3d> { tan1.Value, tan2.Value };
            }
            else
            {
                return null;
            }
        }
 
        #region private methods:
 
        private static bool IsTheSamePoint(Point3d p1, Point3d p2)
        {
            var dist=p1.DistanceTo(p2);
            return dist <= Tolerance.Global.EqualPoint;
        }
 
        private static List<Point3d> GetPossibleTangentialPoints(
            Polyline poly, Point3d originPt)
        {
            var points=new List<Point3d>();
 
            DBObjectCollection segsnew DBObjectCollection();
            poly.Explode(segs);
            foreach (DBObject seg in segs)
            {
                var line = seg as Line;
                if (line!=null)
                {
                    points.Add(line.StartPoint);
                    points.Add(line.EndPoint);
                }
                var arc = seg as Arc;
                if (arc!=null)
                {
                    var tanPts = GetTangentPointsOnArc(arc, originPt);
                    if (tanPts!=null)
                    {
                        points.AddRange(tanPts);
                    }
                }
            }
 
            return points;
        }
 
        private static void CalculateTangentPointOfCircularArc(
            CircularArc3d arc, Point3d point,
            out Point3d? tangent1out Point3d? tangent2)
        {
            tangent1 = null;
            tangent2 = null;
 
            var dist = point.DistanceTo(arc.Center);
            if (dist < arc.Radius) return;
 
            var angle = Math.Acos(arc.Radius / dist);
 
            using (var line = new Line(arc.Center, point))
            {
                var angle1 = line.Angle + angle;
                var angle2 = line.Angle - angle;
                using (var circle = new Circle(arc.Center, arc.Normal, arc.Radius))
                {
                    var arcLen = angle1 * arc.Radius;
                    var pt = circle.GetPointAtDist(arcLen);
                    if (arc.IsOn(pt))
                    {
                        tangent1 = pt;
                    }
                    arcLen = angle2 * arc.Radius;
                    pt = circle.GetPointAtDist(arcLen);
                    if (arc.IsOn(pt))
                    {
                        tangent2 = pt;
                    }
                }
            }
        }
 
        private static bool IsTangetiallyTouched(
            Curve curve, Point3d ptOnCurve, Point3d ptOrigin)
        {
            var pts = new Point3dCollection();
 
            using (var ray = new Ray())
            {
                ray.BasePoint = ptOrigin;
                ray.SecondPoint = ptOnCurve;
 
                curve.IntersectWith(
                    ray, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
            }
 
            return pts.Count == 1;
        }
 
        private static bool IsInside(Curve curve, Point3d pt)
        {
            if (!curve.Closed) return false;
            var inside = false;
            using (var ray=new Ray())
            {
                ray.BasePoint = pt;
                var ext = curve.GeometricExtents;
                var h = ext.MaxPoint.Y - ext.MinPoint.Y;
                var w=ext.MaxPoint.X-ext.MinPoint.X;
                var l = Math.Max(h, w);
                ray.SecondPoint=new Point3d(pt.X+2*l, pt.Y+2*l, pt.Z);
 
                var pts = new Point3dCollection();
                curve.IntersectWith(
                    ray, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
 
                if (pts.Count > 0)
                {
                    inside = pts.Count == 1 ? true : pts.Count % 2 != 0;
                }
            }
            return inside;
        }
 
        #endregion
    }
}

Then the code that does the fancy jig-style work of dynamically drawing the rays passing through the tangential points:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
 
namespace FindTangentialPoints
{
    public class TangentialLines : IDisposable
    {
        private readonly TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
        private readonly Document _dwg;
        private readonly Database _db;
        private readonly Editor _ed;
 
        private Ray _tanLine1 = null;
        private Ray _tanLine2 = null;
        private DBPoint _tanPt1 = null;
        private DBPoint _tanPt2 = null;
 
        private Curve _curve = null;
 
        public TangentialLines()
        {
            _dwg = Application.DocumentManager.MdiActiveDocument;
            _db = _dwg.Database;
            _ed = _dwg.Editor;
        }
 
        public void DrawTangentialLines(ObjectId curveId)
        {
            short ptMode = (short)Application.GetSystemVariable("PDMODE");
            Application.SetSystemVariable("PDMODE", 34);
 
            try
            {
                _ed.PointMonitor += Editor_PointMonitor;
                using (var tran = _db.TransactionManager.StartTransaction())
                {
                    _curve = (Curve)tran.GetObject(curveId, OpenMode.ForRead);
                    _curve.Highlight();
 
                    var res = _ed.GetPoint("\nSelect tangential line's origin:");
                    if (res.Status == PromptStatus.OK)
                    {
                        if (_tanPt1 != null && _tanPt2 != null)
                        {
                            CreateTangentialLines(
                                res.Value, _tanPt1.Position, _tanPt2.Position, tran);
                        }
                    }
                    _curve.Unhighlight();
                    tran.Commit();
                }
 
                _ed.UpdateScreen();
            }
            finally
            {
                _ed.PointMonitor -= Editor_PointMonitor;
                ClearTransients();
                Application.SetSystemVariable("PDMODE", ptMode);
            }
        }
 
        public void Dispose()
        {
            ClearTransients();
        }
 
        #region private methods
 
        private void ClearTransients()
        {
            if (_tanLine1!=null)
            {
                _tsManager.EraseTransient(_tanLine1, new IntegerCollection());
                _tanLine1.Dispose();
            }
            if (_tanLine2 != null)
            {
                _tsManager.EraseTransient(_tanLine2, new IntegerCollection());
                _tanLine2.Dispose();
            }
            if (_tanPt1 != null)
            {
                _tsManager.EraseTransient(_tanPt1, new IntegerCollection());
                _tanPt1.Dispose();
            }
            if (_tanPt2 != null)
            {
                _tsManager.EraseTransient(_tanPt2, new IntegerCollection());
                _tanPt2.Dispose();
            }
        }
 
        private void UpdateTransients(Point3d origin, Point3d pt1, Point3d pt2)
        {
            if (_tanPt1 == null || _tanPt1.IsDisposed)
            {
                _tanPt1 = new DBPoint(pt1);
                _tanPt1.ColorIndex = 1;
                _tsManager.AddTransient(
                    _tanPt1, 
                    TransientDrawingMode.DirectShortTerm, 
                    128, new IntegerCollection());
            }
            else
            {
                _tanPt1.Position = pt1;
                _tsManager.UpdateTransient(_tanPt1, new IntegerCollection());
            }
 
            if (_tanPt2 == null || _tanPt2.IsDisposed)
            {
                _tanPt2 = new DBPoint(pt2);
                _tanPt2.ColorIndex = 1;
                _tsManager.AddTransient(
                    _tanPt2, 
                    TransientDrawingMode.DirectShortTerm, 
                    128, new IntegerCollection());
            }
            else
            {
                _tanPt2.Position = pt2;
                _tsManager.UpdateTransient(_tanPt2, new IntegerCollection());
            }
 
            if (_tanLine1 == null || _tanLine1.IsDisposed)
            {
                _tanLine1 = new Ray();
                _tanLine1.BasePoint = origin;
                _tanLine1.SecondPoint = pt1;
                _tanLine1.ColorIndex = 2;
                _tsManager.AddTransient(
                    _tanLine1, 
                    TransientDrawingMode.DirectShortTerm, 
                    128, new IntegerCollection());
            }
            else
            {
                _tanLine1.BasePoint = origin;
                _tanLine1.SecondPoint = pt1;
                _tsManager.UpdateTransient(_tanLine1, new IntegerCollection());
            }
 
            if (_tanLine2 == null || _tanLine2.IsDisposed)
            {
                _tanLine2 = new Ray();
                _tanLine2.BasePoint = origin;
                _tanLine2.SecondPoint = pt2;
                _tanLine2.ColorIndex = 2;
                _tsManager.AddTransient(
                    _tanLine2, 
                    TransientDrawingMode.DirectShortTerm, 
                    128, new IntegerCollection());
            }
            else
            {
                _tanLine2.BasePoint = origin;
                _tanLine2.SecondPoint = pt2;
                _tsManager.UpdateTransient(_tanLine2, new IntegerCollection());
            }
        }
 
        private void CreateTangentialLines(
            Point3d origin, Point3d tanPt1, Point3d tanPt2, Transaction tran)
        {
            var space = (BlockTableRecord)tran.GetObject(
                _db.CurrentSpaceId, OpenMode.ForWrite);
 
            var line1 = new Line(origin, tanPt1);
            line1.SetDatabaseDefaults();
            line1.ColorIndex = 3;
            space.AppendEntity(line1);
            tran.AddNewlyCreatedDBObject(line1, true);
 
            var line2 = new Line(origin, tanPt2);
            line2.SetDatabaseDefaults();
            line2.ColorIndex = 3;
            space.AppendEntity(line2);
            tran.AddNewlyCreatedDBObject(line2, true);
        }
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            var originPt = e.Context.RawPoint;
            var tanPoints = CadUtils.GetTangentialPoints(_curve, originPt);
            if (tanPoints.tanPt1.HasValue &&  tanPoints.tanPt2.HasValue)
            {
                UpdateTransients(
                    originPt, tanPoints.tanPt1.Value, tanPoints.tanPt2.Value);
                _ed.UpdateScreen();
            }
            else
            {
                ClearTransients();
            }
        }
 
        #endregion
    }
}

Finally, the commands I used to demonstrate the code action:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(FindTangentialPoints.MyCommands))]
 
namespace FindTangentialPoints
{
    public class MyCommands 
    {
        [CommandMethod("DynamicTans")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            var selected = SelectCurve(editor);
            if (selected.IsNull)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            using (var tanLines = new TangentialLines())
            {
                tanLines.DrawTangentialLines(selected);
            }  
        }
 
        [CommandMethod("PickTans")]
        public static void PickPointForTangentialLines()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            var selected = SelectCurve(editor);
            if (selected.IsNull)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            var res = editor.GetPoint("\nSelect point:");
            if (res.Status == PromptStatus.OK)
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var curve = (Curve)tran.GetObject(selected, OpenMode.ForRead);
                    var tanPoints = CadUtils.GetTangentialPoints(curve, res.Value);
                    if (tanPoints.tanPt1.HasValue && tanPoints.tanPt2.HasValue)
                    {
                        DrawingTangentLines(
                            res.Value, 
                            tanPoints.tanPt1.Value, 
                            tanPoints.tanPt2.Value, 
                            dwg.Database.CurrentSpaceId, tran);
                    }
                    else
                    {
                        editor.WriteMessage("\nCannot find tangentail points.");
                    }
                    tran.Commit();
                }
            }
        }
 
        private static ObjectId SelectCurve(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nSelect an entity (ARC/CIRCLE/LINE/POLYLINE):");
            opt.SetRejectMessage("\nInvalid: must be ARC/CIRCLE/LINE/POLYLINE.");
            opt.AddAllowedClass(typeof(Arc), true);
            opt.AddAllowedClass(typeof(Circle), true);
            opt.AddAllowedClass(typeof(Line), true);
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void DrawingTangentLines(
            Point3d origin, Point3d pt1, Point3d pt2, ObjectId spaceId, Transaction tran)
        {
            var space = (BlockTableRecord)tran.GetObject(spaceId, OpenMode.ForWrite);
 
            var line1 = new Line(origin, pt1);
            line1.SetDatabaseDefaults();
            line1.ColorIndex = 2;
            space.AppendEntity(line1);
            tran.AddNewlyCreatedDBObject(line1, true);
 
            var line2 = new Line(origin, pt2);
            line2.SetDatabaseDefaults();
            line2.ColorIndex = 2;
            space.AppendEntity(line2);
            tran.AddNewlyCreatedDBObject(line2, true);
        }
    }
}

Some Points to Explore:

As mentioned, I choose to limited the target "obstacle" entities to ARC/CIRCLE/LINE/POLYLINE to simplify my effort. If the target entity is other types, I'd use get the entity's shape as one of the basic entities for the tangential point calculation. Such as:

1. DBText/MText. Use its bounding box to generate a rectangle polyline. Note, if the text entity is rotated, the rectangle bounding polyline should be generated by rotating the text entity to 0 degree and then transform the rectangle to the text entity's rotation angle.

2. Hatch. Find its outer loop as a Polyline.

3. A group of entities of mixed types. The possible approach would be

a. find tangential points of each entity in the group and place all the points in a collection, say a List<Point3d>;

b. randomly choose one and create a ray from the origin point;

c. create a ray with each of the other points, get the angle between this ray and the first ray to determine the largest angles clockwise and counterclockwise to eventually decide the outmost 2 tangential points of these entities in the group.

4. BlockReference. Explode the block reference and do the tangential point calculation on individual elements, then do as described in 3.

The source is available for download here.


5 comments:

Kerry Brown said...

Very nice Norman.
Almost has a gameengine pathfinder feel with the dynamicTans routine :)

Regards,

CubeK said...

Nicely done! exactly what I was looking for.
educational material. thanks alot sir

didier A said...

you always surprise me and I love it! bravo for the tenacity and the work provided.
Could you fix the "bug" that prohibits viewing your videos in "full screen". THANK YOU VERY MUCH

Norman Yuan said...

I have no issue viewing the video clip in full screen mode with either Edge or Chrome browser.

3deducators said...

3D EDUCATORS offers a comprehensive training in pakistan Training program that delves into Civil, Mechanical, Architectural, and Electrical drawings. Encompassing both 2D and 3D modeling, this course equips participants to create a wide range of diagrams and drawings specific to the unique requirements of these specialized domains.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.