Sunday, December 29, 2024

Manually Selecting Points for a Polygon

It is a quite common coding task for AutoCAD programming that users are asked to indicate an area/polygon, or its boundary, by selecting a series of points. So, the coding practice is to allow the user to select points in a loop until enough points are selected or the loop is cancelled. During the point picking, it is important to provide the user proper visual feedback to indicate the area/boundary to be formed. Surely, we can use code to mimic "LINE"/"POLYLINE" command to let the user to continuously draw Line/Polyline as needed. However, we often only need to obtain a collection of points that can form an area/boundary, and do not actually want Line/Polyline entities are actually created. Hence the topic of this article: providing visual help while the points being picked without adding entities into the drawing.

At first, I did a version of it by simply handling Editor_PointMonitor event when Editor.GetPoint() is called, so the area/boundary is shown as Transient graphic. The code is rather simple and straightforward.

Then, I though it would make the visual hint area more appealing to the user's eyes if the visual hint area could be filled with background of certain transparency. But using Transient graphics in Editor.PointMonitor event handler would not let me draw such area easily: I may have to dynamically generate a Hatch/MPolygon/Region based on the dynamically drawn polyline. So, I decided to go a different route by using a DrawJig, which provides me a WorldDraw object in the overridden WorldDraw() method, which I can use to fill the background of the area.

After I did the 2 polygon picking classes, I thought why not have the code of the both classes posted here? Then, since both the classes have similar portions of code, so I create a base class and derive the 2 point-picking classes from it.

Here is the base/abreact class:

namespace DriveCadWithCode2025.AutoCADUtilities
{
    public abstract class PolygonPointsPicker
    {
        private readonly Editor _ed;
        private readonly List<Point3d> _points = new List<Point3d>();
 
        public PolygonPointsPicker(Editor edint boundaryColorIndex = 2)
        {
            _ed= ed;
            BoundaryColorIndex = boundaryColorIndex;
        }
 
        public List<Point3d> BoundaryPoints => _points;
        protected Editor ThisEditor => _ed;
        protected Polyline? Boundary { getset; } = null;
 
        protected int BoundaryColorIndex { get; }
 
        public virtual bool PickPolygon()
        {
            if (!PickFirstTwoPoints())
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return false;
            }
            else
            {
                return true;
            }
        }
 
        protected Polyline CreateGhostPolyline()
        {
            var poly = new Polyline();
            for (int i = 0; i < BoundaryPoints.Count; i++)
            {
                poly.AddVertexAt(inew Point2d(
                    BoundaryPoints[i].X, BoundaryPoints[i].Y), 0.0, 0.0, 0.0);
            }
 
            poly.AddVertexAt(BoundaryPoints.Count, new Point2d(
                BoundaryPoints[BoundaryPoints.Count - 1].X, 
                BoundaryPoints[BoundaryPoints.Count - 1].Y), 0.0, 0.0, 0.0);
 
            poly.ColorIndex = BoundaryColorIndex;
 
            if (poly.NumberOfVertices > 2)
            {
                poly.Closed = true;
            }
 
            return poly;
        }
 
        private bool PickFirstTwoPoints()
        {
            var res = _ed.GetPoint("Select polygon's first point:");
            if (res.Status != PromptStatus.OK) return false;
 
            _points.Add(res.Value);
 
            var opt = new PromptPointOptions("\nSelect polygon's next point:");
            opt.UseBasePoint = true;
            opt.BasePoint = res.Value;
            opt.UseDashedLine = true;
            res = _ed.GetPoint(opt);
            if (res.Status != PromptStatus.OK) return false;
 
            _points.Add(res.Value);
            return true;
        }
    }
}

Here is the first point-picking class, which uses Transient to draw a closed polyline by handling Editor.PointMonitor event:

using Autodesk.AutoCAD.GraphicsInterface;
 
namespace DriveCadWithCode2025.AutoCADUtilities
{
    public class PolygonBoundaryPicker : PolygonPointsPicker
    {
        private readonly TransientManager _tsMng = 
            TransientManager.CurrentTransientManager;
 
        public PolygonBoundaryPicker(Editor edint boundaryColorIndex = 2) : 
            base(edboundaryColorIndex)
        { 
 
        }
 
        public override bool PickPolygon()
        {
            if(!base.PickPolygon()) return false;
 
            var ok = false;
            try
            {
                ThisEditor.PointMonitor += Editor_PointMonitor;
                while(true)
                {
                    Boundary = CreateGhostPolyline();
                    _tsMng.AddTransient(
                        Boundary, 
                        TransientDrawingMode.DirectTopmost, 
                        128, 
                        new IntegerCollection());
 
                    var opt = new PromptPointOptions("\nSelect polygon's next point:");
                    if (BoundaryPoints.Count>=3)
                    {
                        opt.AllowNone = true;
                        opt.Keywords.Add("Done");
                        opt.Keywords.Default = "Done";
                    }
                    var pRes = ThisEditor.GetPoint(opt);
                    if (pRes.Status== PromptStatus.OK)
                    {
                        BoundaryPoints.Add(pRes.Value);
                        ClearGhostPolyline();
                    }
                    else if (pRes.Status == PromptStatus.Keyword)
                    {
                        ok = true;
                        break;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            finally
            {
                ThisEditor.PointMonitor -= Editor_PointMonitor;
                ClearGhostPolyline();
            }
 
            return ok;
        }
 
        private void Editor_PointMonitor(object senderPointMonitorEventArgs e)
        {
            if (Boundary == null || Boundary.NumberOfVertices < 3) return;
 
            var pt=new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);
            Boundary.SetPointAt(Boundary.NumberOfVertices-1, pt);
            _tsMng.UpdateTransient(Boundary, new IntegerCollection());
        }
 
        #region private methods
 
        private void ClearGhostPolyline()
        {
            if (Boundary != null)
            {
                _tsMng.EraseTransient(Boundary, new IntegerCollection());
                Boundary.Dispose();
                Boundary = null;
            }
        }
 
        #endregion
    }
}

The second point-picking class, as aforementioned, contains a private custom DrawJig class used for drawing the area with background fill. Here is its code:

using Autodesk.AutoCAD.GraphicsInterface;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace DriveCadWithCode2025.AutoCADUtilities
{
    public class PolygonAreaPicker : PolygonPointsPicker
    {
        private class PolygonPointJig : DrawJig
        {
            private CadDb.Polyline _polyline;
            private Point3d _prevPoint;
            private Point3d _currPoint;
            private readonly int _transparencyPercentile;
            public PolygonPointJig(CadDb.Polyline polylineint backgroundTransparency = 50)
            {
                _polyline = polyline;
                _transparencyPercentile = backgroundTransparency;
                var lastPt = _polyline.GetPoint3dAt(_polyline.NumberOfVertices - 1);
                _prevPoint = lastPt;
                _currPoint = lastPt;
            }
 
            public Point3d CurrentPoint => _currPoint;
 
            protected override SamplerStatus Sampler(JigPrompts prompts)
            {
                var opt = new JigPromptPointOptions("\nSelect polygon's next point:");
                if (_polyline.NumberOfVertices > 3)
                {
                    opt.UserInputControls = UserInputControls.NullResponseAccepted;
                    opt.Keywords.Add("Done");
                    opt.Keywords.Default = "Done";
                }
 
                var res = prompts.AcquirePoint(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.Value.IsEqualTo(_prevPoint))
                    {
                        return SamplerStatus.NoChange;
                    }
                    else
                    {
                        _prevPoint = _currPoint;
                        _currPoint = res.Value;
                        return SamplerStatus.OK;
                    }
                }
                else if (res.Status == PromptStatus.Keyword)
                {
                    return SamplerStatus.OK;
                }
                else
                {
                    return SamplerStatus.Cancel;
                }
            }
 
            protected override bool WorldDraw(WorldDraw draw)
            {
                draw.Geometry.Draw(_polyline);
 
                var pt = new Point2d(_currPoint.X, _currPoint.Y);
                _polyline.SetPointAt(_polyline.NumberOfVertices - 1, pt);
 
                Point3dCollection pts = new Point3dCollection();
                for (int i = 0; i < _polyline.NumberOfVertices; i++)
                {
                    pts.Add(_polyline.GetPoint3dAt(i));
                }
 
                draw.SubEntityTraits.FillType = FillType.FillAlways;
                draw.SubEntityTraits.Transparency =
                    AcadGenericUtilities.GetTransparencyByPercentage(_transparencyPercentile);
                draw.SubEntityTraits.Color = 2;
                draw.Geometry.Polygon(pts);
 
                return true;
            }
        }
 
        private int _transparencyPercentile = 50;
 
        public PolygonAreaPicker(
            Editor ed, 
            int boundaryColorIndex = 2,  
            int backgroundTransparency=50) : base(edboundaryColorIndex)
        {
            _transparencyPercentile = backgroundTransparency;
        }
 
        public override bool PickPolygon()
        {
            if (!base.PickPolygon()) return false;
 
            var ok = false;
 
            try
            {
                while(true)
                {
                    Boundary = CreateGhostPolyline();
                    var jig = new PolygonPointJig(Boundary);
                    var jigRes = ThisEditor.Drag(jig);
                    if (jigRes.Status== PromptStatus.OK)
                    {
                        BoundaryPoints.Add(jig.CurrentPoint);
                        Boundary.Dispose();
                        Boundary = null;
                        jig = null;
                    }
                    else if (jigRes.Status == PromptStatus.Keyword)
                    {
                        ok = true;
                        break;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            finally
            {
                if (Boundary != null)
                {
                    Boundary.Dispose();
                }
            }
 
            return ok;
        }
    }
}

In the provate jig class's WorldDraw() method, I used a static method from a referenced AutoCAD utility class for getting Transparency value in the range of 0 to 100, where 0 means no transparency and 100 for total transparency:

public static Transparency GetTransparencyByPercentage(int transparencyPercent)
{
    if (transparencyPercent > 90) transparencyPercent = 90;
    var alpha = Convert.ToInt32(Math.Floor(255 * ((100 - transparencyPercent) / 100.0)));
 
    return new Transparency((byte)alpha);
}

Here is the CommandMethod that runs the 2 point-picking classes:

[CommandMethod("PickPolygon")]
public static void TestPolygonPicker()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptKeywordOptions("Use Boundary Picker or Area Picker:");
    opt.AppendKeywordsToMessage = true;
    opt.Keywords.Add("Boundary");
    opt.Keywords.Add("Area");
    var res = ed.GetKeywords(opt);
    if (res.Status != PromptStatus.OK) return;
 
    try
    {
        PolygonPointsPicker picker;
 
        if (res.StringResult == "Boundary")
        {
            picker = new PolygonBoundaryPicker(ed, 1);
        }
        else
        {
            picker = new PolygonAreaPicker(ed, 1, 25);
        }
 
        if (picker.PickPolygon())
        {
            var points = picker.BoundaryPoints;
            ed.WriteMessage($"\nPicked points: {points.Count}");
        }
    }
    catch(System.Exception ex)
    {
        CadApp.ShowAlertDialog($"Error:\n{ex.Message}");
    }
}

The videos below show the code in action:



No comments:

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.