Wednesday, February 27, 2019

Visually Select A Segment Of Polyline

This post is inspired by a recent discussion in Autodesk's .NET discussion forum.

With .NET API, when user picks a polyline, it is fairly easy to figure out where on the polyline use picked, that is, which segment between 2 vertices is picked. However, if we code a process and ask user to select certain segment of a polyline, we need to provide good visual hint for user interaction, so that use would be able to explicitly make his/her selection. In the case of asking user to select a certain segment of polyline, there is no direct and simple API methods to use, but I thought it could achieved with a bit of coding, when I saw the question was raised in the discussion forum, and decided I would give it a try if no one would offer concrete solution later.

Now, I have found a bit time and completed some code which does what I thought is the answer to the original question. This time, let see the video showing how the code visually indicates a segment of polyline is selected first. Here is the video clip.

Here are some considerations to bear in mind:

1. Use Editor.GetEntity() for picking, with filter set to only allow polyline being selected;
2. Start handling Editor.PointMonitor event right before Editor.GetEntity() is called, so that when user moves cursor for selecting, the code would have chance to calculate which segment of the polyline the mouse cursor is hovering on (or not hover on a polyline at all); then draw transient graphics of the segment of the polyline to provide user a visual hint;
3. Use Polyline.GetSplitCurves() to obtain an non-DB residing entity to draw transient graphics and return as the segment selecting result (up to the method caller to either adding the segment entity to database, or dispose it).

Let us look at the code. First the class PartialPolylinePicker:
using System;
using System.Collections.Generic;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectPartialPolyline
{
    public class PartialPolylinePicker : IDisposable
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private ObjectId _polyId = ObjectId.Null;
        private TransientManager _tsMgr = TransientManager.CurrentTransientManager;
        private List<Drawable> _drawables = new List<Drawable>();
 
        public void Dispose()
        {
            ClearTransientGraphics();
        }
 
        public Entity PickPartialPolyline(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
 
            Entity curve = null;
 
            ClearTransientGraphics();
 
            try
            {
                _ed.PointMonitor += Editor_PointMonitor;
 
                var opt = new PromptEntityOptions(
                    "\nSelect polyline:");
                opt.SetRejectMessage("\nInvalid: not a polyline.");
                opt.AddAllowedClass(typeof(Autodesk.AutoCAD.DatabaseServices.Polyline), true);
                
                var res = _ed.GetEntity(opt);
                if (res.Status== PromptStatus.OK)
                {
                    var pickPt = res.PickedPoint;
                    var polyId = res.ObjectId;
                    
                    var vertexIndexes = GetVertexIndexes(pickPt, polyId);
                    curve = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
                  curve=GetPolylineSegmentCurve(pickPt, polyId);
                }             }             finally             {                 _ed.PointMonitor -= Editor_PointMonitor;             }             return curve;         }         private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)         {             ClearTransientGraphics();             var curPt = e.Context.RawPoint;             var polyId = GetClosestPolylineAtPoint(_ed, curPt);             if (!polyId.IsNull)             {                 HighlightPolyline(polyId, curPt);             }         }         #region private methods
private Curve GetPolylineSegmentCurve(
          Point3d pickedPoint, ObjectId polyIdbool hightlight=false)
{
    Autodesk.AutoCAD.DatabaseServices.Curve curve = null;
 
    using (var tran = polyId.Database.TransactionManager.StartTransaction())
    {
        var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
        var index = GetSegmentIndex(pickedPoint, poly);
        curve = GetSegmentCurve(poly, index);
        if (curve!=null && hightlight)
        {
            curve.ColorIndex = poly.ColorIndex != 2 ? 2 : 1;
        }
        tran.Commit();
    }
    
    return curve;
}
 
private int GetSegmentIndex(
           Point3d pickedPosition, Autodesk.AutoCAD.DatabaseServices.Polyline poly)
{
    var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
    var param = poly.GetParameterAtPoint(closestPoint);
    return Convert.ToInt32(Math.Floor(param));
}
 
private Curve GetSegmentCurve(
           Autodesk.AutoCAD.DatabaseServices.Polyline polyint index)
{
    Curve3d geCurve = null;
    var segType = poly.GetSegmentType(index);
    switch (segType)
    {
        case SegmentType.Line:
            geCurve = poly.GetLineSegmentAt(index);
            break;
        case SegmentType.Arc:
            geCurve=poly.GetArcSegmentAt(index);
            break;
    }
 
    if (geCurve!=null)
    {
        return Curve.CreateFromGeCurve(geCurve);
    }
    else
    {
        return null;
    }
}
        private Tuple<intint> GetVertexIndexes(Point3d pickedPosition, ObjectId polyId)
        {
            int first = 0;
            int second = 0;
 
            using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                    tran.GetObject(polyId, OpenMode.ForRead);
 
                var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
                var len = poly.GetDistAtPoint(closestPoint);
 
                for (int i = 1; i < poly.NumberOfVertices - 1; i++)
                {
                    var pt1 = poly.GetPoint3dAt(i);
                    var l1 = poly.GetDistAtPoint(pt1);
 
                    var pt2 = poly.GetPoint3dAt(i + 1);
                    var l2 = poly.GetDistAtPoint(pt2);
 
                    if (len > l1 && len < l2)
                    {
                        first = i;
                        second = i + 1;
                        break;
                    }
                }
 
                tran.Commit();
            }
 
            return new Tuple<intint>(first, second);
        }
 
        private void ClearTransientGraphics()
        {
            if (_drawables.Count>0)
            {
                foreach (var d in _drawables)
                {
                    _tsMgr.EraseTransient(d, new IntegerCollection());
                    d.Dispose();
                }
 
                _drawables.Clear();
            }
        }
 
        private ObjectId GetClosestPolylineAtPoint(Editor ed, Point3d position)
        {
            var returnId = ObjectId.Null;
 
            var selResult = SelectAtPickBox(ed, position);
            if (selResult.Status== PromptStatus.OK)
            {
                var ids = new List<ObjectId>();
                foreach (ObjectId id in selResult.Value.GetObjectIds())
                {
                    if (id.ObjectClass.DxfName.ToUpper()=="LWPOLYLINE")
                    {
                        ids.Add(id);
                    }
                }
 
                if (ids.Count > 0)
                {
                    if (ids.Count == 1)
                    {
                        returnId = ids[0];
                    }
                    else
                    {
                        // If the pick box hover on multiple polyline, find the one that is
                        // closest to the pick box center
                        returnId = FindClosestPolyline(ids, position, ed.Document.Database);
                    }
                }
            }
 
            return returnId;
        }
 
        private PromptSelectionResult SelectAtPickBox(Editor ed, Point3d pickBoxCentre)
        {
            //Get pick box's size on screen
            System.Windows.Point screenPt = ed.PointToScreen(pickBoxCentre, 1);
     
            //Get pickbox's size. Note, the number obtained from
            //system variable "PICKBOX" is actually the half of
            //pickbox's width/height
            object pBox = CadApp.GetSystemVariable("PICKBOX");
            int pSize = Convert.ToInt32(pBox);
    
            //Define a Point3dCollection for CrossingWindow selecting
            Point3dCollection points = new Point3dCollection();
    
            System.Windows.Point p;
            Point3d pt;
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt );
    
            return ed.SelectCrossingPolygon(points);
        }
 
        private ObjectId FindClosestPolyline(IEnumerable<ObjectId> ids, Point3d position, Database db)
        {
            ObjectId polyId = ObjectId.Null;
            double dist = double.MaxValue;
 
            using (var tran = db.TransactionManager.StartOpenCloseTransaction())
            {
                foreach (var id in ids)
                {
                    var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                        tran.GetObject(id, OpenMode.ForRead);
                    var pt = poly.GetClosestPointTo(position, false);
                    var d = pt.DistanceTo(position);
                    if (d < dist)
                    {
                        polyId = id;
                        dist = d;
                    }
                }
 
                tran.Commit();
            }
 
            return polyId;
        }
 
        #endregion
 
        #region private methods: highlight polyline's segment
 
        private void HighlightPolyline(ObjectId polyId, Point3d curPt)
        {
            var vertexIndexes = GetVertexIndexes(curPt, polyId);
            var ent = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
           var ent=GetPolylineSegmentCurve(curPt, polyId, true);
            if (ent != null)             {                 _drawables.Add(ent);                 _tsMgr.AddTransient(ent, TransientDrawingMode.DirectTopmost, 128, new IntegerCollection());             }         }         private Entity GetSplitCurveAtVertex(ObjectId polyId, int vertexIndex)         {             Entity curve = null;             using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())             {                 var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)                     tran.GetObject(polyId, OpenMode.ForRead);                 var vertices = new Point3dCollection();                 for (int i=0; i<poly.NumberOfVertices; i++)                 {                     vertices.Add(poly.GetPoint3dAt(i));                 }                 using (var dbObjs = poly.GetSplitCurves(vertices))                 {                     for (int i=0; i<dbObjs.Count; i++)                     {                         if (i==vertexIndex)                         {                             curve = (Entity)dbObjs[i];                         }                         else                         {                             dbObjs[i].Dispose();                         }                     }                     dbObjs.Clear();                 }                 if (curve!=null)                 {                     curve.ColorIndex = poly.ColorIndex != 1 ? 1 : 2;                 }                 tran.Commit();             }             return curve;         }                  #endregion     } }

Here is the command class:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SelectPartialPolyline.MyCommand))]
 
namespace SelectPartialPolyline
{
    public class MyCommand 
    {
        [CommandMethod("PickPoly")]
        public static void DoPartialPolylineSelection()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                Entity curve = null;
 
                using (var picker = new PartialPolylinePicker())
                {
                    curve = picker.PickPartialPolyline(dwg);
                }
 
                if (curve!=null)
                {
                    ed.WriteMessage("\nPartial polyline picking suceeded.\n");
 
                    // Do whatever with the curve segment from the polyline,
                    // which is not database residing at the moment
                    // if it is not to be added to database, make sure to dispose it
                    curve.Dispose();
                }
                else
                {
                    ed.WriteMessage("\nPartial polyline picking was cancelled\n");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}

Download the source code here.
Also, see this video clip here showing how the code works:



Update:

While this bug fix update is quite late, I did it anyway: now it works with closed polyline as expected. Actually the updated code is much simpler than the buggy old code. I do not remember what I was thinking then😅 The source code for download has also been updated. By the way, I have a later article, that was also about nearly the same topic same topic here.


4 comments:

  1. Hi sir,
    Please update with closed pline!

    ReplyDelete
  2. Hi,
    Thank you, very good code..
    Anyway as someone noticed it doesnt works well if polyline is closed..
    Please can you update it?

    ReplyDelete
  3. Nice! What about using the Highlight method instead of changing color of selected object? And it is possible to maintain the selected highlighted while user continue selecting other segments? (if the user need more than one segment, but not all of them. Thanks.

    ReplyDelete