Tuesday, November 10, 2020

A User-Friendly Command To Copy Part of Polyline

 I recently replied a question asked in Autodesk's AutoCAD .NET API user forum, about how to copy part of Polyline. Naturally, I also wrote some quick test code to make sure my reply does make sense. Afterwards, I though why don't I polish the code a bit more to make it a user-friendly command, hence this post.

The operation requires 3 user inputs: a polyline, two points on the polyline. The output is a copy of the portion of this polyline between the 2 points.

We can easily split a Polyline (or a Curve in general) with Curve.GetSplitCurves(Point3dCollection). In our case, we just need to pass the 2 points as Point3DCollection to Curve.GetSplitCurves() method and pick out the portion of this polyline between the 2 points from the returned DBObjectCollection. However, there are a couple of things to be handled carefully:

1. The points in the argument Point3DCollection of GetSplitCurves() method must be in correct order: the points should be sorted according to its distance from the polyline's start point; When user selects 2 points on polyline, he/she can do the selection along different direction of the Polyline, thus the need of sorting.

2. User could select one of the Polyline's end (StartPoint or EndPoint). User could even choose to select the Polyline's StartPoint and EndPoint (i.e. selecting entire polyline), 

Here is a class PatialPolylineSelector that guide user to select a Polyline, then select 2 points on it. Once 2 points are selected, a non-database-residing Polyline (a clone of the portion of the selected Polyline between the 2 points) is returned to calling process. To make the point selection very user-friendly, when user moves mouse cursor for selecting second point, a ghost polyline is drawn as Transient Graphics to show the output portion of the Polyline.

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace GetPartialPolyline
{
    public class PartialPolylineSelector
    {
        private readonly Document _dwg;
        private readonly Editor _ed;
 
        private Point3d _firstPoint;
        private ObjectId _polylineId = ObjectId.Null;
 
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
        private CadDb.Polyline _ghostPolyline = null;
 
        public PartialPolylineSelector(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
        }
 
        public CadDb.Polyline SelectPartialPolyline()
        {
            var polyId = SelectPolyline(out Point3d pickPt1);
            if (polyId.IsNull)
            {
                _ed.WriteMessage("\n*Cancel*");
                return null;
            }
 
            if (!SelectTwoPointsOnPolyline(
                polyId, pickPt1, 
                out Point3d startPt, out Point3d endPt))
            {
                _ed.WriteMessage("\n*Cancel*");
                return null;
            }
 
            var poly = GeneratePartialPolyline(polyId, startPt, endPt);
            return poly;
        }
 
        #region private methods
 
        private ObjectId SelectPolyline(out Point3d pickPoint)
        {
            pickPoint = Point3d.Origin;
            var opt = new PromptEntityOptions(
                "\nSelect a polyline:");
            opt.SetRejectMessage("\nInvalid: not a polyline!");
            opt.AddAllowedClass(typeof(CadDb.Polyline), true);
 
            var res = _ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                using (var tran = 
                    res.ObjectId.Database.TransactionManager.StartTransaction())
                {
                    var poly = (CadDb.Polyline)tran.GetObject(
                        res.ObjectId, OpenMode.ForRead);
                    //make sure the output point is on the polyline
                    pickPoint = poly.GetClosestPointTo(res.PickedPoint, false);
                    tran.Commit();
                }
 
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private bool SelectTwoPointsOnPolyline(
            ObjectId polyId, Point3d prevPoint,
            out Point3d startPt, out Point3d endPt)
        {
            startPt = Point3d.Origin;
            endPt = Point3d.Origin;
 
            _firstPoint = prevPoint;
            var nextPoint = Point3d.Origin;
            _polylineId = polyId;
 
            bool ok = false;
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
 
                while (true)
                {
                    ClearGhostPolyline();
                    var opt = new PromptPointOptions(
                        "\nSelect second point on the polyline:")
                    {
                        AppendKeywordsToMessage = true,
                        AllowNone = true
                    };
                    opt.Keywords.Add("First point");
                    opt.Keywords.Default = "First point";
 
                    PromptPointResult res;
                    try
                    {
                        // when selecting another point on polyline
                        // show the part of polyline to be cloned
                        // as Transient Graphics
                        _ed.PointMonitor += Editor_PointMonitor;
 
                        res = _ed.GetPoint(opt);
                    }
                    finally
                    {
                        _ed.PointMonitor -= Editor_PointMonitor;
                        ClearGhostPolyline();
                    }
 
                    if (res.Status == PromptStatus.OK)
                    {
                        nextPoint = poly.GetClosestPointTo(res.Value, false);
                        ok = true;
                        break;
                    }
                    else if (res.Status == PromptStatus.Keyword)
                    {
                        // re-select the first point on polyline
                        var cancel = false;
                        while (true)
                        {
                            var op = new PromptPointOptions(
                                "\nSelect first point on polyline:");
                            var rs = _ed.GetPoint(op);
                            if (rs.Status == PromptStatus.OK)
                            {
                                _firstPoint = poly.GetClosestPointTo(rs.Value, false);
                                break;
                            }
                            else
                            {
                                cancel = true;
                                break;
                            }
                        }
 
                        if (cancel)
                        {
                            ok = false;
                            break;
                        }
                    }
                    else
                    {
                        ok = false;
                        break;
                    }
                }
 
                if (ok)
                {
                    SortPickedPoints(
                        poly, _firstPoint, nextPoint, out startPt, out endPt);
                }
 
                tran.Commit();
            }
 
            return ok;
        }
 
        private void SortPickedPoints(
            CadDb.Polyline poly, Point3d picked1, Point3d picked2,
            out Point3d startPt, out Point3d endPt)
        {
            var dist1 = poly.GetDistAtPoint(picked1);
            var dist2 = poly.GetDistAtPoint(picked2);
            if (dist1 < dist2)
            {
                startPt = picked1;
                endPt = picked2;
            }
            else
            {
                startPt = picked2;
                endPt = picked1;
            }
        }
 
        private CadDb.Polyline GeneratePartialPolyline(
            ObjectId polylineId, Point3d startPt, Point3d endPt)
        {
            CadDb.Polyline poly = null;
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var pline = (CadDb.Polyline)tran.GetObject(polylineId, OpenMode.ForRead);
 
                Point3dCollection points = new Point3dCollection();
                if (startPt.IsEqualTo(pline.StartPoint) &&
                    endPt.IsEqualTo(pline.EndPoint))
                {
                    poly = pline.Clone() as CadDb.Polyline;
                }
                else
                {
                    if (startPt.IsEqualTo(pline.StartPoint))
                    {
                        points.Add(endPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 2)
                        {
                            poly = dbObjects[0] as CadDb.Polyline;
                            dbObjects[1].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                    else if (endPt.IsEqualTo(pline.EndPoint))
                    {
                        points.Add(startPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 2)
                        {
                            poly = dbObjects[1] as CadDb.Polyline;
                            dbObjects[0].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                    else
                    {
                        points.Add(startPt);
                        points.Add(endPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 3)
                        {
                            poly = dbObjects[1] as CadDb.Polyline;
                            dbObjects[0].Dispose();
                            dbObjects[2].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                }
 
                tran.Commit();
            }
 
            return poly;
        }
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearGhostPolyline();
 
            var poly = _polylineId.GetObject(OpenMode.ForRead) as CadDb.Polyline;
            var nextPoint = poly.GetClosestPointTo(e.Context.RawPoint, false);
            var dist = nextPoint.DistanceTo(e.Context.RawPoint);
            if (dist<poly.Length/10.0)
            {
                SortPickedPoints(
                    poly, _firstPoint, nextPoint,
                    out Point3d startPt, out Point3d endPt);
                var ghost = GeneratePartialPolyline(_polylineId, startPt, endPt);
                if (ghost!=null)
                {
                    _ghostPolyline = ghost;
                    _ghostPolyline.ColorIndex = 1;
                    _tsManager.AddTransient(
                        _ghostPolyline, 
                        TransientDrawingMode.DirectTopmost, 
                        128, 
                        new IntegerCollection());
                }
 
                e.AppendToolTipText(
                    $"Selected polyline length = {_ghostPolyline.Length}");
            }
        }
 
        private void ClearGhostPolyline()
        {
            if (_ghostPolyline!=null)
            {
                _tsManager.EraseTransient(
                    _ghostPolyline, new IntegerCollection());
                _ghostPolyline.Dispose();
            }
        }
 
        #endregion
    }
}


This the CommandClass to run the process:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(GetPartialPolyline.MyCommands))]
 
namespace GetPartialPolyline
{
    public class MyCommands
    {
        [CommandMethod("PartialPoly")]
        public static void CreatePartialPolyline()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var selector = new PartialPolylineSelector(dwg);
                var poly = selector.SelectPartialPolyline();
                if (poly!=null)
                {
                    poly.ColorIndex = 2;
                    AddPolylineToDb(dwg.Database, poly);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nExecution error:\n{ex.Message}\n");
            }
        }
 
        private static void AddPolylineToDb(Database db, Polyline poly)
        {
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var space = (BlockTableRecord)tran.GetObject(
                    db.CurrentSpaceId, OpenMode.ForWrite);
                space.AppendEntity(poly);
                tran.AddNewlyCreatedDBObject(poly, true);
                tran.Commit();
            }
 
        }
    }
}

This the video clip showing the command execution effect:






3 comments:

khanhdangxd93 said...

Nice post sir!

Kelcyo said...

Norman you can make the source code available or revise it, I am encountering a problem when running in version 2021 ..

varditearnshaw said...

Casino Player: No deposit bonuses | Dr.MCD
Casino Player 광주 출장마사지 is rated 3.1 out of 5 by our members and 46% of them said: "liked it". Casino 경기도 출장샵 Player is also rated 3.6 out of 5 by our members 나주 출장샵 and 46% of them  계룡 출장마사지 Rating: 경산 출장안마 3.2

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.