Tuesday, March 5, 2024

Showing Intersecting Polygon in Jig's Dragging

A recent discussion in the AutoCAD .NET forum raises question about how to show a dynamic area visually during jig's dragging. 


The OP tried to use Hatch to present the dynamic area. However, creating Hatch entity could only be completed by appending necessary HatchLoops, which require database-residing entities playing the role of the loop. So, it is imagine-able that using Hatch to show a dynamic area during Jig's dragging operation, AutoCAD needs to adding HatchLoop entity or entities into database and then erase it repeatedly in high rate (depending how fast the user drags the mouse), therefore the operation would be sticky, as expected, or even not practically usable. This reminds me one of my old article about a custom "AREA" command, in that article I also used Hatch to show dynamic area in a "Jig" style.

In the case of the OP, the Jig would drag a closed curve, or a region, entity against a collection of regions (or simply a collection of closed curves), during the dragging, the dynamically identifiable intersecting areas should be visually presented. Obviously, region entity would be used inside the Jig's code for the boolean-intersecting calculation in order to identify the intersecting areas.

For simplicity, I create a DrawJig with the inputs of a closed polyline (as the moving area) and a collection of regions (as the target areas that could be intersecting the moving area).

Here is the class PolygonIntersectingJig:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
using System;
using System.Collections.Generic;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace AreaIntersectionJig
{
    public class PolygonIntersectingJig : DrawJig, IDisposable
    {
        private CadDb.Polyline _polygon;
        private List<Region> _regions;
 
        private Region _movingRegion;
        private List<Region> _intersectingRegions = new List<Region>();
 
        private Point3d _basePoint;
        private Point3d _currPoint;
 
        public PolygonIntersectingJig(
            CadDb.Polyline polygon, Point3d basePoint, List<Region> regions)
        {
            _polygon = polygon;
            _regions= regions;
            _movingRegion = CreateRegionFromPolyline(polygon);
            _basePoint= basePoint;
            _currPoint= basePoint;
        }
 
        public Point3d JigPoint => _currPoint;
 
        public void Dispose()
        {
            ClearIntersectingAreas();
            _movingRegion.Dispose();
        }
 
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var opt = new JigPromptPointOptions("\nMove to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.Cursor = CursorType.RubberBand;
 
            var res = prompts.AcquirePoint(opt);
            if (res.Status==PromptStatus.OK)
            {
                if (res.Value.Equals(_currPoint))
                {
                    return SamplerStatus.NoChange;
                }
                else
                {
                    var mt = Matrix3d.Displacement(_currPoint.GetVectorTo(res.Value));
                    _movingRegion.TransformBy(mt);
                    _currPoint = res.Value;
 
                    GenerateIntersectingAreas();
 
                    return SamplerStatus.OK;
                }
            }
            else
            {
                return SamplerStatus.Cancel;
            }
        }
 
        protected override bool WorldDraw(WorldDraw draw)
        {
            draw.Geometry.Draw(_movingRegion);
 
            if (_intersectingRegions.Count > 0)
            {
                foreach (var item in _intersectingRegions)
                {
                    draw.Geometry.Draw(item);
                }
            }
            return true;
        }
 
        private Region CreateRegionFromPolyline(CadDb.Polyline poly)
        {
            var dbCol = new DBObjectCollection() { poly };
            var regs = Region.CreateFromCurves(dbCol);
            if (regs.Count>0)
            {
                var region =  (Region)regs[0];
                region.ColorIndex = 2;
                return region;
            }
            else
            {
                throw new ArgumentException(
                    "Selected polyline cannot form a Region object.");
            }
        }
 
        private void ClearIntersectingAreas()
        {
            if (_intersectingRegions.Count > 0)
            {
                foreach (var item in _intersectingRegions) item.Dispose();
            }
        }
 
        private void GenerateIntersectingAreas()
        {
            ClearIntersectingAreas();
 
            foreach (var r in _regions)
            {
                using (var tempRegion = _movingRegion.Clone() as Region)
                {
                    var intersectingRegion = r.Clone() as Region;
                    intersectingRegion.BooleanOperation(
                        BooleanOperationType.BoolIntersect, tempRegion);
                    if (!intersectingRegion.IsNull)
                    {
                        intersectingRegion.ColorIndex = 1;
                        _intersectingRegions.Add(intersectingRegion);
                    }
                }
            }
            CadApp.UpdateScreen();
        }
 
        private Curve FindIntersectingArea(Region region1, Region region2)
        {
            Curve areaCurve = null;
            region1.BooleanOperation(BooleanOperationType.BoolIntersect, region2);
            if (!region1.IsNull)
            {
                areaCurve = region1.ToBoundaryPolyline();
            }
            return areaCurve;
        }
    }
}
Here is the CommandMethod that runs the PolygonIntersectingJig:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(AreaIntersectionJig.MyCommands))]
 
namespace AreaIntersectionJig
{
    public class MyCommands
    {
        [CommandMethod("MyJig")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            var poly = SelectPolygon(editor);
            if (poly.entId.IsNull)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            var regionIds = SelectRegions(editor);
            if (regionIds==null)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var movingPolygon = (Polyline)tran.GetObject(poly.entId, OpenMode.ForWrite);
                var regions = GetRegions(regionIds, tran);
                var jigOk = false;
 
                using (var jig = new PolygonIntersectingJig(movingPolygon, poly.basePt, regions))
                {
                    var res = editor.Drag(jig);
                    if (res.Status == PromptStatus.OK)
                    {
                        var mt = Matrix3d.Displacement(poly.basePt.GetVectorTo(jig.JigPoint));
                        movingPolygon.TransformBy(mt);
 
                        jigOk = true;
                    }
                }
                
                if (jigOk)
                {
                    tran.Commit();
                }
                else
                {
                    tran.Abort();
                }
            }
            editor.UpdateScreen();
        }
 
        private static (ObjectId entId, Point3d basePt) SelectPolygon(Editor ed)
        {
            var opt = new PromptEntityOptions("\nSelect a closed polyline:");
            opt.SetRejectMessage("\nInvalid: not a polyline.");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                var pRes = ed.GetPoint("\nSelect base point for moving:");
                if (pRes.Status == PromptStatus.OK)
                {
                    return (res.ObjectId, pRes.Value);
                }
                else
                {
                    return (ObjectId.Null, new Point3d());
                }
            }
            else
            {
                return (ObjectId.Null, new Point3d());
            }
        }
 
        private static IEnumerable<ObjectId> SelectRegions(Editor ed)
        {
            var vals = new TypedValue[] { new TypedValue((int)DxfCode.Start, "REGION") };
            var filter = new SelectionFilter(vals);
 
            var opt = new PromptSelectionOptions();
            opt.MessageForAdding = "\nSelect regions:";
 
            var res = ed.GetSelection(opt, filter);
            if (res.Status== PromptStatus.OK)
            {
                return res.Value.GetObjectIds();
            }
            else
            {
                return null;
            }
        }
 
        private static List<Region> GetRegions(IEnumerable<ObjectId> regionIds, Transaction tran)
        {
            var regions= new List<Region>();
 
            foreach (var regionId in regionIds)
            {
                var region = (Region)tran.GetObject(regionId, OpenMode.ForRead);
                regions.Add(region);
                
            }
 
            return regions;
        }
    }
}
See the following video clip showing how the dynamic intersecting areas are visually presented during the jig's dragging:


Without using Hatch to show dynamic intersecting areas, the Jig's dragging is responsive quite well. However, the presented dynamic areas might not be as eye-catching "hatched" area, fore sure. But I thought a proper color of the region would be visually satisfying in most cases. One possible solution I could try is to use MPolygon for the visual presentation.

The code here is only meant for showing how to presenting dynamic intersecting areas during Jig's dragging. As for the practical use of it, there could be a few enhancements to make it useful, such as dynamically showing the intersecting areas' area value.

UPDATE
I forgot to include an extension method that returns a Polyline as a region's boundary (Thank you, CubeK, for pointed out in your comment). Here is the code:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using System;
using System.Collections.Generic;
 
namespace AreaIntersectionJig
{
    // Following code is mainly based on the code from Mr. Gilles Chanteau,
    // posted in Autodesk's AutoCAD .NET API discussion forum
    // https://forums.autodesk.com/t5/net/create-the-outermost-border-for-the-curves-boundary/td-p/12164598
    // I modified the code slightly to make it an Extension Method to be used in my article
    public static class RegionExtension
    {
        private struct Segment
        {
            public Point2d StartPt { get; set; }
            public Point2d EndPt { get; set; }
            public double Bulge { get; set; }
        }
        public static Polyline ToBoundaryPolyline(this Region reg)
        {
                
 
            var segments = new DBObjectCollection();
            reg.Explode(segments);
 
            var segs = new List<Segment>();
            var plane = new Plane(Point3d.Origin, reg.Normal);
            for (int i = 0; i < segments.Count; i++)
            {
                if (segments[i] is Region r)
                {
                    r.Explode(segments);
                    continue;
                }
                Curve crv = (Curve)segments[i];
                Point3d start = crv.StartPoint;
                Point3d end = crv.EndPoint;
                double bulge = 0.0;
                if (crv is Arc arc)
                {
                    double angle = arc.Center.GetVectorTo(start).
                        GetAngleTo(arc.Center.GetVectorTo(end), arc.Normal);
                    bulge = Math.Tan(angle / 4.0);
                }
                segs.Add(
                    new Segment 
                    { 
                        StartPt = start.Convert2d(plane), 
                        EndPt = end.Convert2d(plane), 
                        Bulge = bulge 
                    });
            }
 
            foreach (DBObject o in segments) o.Dispose();
 
            var pline = new Polyline();
            pline.AddVertexAt(0, segs[0].StartPt, segs[0].Bulge, 0.0, 0.0);
            Point2d pt = segs[0].EndPt;
            segs.RemoveAt(0);
            int vtx = 1;
            while (true)
            {
                int i = segs.FindIndex((s) => s.StartPt.IsEqualTo(pt) || s.EndPt.IsEqualTo(pt));
                if (i < 0) break;
                Segment seg = segs[i];
                if (seg.EndPt.IsEqualTo(pt))
                    seg = new Segment { StartPt = seg.EndPt, EndPt = seg.StartPt, Bulge = -seg.Bulge };
                pline.AddVertexAt(vtx, seg.StartPt, seg.Bulge, 0.0, 0.0);
                pt = seg.EndPt;
                segs.RemoveAt(i);
                vtx++;
            }
            pline.Closed = true;
            return pline;
        }
    }
}

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.


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.