Sunday, November 5, 2023

Preventing "Explodable" Property of a Block Definition from Being Changed

A recent thread in the AutoCAD .NET API forum discussed a scenario of how to prevent user to use Block Editor to change the "Explodable" property of a block definition (the OP of that discussion thread wants to keep the block as non-explodable). In my reply to that question, I suggested to use SystemVariableChanged even handler to monitor the system variable "BlockEditor" to detect if user opens or close Block Editor.

Well, after digging in deeper, I found I was wrong: when system variable "BlockEditor" changes its value (0 or 1, when the Block Editor is opened, or closed), the SystemVariableChange event is not triggered. According to AutoCAD .NET API, it is not guaranteed that SystemVariableChanged event is triggered when these system variables' value is changed by certain commands. Unfortunately, system variable "BlockEditor" is one of them.

On the other handle, using code to test whether a block definition's "Explodable" property is true/false and change it is rather easy. So the real issue here is to decide when to run the code to detect the change and reverse it back if necessary. In fact, one can trigger the code running with many events that occur with AutoCAD application, document, or database, for example, Application.Idle event, or Document.CommandEnded event. The only issue is, with these event being fired, the chance of "Explodable" property being changed are quite low, so the code would run with nothing being done in most cases. While it is mostly harmless, it would be better to only run code when use does something, in which the "Explodable" property is likely being changed. Obviously, if user opens Block Editor, the chance of "Explodable" property being changed is higher.

With this in mind, I stick with the approach of watching SystemVariableChange events to see what happen when I open and close BlockEditor. Here are what I found:

1. When user opens Block Editor (selecting a block, right-clicking to show context menu and selecting "Block Editor...", or simply entering command "BEDIT"), SystemVariableChanged events fire against following system variables:

USCNAME, CLAYER, VIEWDIR

2. When user closes Block Editor, SystemVariableChanged events fire against following system variables:

UCSNAME, CLAYER, EXTMIN, EXTMAX, CANNOSCALE

So, I thought I can detect if Block Editor is opened or closed when these 2 group of system variables are changed. When Block Editor is detected being opened, I can safely assume its close will be detected, unless the user shuts down AutoCAD without closing it (then even use indeed changes a block's "Explodable" property, it will not take effect unless the Block Editor is closed properly). Therefore, as long as I detected Block Editor is opened and then closed, there is chance the user has changed the "Explodable" property of a block definition, thus, I need to run the code to make sure the change is reversed back after the Block Editor is closed.

The code is rather simple, as shown here:

using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace StopBlockExplosion
{
    public class BlockExplosionGuard
    {
        private readonly IEnumerable<string> _blockNames;
        private bool _enabled = false;
        private bool _blockEditorOn = false;
 
        public BlockExplosionGuard(IEnumerable<stringtargetBlockNames)
        {
            _blockNames = targetBlockNames;
        }
 
        #region public methods
 
        public bool IsEnabled=>_enabled;
        public void Enable(bool enable)
        {
            _enabled= enable;
            if (_enabled)
            {
                CadApp.SystemVariableChanged += CadApp_SystemVariableChanged;
            }
            else
            {
                CadApp.SystemVariableChanged -= CadApp_SystemVariableChanged;
            }
        }
 
        #endregion
 
        #region private method: reverse changed "Explodable" property of the BlockTableRecord
 
        private void VerifyNonExplodableBlocksInCurrentDwg(Document dwg)
        {
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blkTable = (BlockTable)tran.GetObject(
                    dwg.Database.BlockTableId, OpenMode.ForRead);
                foreach (var blkName in _blockNames)
                {
                    if (!blkTable.Has(blkName)) continue;
                    var blk = (BlockTableRecord)tran.GetObject(
                        blkTable[blkName], OpenMode.ForRead);
                    if (blk.Explodable)
                    {
                        CadApp.ShowAlertDialog(
                            $"Block \"{blk.Name} has been accidently changed to EXPLODABLE!\n\n" +
                            "Our CAD standard now forces it back to NON-EXPLODABLE!");
                        blk.UpgradeOpen();
                        blk.Explodable = false;
                    }
                }
 
                tran.Commit();
            }
        }
 
        #endregion
 
        #region private methods
 
        private void CadApp_SystemVariableChanged(
            object sender, Autodesk.AutoCAD.ApplicationServices.SystemVariableChangedEventArgs e)
        {
            var vName = e.Name.ToUpper();
 
            if (!_blockEditorOn)
            {
                // detect Block Editor is turned on
                if (vName == "UCSNAME" || vName == "CLAYER" || vName == "VIEWDIR")
                {
                    var val = (short)CadApp.GetSystemVariable("BLOCKEDITOR");
                    if (val == 1)
                    {
                        _blockEditorOn = true;
                    }
                }
            }
            else
            {
                if (vName == "UCSNAME" || vName == "CLAYER" || 
                    vName == "EXTMIN" || vName =="EXTMAX" || vName=="CANNOSCALE")
                {
                    var val = (short)CadApp.GetSystemVariable("BLOCKEDITOR");
                    if (val == 0)
                    {
                        _blockEditorOn = false;
                        CadApp.Idle += CadApp_Idle;
                    }
                }
            }
        }
 
        private void CadApp_Idle(object sender, EventArgs e)
        {
            CadApp.Idle -= CadApp_Idle;
 
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            using (dwg.LockDocument())
            {
                VerifyNonExplodableBlocksInCurrentDwg(dwg);
            }
        }
 
        #endregion
    }
}

To place the code into work:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(StopBlockExplosion.MyCommands))]
 
namespace StopBlockExplosion
{
    public class MyCommands
    {
        private static BlockExplosionGuard _blkExplosionGuard = null;
 
        [CommandMethod("NoBlkExp")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            if (_blkExplosionGuard==null)
            {
                _blkExplosionGuard = new BlockExplosionGuard(
                    new[] { "TestBlk1""TestBlk2" });
            }
 
            if (!_blkExplosionGuard.IsEnabled)
            {
                _blkExplosionGuard.Enable(true);
                editor.WriteMessage(
                    "\nBlockExplosionGuard is enabled.");
            }
            else
            {
                _blkExplosionGuard.Enable(false);
                editor.WriteMessage(
                    "\nBlockExplosionGuard is disabled.");
            }
        }
    }
}

The video clip below showing the code in action:





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.