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:






2 comments:

Anonymous said...

Hi Norman,

Do you have any idea how to explode DBText/MText through the API? There is a routine in the express tools that can do this but I'd like to do this in code without having to call the explode text tool. The express tool calls a worlddraw and I'm trying to explode text within an overrule.

Writer Jordan said...
This comment has been removed by the author.

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.