Saturday, December 24, 2022

Getting entity's Bounding Box in UCS

In AutoCAD, the bounding box of an entity is a minimal orthogonal rectangle that enclose the entity. With .NET API, one can get the bounding box of an entity with Entity.GeometricExtents property, which is an Extents3d structure. With COM API, one can call AcadEntity.GetBoundingBox(minPoint, maxPoint) to obtain the bounding box's minimum point (lower-left corner) and maximum point (the upper-right corner).

There was an interesting question asked recently in the AutoCAD VBA discussion forum. The OP wanted to get the bounding box of an entity as it appears in an UCS. Obviously, depending on the transformation of an UCS from WCS (World Coordinate System), the appeared bounding box in current UCS would be different from the Bounding Box calculated with .NET API/COM API, as shown in the pictures below.

The bounding box of an Hatch entity obtained through API (Entity.GeometricExtents, or AcadEntity.GetBoundingBox()), is shown in red:


When an UCS is set current, the appeared bounding box (which was what the OP of aforementioned forum discussion wanted) is shown in yellow:


So, the question here is how to find the corner coordinates of the UCS-appeared bound box in the current UCS.

It turns out, with AutoCAD .NET API support, the calculation is rather easy: simply transform the entity with inverse Matrix3d of the current UCS and then get the transformed entity's GeometricExtents. Following code shows how to (only 2 lines of code in red does the trick!):
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(UcsToWcsTest.MyCommands))]
 
namespace UcsToWcsTest
{
    public class MyCommands
    {
        [CommandMethod("GetUcsBox")]
        public static void RunCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var entId = SelectEntity(ed);
            if (entId.IsNull)
            {
                ed.WriteMessage("\n*Cancel*");
                return;
            }
 
            GetUcsBoundingBox(entId, ed);
        }
 
        private static ObjectId SelectEntity(Editor ed)
        {
            var res = ed.GetEntity("\nSelect an entity:");
            if (res.Status == PromptStatus.OK)
                return res.ObjectId;
            else
                return ObjectId.Null;
        }
 
        private static void GetUcsBoundingBox(ObjectId entId, Editor ed)
        {
            var db = entId.Database;
            Extents3d bound;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var space = (BlockTableRecord)tran.GetObject(
                    db.CurrentSpaceId, OpenMode.ForWrite);
                var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
 
                using (var entCopy = ent.Clone() as Entity)
                {
                    entCopy.TransformBy(ed.CurrentUserCoordinateSystem.Inverse());
                    bound = entCopy.GeometricExtents;
                }
 
                tran.Commit();
            }
 
            var pt = bound.MinPoint;
            ed.WriteMessage($"\nFirst corner: {PointToString(pt)}");
 
            pt = new Point3d(bound.MinPoint.X,bound.MaxPoint.Y, bound.MinPoint.Z);
            ed.WriteMessage($"\nSecond corner: {PointToString(pt)}");
 
            pt = bound.MaxPoint;
            ed.WriteMessage($"\nThird corner: {PointToString(pt)}");
 
            pt = new Point3d(bound.MaxPoint.X, bound.MinPoint.Y, bound.MinPoint.Z);
            ed.WriteMessage($"\nFourth corner: {PointToString(pt)}");
        }
 
        private static string PointToString(Point3d pt)
        {
            var x = Converter.DistanceToString(pt.X);
            var y= Converter.DistanceToString(pt.Y);
 
            return $"({x}{y})";
        }
    }
}

Noted that I used a copy of the entity for the calculation, so that the real entity is not changed/transformed. Otherwise it would need to be transformed back.

See the video click below for the effect of the code execution.


Since the aforementioned question was asked in AutoCAD VBA forum, then, how can we find out the appeared bounding box in UCS with VBA code? Well, unlike AutoCAD .NET API's support to solve this problem, it is a bit complicated with VBA/AutoCAD COM API, or at least, not straightforward. Because I have posted the VBA code in the VBA discussion forum, go to here to take a look, if interested in:

https://forums.autodesk.com/t5/vba/lisp-code-to-vba-code-conversion/td-p/11561281

 


Thursday, November 17, 2022

Loop Operation with SendStringToExecute()

While programming an AutoCAD plugin, there are many occasions we use Document.SendStringToExecute() method to existing command, which can be built-in commands or custom commands (including lisp-defined commands). 

Using SendStringToExecute() could save a lot of programming effort for particular AutoCAD operations when we know there are existing commands/lisp routines that does exactly the work, especially when the existing commands/lisp routines do some work that we programmers cannot easily do with our own code, and in many case, why reinventing the wheel anyway?

However, due to its "async" nature, it cannot be used in middle of a chunk of our code and expect the code following it can operate on the result produced from the SendStringToExecute() call. For example, if we need to call SendStringToExecute() repeatedly as loop in a custom operation of our code, following code simply does not work:

private void DoRepeatedWork(IEnumerable<ObjectId> entIds)
{
    var dwg = Application.DocumentManager.MdiActiveDocument;
    foreach (var entId in entIds)
    {
        // set PickFirst selections
 
        dwg.SendStringToExecute("SomeCommand\n"falsefalsefalse);
 
        // Do something with the result of SendStringToExecute()
 
    }
}

The key to be able to repeatedly call SendStringToExecute() is to know when the command/lisp routine executed by SendStringToExecute() ends. Fortunately, in the .NET API, there are Document.CommandEnded/Cancelled and Document.LispEnded/Cancelled events for us to handle, so that we know when command/lisp routine execution ends, thus, we can repeatedly call SendStringToExecute() in a loop.

Following code example shows how to use SendStringToExecute() to run an command repeatedly against a collection of entities, one at a time. I created a command "ThirdPartyCmd" to mimic a third party command, which we really do not know (or do not need to know) how it does its work inside. But for the sake to show the "third party" command's effect, I let the command only accept one Line entity as PickFirst selection and rotate it, change its color.

#region Handle CommandEnded for looping
 
private ObjectId[] _selectedLines = null;
private int _currentIndex = 0;
 
private Document _thisDwg = null;
 
[CommandMethod("CmdLoop", CommandFlags.Session)]
public void RunLoopingCommand()
{
    _thisDwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = _thisDwg.Editor;
 
    // Select target entities when the command loop begins
    if (_selectedLines == null)
    {
        var filter = new SelectionFilter(
            new[] { new TypedValue((int)DxfCode.Start, "LINE") });
        var res = ed.GetSelection(filter);
        if (res.Status == PromptStatus.OK)
        {
            _selectedLines = res.Value.GetObjectIds();
        }
        else
        {
            return;
        }
 
        _currentIndex = 0;
    }
    
    // if necessary, do something with the 
    // entity that was processed by SendStringToExecute()
    if (_currentIndex>0)
    {
        //DoSomething(_selectedLines[_currentIndex-1]);
    }
 
    _thisDwg.Editor.SetImpliedSelection(new[] { _selectedLines[_currentIndex] });
    _thisDwg.CommandEnded += ThirdPartyCmd_Ended;
    _thisDwg.SendStringToExecute("ThirdPartyCmd\n"falsefalsefalse);
}
 
private void ThirdPartyCmd_Ended(object sender, CommandEventArgs e)
{
    if (e.GlobalCommandName.ToUpper() != "THIRDPARTYCMD"return;
 
    _thisDwg.CommandEnded -= ThirdPartyCmd_Ended;
 
    _thisDwg.Editor.WriteMessage(
        $"\nLine being processed by ThirdPartyCmd: {_selectedLines[_currentIndex]}");
 
    // increment index
    _currentIndex++;
    _thisDwg.Editor.WriteMessage(
        $"\nThirdPartyCmd execution count: {_currentIndex}");
 
    // End the loop after last target entity is done.
    if (_currentIndex >= _selectedLines.Length)
    {
        _selectedLines= null;
        return;
    }
 
    //Back to original command for another loop
    _thisDwg.SendStringToExecute("CmdLoop\n"truefalsetrue);
}
 
[CommandMethod("ThirdPartyCmd", CommandFlags.UsePickSet)]
public void RotateEntity()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.SelectImplied();
    
    ed.WriteMessage("\nRunning third party command...,");
    if (res.Status == PromptStatus.OK)
    {
        ed.WriteMessage($"\nProcessing selected entity: {res.Value[0].ObjectId}");
 
        using(var tran=dwg.TransactionManager.StartTransaction())
        {
            var line = (Line)tran.GetObject(res.Value[0].ObjectId, OpenMode.ForWrite);
            line.ColorIndex = new Random().Next(0, 255);
            line.TransformBy(Matrix3d.Rotation(
                Math.PI / 2.0, Vector3d.ZAxis, line.StartPoint));
            tran.Commit();
        }
    }
    
    ed.WriteMessage("\ndone!");
}
 
#endregion

The code is quite self-explanatory. See this video clip see the code running result:


This post is actually inspired by this discussion in the .NET forum. The original poster want to execute command "TxtExp" (exploding text, from Express Tools) repeatedly against a selection of text entities, one at a time, and between the "TxtExp" calls, he/she also want to do something with the exploding-generated entities. He/she also referred an article posted by famous Kean Walmsley about using "TxtExp". "TxtExp" is a lisp command from the Express Tool suite. While it is certainly possible to write our own code to do the same text exploding work, but it would be quite complicated. If we could just use it in our code to achieve our goal, we can save a lot of time/effort. So, I gave it a try, use the same approach as above code shows. 

The difference is that handling CommandEnded event is changed to handling LispEnded. Also, since we know the "TxtExp" (one can study the lisp routine "txtexp.lsp" in "Express" folder for some inside of this command) command generating a bunch of geometry entities from the selected entities, adding them into database, and then erasing the text, we can somehow collect these "exploded" geometry entities between "TxtExp" calls and do something as needed. Again, following code are quite easy to follow, so no extra explanation is needed:

#region Handle LispEnded for looping
 
        private ObjectId[] _selectedTexts = null;
        private List<ObjectId> _explodedEntities = null;
        private int _index = 0;
        private Document _dwg = null;
 
        [CommandMethod("ExplodeTexts")]
        public void ExplodeTextsToGeometryEntities()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = _dwg.Editor;
 
            if (_selectedTexts == null)
            {
                var filter = new SelectionFilter(new TypedValue[]
                {
                new TypedValue((int)DxfCode.Start, "TEXT")
                });
 
                var res = ed.GetSelection(filter);
                if (res.Status != PromptStatus.OK) return;
 
                _selectedTexts = res.Value.GetObjectIds();
 
                _index= 0;
            }
 
            if (_index>0)
            {
                UpdateExplodedEntities();
            }
 
            _dwg.LispEnded += Lisp_Ended;
            _dwg.Database.ObjectAppended += Entity_Appended;
 
            _explodedEntities = new List<ObjectId>();
            
            ed.SetImpliedSelection(new[] { _selectedTexts[_index] });
            _dwg.SendStringToExecute("TXTEXP\n"falsefalsefalse);
        }
 
        private void Lisp_Ended(object sender, EventArgs e)
        {
            ObjectId id = _selectedTexts[_index];
            if (id.IsErased)
            {
                _dwg.LispEnded -= Lisp_Ended;
                _dwg.Database.ObjectAppended -= Entity_Appended;
 
                _dwg.Editor.WriteMessage($"\nExploding text count: {_index + 1}");
                _index++;
 
                // end looping
                if (_index >= _selectedTexts.Length)
                {
                    _selectedTexts = null;
                    return;
                }
 
                // Do next loop
                _dwg.SendStringToExecute("ExplodeTexts\n"falsefalsefalse);
            }
        }
 
        private void Entity_Appended(object sender, ObjectEventArgs e)
        {
            if (e.DBObject is Entity)
            {
                _explodedEntities.Add(e.DBObject.ObjectId);
            }
        }
 
        private void UpdateExplodedEntities()
        {
            var color = new Random().Next(0, 255);
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                foreach (var entId in _explodedEntities)
                {
                    if (!entId.IsErased)
                    {
                        var ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
                        ent.ColorIndex = color;
                    }
                }
                tran.Commit();
            }
        }
 
        #endregion


See the video clip below showing the effect of the code running:


Obviously I omitted the code to handle possible CommandCancelled/LispCancelled event, if the command/lisp routine to be executed could be cancelled by design, we may want to handle this event if necessary.

Wednesday, October 5, 2022

Extract Entity Image From Side-Loaded Database

There have been some discussions about getting image of an entity, and code samples could be found online. Most of them do the work with the entities in a drawing opened in AutoCAD.

I published 2 articles that could prove to be useful for this discussion:

In these 2 articles, I showed how we can use Autodesk.Autodesk.Internal.Utils.GetBlockImage() to generate the image of a block without having to open the drawing in AutoCAD as Document.

Seeing the question posted in the .NET discussion forum about exporting image of single entity, I thought it should be possible to use the same approach to do the job. With a short brain storm, I came up these steps to do this job:

1. As the user for drawing file name where the entity images to be extracted from;
2. Ask the user to select a folder to save the extracted images;
3. Open the drawing as side database;
4. Create an empty BlockTableRecord as a container to hold an entity from which the image is to be generated;
5. Loop through ModelSpace for each entity
a. clear entity from the container BlockTableRecord;
b. add a clone of the entity to the container BlockTableRecord;
c. Call GetBlockImage() method against the container BlockTableRecord;
d. Save the obtained imager;

With these steps in mind, I went ahead to implement them in code.

The class EntityImageExtracter:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Internal;
using System;
using System.Linq;
 
namespace ExtractEntityImages
{
    public class EntityImageExtracter
    {
        private string _sourceDwg;
        private string _outputFolder;
 
        public EntityImageExtracter()
        {
 
        }
 
        // Notify the calling process
        public static Action<int> ExtractionStarted;
        public static Action<int> ExtractionProgressed;
        public static Action<int> ExtractionEnded;
 
        public void ExtractEntityImages(string sourceDwgstring outputFolder)
        {
            _sourceDwg = sourceDwg;
            _outputFolder = outputFolder;
 
            using (var db = new Database(falsetrue))
            {
                db.ReadDwgFile(_sourceDwg, FileOpenMode.OpenForReadAndAllShare, falsenull);
                ExtractImagesFromDatabase(db);
            }
        }
 
        #region private methods
 
        private void ExtractImagesFromDatabase(Database db)
        {
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForRead);
                var ids = model.Cast<ObjectId>();
 
                ExtractionStarted?.Invoke(ids.Count());
 
                var blkDef = CreateNewBlockTableRecord(db.BlockTableId, tran);
 
                var doneCount = 0;
                var count = 0;
                foreach(var id in ids)
                {
                    count++;
                    ExtractionProgressed?.Invoke(count);
 
                    var img = ExtractEntityImage(id, blkDef, tran);
                    if (img!=null)
                    {
                        //Save the image to file
                        var imgFile = $"{id.ObjectClass.DxfName}_{id.Handle.ToString()}.png";
                        img.Save(System.IO.Path.Combine(_outputFolder, imgFile));
                        doneCount++;
                        img.Dispose();
                    }
                }
 
                ExtractionEnded?.Invoke(doneCount);
 
                tran.Abort();
            }
        }
 
        private BlockTableRecord CreateNewBlockTableRecord(ObjectId btId, Transaction tran)
        {
            var bt = (BlockTable)tran.GetObject(btId, OpenMode.ForWrite);
 
            // make sure no duplicated block name
            var name = "ExtractImage";
            var i = 1;
            var blkName = $"{name}_{i}";
            while(bt.Has(blkName))
            {
                i++;
                blkName = $"{name}_{i}";
            }
 
            var blk = new BlockTableRecord();
            blk.Name = blkName;
            blk.Origin = Point3d.Origin;
 
            bt.Add(blk);
            tran.AddNewlyCreatedDBObject(blk, true);
 
            return blk;
        }
 
        private System.Drawing.Image ExtractEntityImage(
            ObjectId entId, BlockTableRecord tempBlk, Transaction tran)
        {
            var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
            var clone = ent.Clone() as Entity;
 
            // Use the center of the entity's extents at the block's origin
            MoveCenterToOrigin(clone);
            
            foreach (ObjectId id in tempBlk)
            {
                var dbObj = tran.GetObject(id, OpenMode.ForWrite);
                dbObj.Erase();
            }
 
            tempBlk.AppendEntity(clone);
            tran.AddNewlyCreatedDBObject(clone, true);
 
            // generate the image of the block definition
            // where only one entity exists, thus, the image of the entity
            var cl = Autodesk.AutoCAD.Colors.Color.FromColor(
                System.Drawing.Color.WhiteSmoke);
            var blkId = tempBlk.ObjectId;
            var imgPtr = Utils.GetBlockImage(blkId, 300, 300, cl);
            var image = System.Drawing.Image.FromHbitmap(imgPtr);
 
            return image;
        }
 
        private void MoveCenterToOrigin(Entity ent)
        {
            var exts = ent.GeometricExtents;
            var w = exts.MaxPoint.X - exts.MinPoint.X;
            var h= exts.MaxPoint.Y - exts.MinPoint.Y;
 
            var center = new Point3d(
                exts.MinPoint.X + w / 2.0, 
                exts.MinPoint.Y + h / 2.0, 
                exts.MinPoint.Z);
            var mt = Matrix3d.Displacement(center.GetVectorTo(Point3d.Origin));
            ent.TransformBy(mt);
        }
 
        #endregion
    }
}
The code is rather simple and straightforward. Following are the other code to make the process runnable.

A helper class for user to select drawing file and output folder:
using System.IO;
using System.Windows.Forms;
 
namespace ExtractEntityImages
{
    public class GenericHelper
    {
        public static string SelectSourceDrawingFile()
        {
            var fName = "";
            using (var dlg = new OpenFileDialog())
            {
                dlg.Title = "Select Drawing File";
                dlg.Filter = "AutoCAD Drawing (*.dwg)|*.dwg";
                dlg.Multiselect = false;
                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    fName = dlg.FileName;
                }
            }
            return fName;
        }
 
        public static string SelectImageOutputFolder(string initPath="")
        {
            var folder = "";
 
            using (var dlg = new FolderBrowserDialog())
            {
                dlg.Description = "Select Image Output Folder";
                if (!string.IsNullOrEmpty(initPath))
                {
                    dlg.SelectedPath = initPath;
                }
                dlg.ShowNewFolderButton = true;
                if (dlg.ShowDialog()== DialogResult.OK)
                {
                    folder = dlg.SelectedPath;
                }
            }
 
            return folder;
        }
 
        public static void ClearImageOutputFolder(string path)
        {
            var files=Directory.GetFiles(path);
            if (files.Length>0)
            {
                foreach (var f in files)
                {
                    File.Delete(f);
                }
            }
        }
    }
}
The Command class to run the process:
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(ExtractEntityImages.MyCommands))]
 
namespace ExtractEntityImages
{
    public class MyCommands
    {
        [CommandMethod("GetEntImages")]
        public static void ExtractEntityImagedFromDrawing()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            string sourceDwg = GenericHelper.SelectSourceDrawingFile();
            if (string.IsNullOrEmpty(sourceDwg))
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            var initPath = dwg.IsNamedDrawing ?
                System.IO.Path.GetDirectoryName(dwg.Name) : "";
            var outputFolder = GenericHelper.SelectImageOutputFolder(initPath);
            if (string.IsNullOrEmpty(outputFolder))
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            GenericHelper.ClearImageOutputFolder(outputFolder);
 
            ProgressMeter meter = null;
            try
            {
                var extractedCount = 0;
 
                EntityImageExtracter.ExtractionStarted = (count) =>
                {
                    meter = new ProgressMeter();
                    meter.SetLimit(count);
                    meter.Start("Extracting entity images...");
                };
 
                EntityImageExtracter.ExtractionProgressed = (index) =>
                {
                    meter.MeterProgress();
                };
 
                EntityImageExtracter.ExtractionEnded = (totalCount) =>
                {
                    extractedCount = totalCount;
                };
 
                var extracter = new EntityImageExtracter();
                extracter.ExtractEntityImages(sourceDwg, outputFolder);
 
                ed.WriteMessage(
                    $"\nExtracted image count: {extractedCount}\n");
            }
            catch(System.Exception ex)
            {
                ed.WriteMessage(
                    $"\nError:\n{ex.Message}\n");
            }
            finally
            {
                if (meter!=null)
                {
                    meter.Stop();
                    meter.Dispose();
                }
            }
        }
    }
}

Since the entire process of generating image is done with an side-loaded database, so I did not bother to remove the container BlockTableRecord, as long as the side database is not to be saved for the changes.

I only test it with simple drawing, as following video clip shows, the result satisfies me with its speed and the quality of the image. The only thing in the whole thing that might be potentially problematic is the key factor of the approach - the method GetBlockImage() - is from Autodesk.AutoCAD.Internal namespace, meaning use it at your own risk, if Autodesk wants to break it for whatever reason.

See the video clip bellow:









Tuesday, September 27, 2022

Selecting a Segment of a Polyline Visually

By arriving at New Orleans for AU2022 early I planned to explore the The Big Easy city for its unique scene and culture. But the unbearable heat (at least to me, as Canadian from far up north) forced me staying air-conditioned indoor more than I wanted. As a runner, I also planned some morning runs whenever I am in a different city, but I decided I'd better not run here to avoid unexpected heat stroke. Thus I end up having a bit more time to write some code for a question I saw in the .NET discussion forum here. While the answer to that question is fairly simple, but it still needs to be better explained with code, thus this post.

The question asked there actually involves 2 different programming tasks: identifying the selected segment of a polyline (e.g. the user should select a polyline, and the code then determine which segment of the polyline is actually clicked); and showing a visual hint to the user to indicate which segment is selected. With these 2 separate coding tasks in mind, the code then can be structured easily.

First, for identifying selected segment, I create the class PolylineSegmentSelector, which is really simple - if the user selected a polyline, based on where the picked point is, the segment's index is calculated:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace MiscTest
{
    public class PolylineSegmentSelector
    {
        private Document _dwg;
        private Editor _ed;
        private Database _db;
 
        public ObjectId SelectedPolyLine { private setget; } = ObjectId.Null;
        public int SelectedSegment { private setget; } = -1;
 
        public bool SelectSegment(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
            _db = dwg.Database;
 
            SelectedPolyLine = ObjectId.Null;
            SelectedSegment = -1;
 
            var opt = new PromptEntityOptions(
                "\nSelect target segment of a polyline:");
            opt.SetRejectMessage("\nInvalid selection: not a polyline.");
            opt.AddAllowedClass(typeof(CadDb.Polyline), true);
            var res = _ed.GetEntity(opt);
            if (res.Status != PromptStatus.OK) return false;
 
            SelectedPolyLine = res.ObjectId;
            SelectedSegment = FindSelectedSegment(res.ObjectId, res.PickedPoint);
 
            return true;
        }
 
        private int FindSelectedSegment(ObjectId polyId, Point3d pickedPt)
        {
            int index = -1;
            using (var tran = 
                polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                var ptOnPoly = poly.GetClosestPointTo(pickedPt, false);
                index = Convert.ToInt32(Math.Floor(poly.GetParameterAtPoint(ptOnPoly)));
                tran.Commit();
            }
 
            return index;
        }
    }
}

Then, the class PolylineSegmentHighlighter, which presents a temporary visual hint withTransient Graphics:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
using System.Collections.Generic;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace MiscTest
{
    public class PolylineSegmentHighlighter : IDisposable
    {
        private class SegmentGhost
        {
            public ObjectId PolylineId { getset; } = ObjectId.Null;
            public int SegmentIndex { getset; } = 0;
            public int ColorIndex { getset; } = 0;
            public Drawable Ghost { getset; } = null;
        }
 
        private readonly List<SegmentGhost> _ghosts = new List<SegmentGhost>();
        private TransientManager _tsManager = TransientManager.CurrentTransientManager;
 
        public void SetSegmentHighlight(ObjectId polyIdint segIndexint colorIndex)
        {
            RemoveSegmentHighlight(polyId, segIndex);
            var newGhost = CreateHighlightGhost(polyId, segIndex, colorIndex);
            _ghosts.Add(newGhost);
            _tsManager.AddTransient(
                newGhost.Ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new Autodesk.AutoCAD.Geometry.IntegerCollection());
        }
 
        public void RemoveSegmentHighlight(ObjectId polyIdint segIndex)
        {
            foreach (var ghost in _ghosts)
            {
                if (ghost.PolylineId == polyId && 
                    ghost.SegmentIndex == segIndex &&
                    ghost.Ghost!=null)
                {
                    _tsManager.EraseTransient(
                        ghost.Ghost, new Autodesk.AutoCAD.Geometry.IntegerCollection());
                    _ghosts.Remove(ghost);
                    ghost.Ghost.Dispose();
                    break;
                }
            }
        }
 
        public void Dispose()
        {
            foreach (var ghost in _ghosts)
            {
                if (ghost.Ghost != null)
                {
                    _tsManager.EraseTransient(
                        ghost.Ghost, new Autodesk.AutoCAD.Geometry.IntegerCollection());
                    ghost.Ghost.Dispose();
                }
            }
            _ghosts.Clear();
        }
 
        private SegmentGhost CreateHighlightGhost(ObjectId polyIdint segIndexint colorIndex)
        {
            Entity ghost = null;
            using (var tran = 
                polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                if (poly.NumberOfVertices > 2)
                {
                    DBObjectCollection ents = new DBObjectCollection();
                    poly.Explode(ents); 
 
                    for (int i = 0; i < ents.Count; i++)
                    {
                        if (i == segIndex)
                        {
                            ghost = ents[i] as Entity;
                        }
                        else
                        {
                            ents[i].Dispose();
                        }
                    }
                }
                else
                {
                    ghost = poly.Clone() as Entity;
                }
                tran.Commit();
            }
 
            if (ghost != null)
            {
                ghost.ColorIndex = colorIndex;
                return new SegmentGhost() 
                { 
                    PolylineId=polyId, 
                    SegmentIndex=segIndex, 
                    ColorIndex = colorIndex, 
                    Ghost=ghost 
                };
            }
            else
            {
                return null;
            }
        }
    }
}

Following CommandMethod demonstrates how to use these 2 classes:

[CommandMethod("SelectPolySeg")]
public static void SelectPolylineSegment()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var selectedPolySegs = new List<(ObjectId polyId, int segIndex)>();
 
    var selector = new PolylineSegmentSelector();
    using (var highlighter = new PolylineSegmentHighlighter())
    {
        int color = 1;
        while(selector.SelectSegment(dwg))
        {
            var selectedPoly = selector.SelectedPolyLine;
            var segIndex = selector.SelectedSegment;
 
            selectedPolySegs.Add((selectedPoly, segIndex));
 
            highlighter.SetSegmentHighlight(selectedPoly, segIndex, color);
            CadApp.UpdateScreen();
            color++;
        }
    }
 
    if (selectedPolySegs.Count>0)
    {
        //do whatever work required for each of the selected segments
    }
}

See the video clip below:






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.