Monday, January 13, 2025

Determine Object Source When Using COPYCLIP[BASE]/PASTECLIP[ORIG] (Ctrl+C/Ctrl+V)

When using AutoCAD, users can use a standard Windows OS operation to copy/paste entities in the same drawing or across different drawings. The copy/paste operation can be done by simply pressing Ctrl+C/Ctrl+V (as users do with almost all Windows applications); or by entering command "COPYCLIP/COPYBASE" and then "PASTECLIP/PASTEORIG" at AutoCAD's command line; or by right-clicking in AutoCAD's editor and in the content menu, select "Clipboard".

There is an interesting question posted in AutoCAD .NET API forum, asking how to tell the objects being COPY/PASTE-ed are originated from the same drawing, or from different drawing (of course the different drawing has to be open in an AutoCAD session (but source and destination drawings are not necessarily be in the same AutoCAD session).

In order to answer this question, we need to understand how the COPY/PASTE commands work: 

1. When COPYCLIP[BASE] command executes, AutoCAD Wblocks the selected entities and saves it as a DWG file with a name like "A$Cxxxxxxxx.Dwg". The saving location is the location defined in "Temporary Drawing File Lcoation" of the current user profile. Then the temporary file name is somehow stored in Windows Clipboard (this is my guess!). Each time the Ctrl+C pressed, previous temporary file is erased and a new one is created and the Clipboard is updated.

2. When PASTECLIP[ORIG] command executes, AutoCAD locates the temporary file according to the file information in the Clipboard and import its content into the destination drawing (insert as a block, or WblockCloneObject()?). Also, even the source drawing is closed after Ctrl+C is pressed, the "copied" objects can still be pasted to drawings in running AutoCAD sessions. In fact, even AutoCAD session is closed, and opened again, the "copied" objects can still be pasted, as long as the Windows Clipboard has not been updated by pressing Ctrl+C in AutoCAD session, or other Windows applications.

Since the source data is saved as temporary drawing file, thus the source objects can be "pasted" to the same drawing, other drawing (in the same or different AutoCAD session).

Now back to the question of how to know the source objects are originated from which drawing. Now that we know Ctrl+C merely Wblock selected entities to a external drawing file, we can manage to know capture the source document/database information when the Wblocking occurs and make it available when Ctrl+V is pressed. 

Following code shows my attempt of telling where the source entities are originated before them being pasted:

using Autodesk.AutoCAD.DatabaseServices.Filters;
using DriveCadWithCode2025.AutoCADUtilities;
 
[assemblyCommandClass(typeof(AcadMicsTests.MyCommands))]
 
namespace AcadMicsTests
{
    public class MyCommands 
    {
        #region Determine COPY/PASTE source file
 
        private static IntPtr _copySourceDoc = IntPtr.Zero;
        private static bool _isCopyClip = false;
 
        [CommandMethod("HandlePaste")]
        public static void GetDataInClipboard()
        {
            foreach (Document dwg in CadApp.DocumentManager)
            {
                var db = dwg.Database;
 
                db.WblockNotice += Db_WblockNotice;
                dwg.CommandWillStart += Dwg_CommandWillStart;
                dwg.CommandEnded += Dwg_CommandEnded;
            }
 
            CadApp.DocumentManager.DocumentCreated += (oe) =>
            {
                var dwg = e.Document;
                var db = dwg.Database;
 
                db.WblockNotice += Db_WblockNotice;
                dwg.CommandWillStart += Dwg_CommandWillStart;
                dwg.CommandEnded += Dwg_CommandEnded;
            };
        }
 
        private static void Db_WblockNotice(object senderWblockNoticeEventArgs e)
        {
            if (_isCopyClip)
            {
                _copySourceDoc = CadApp.DocumentManager.MdiActiveDocument.UnmanagedObject;
            }
        }
 
        private static void Dwg_CommandEnded(object senderCommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper().Contains("COPYCLIP") ||
                e.GlobalCommandName.ToUpper().Contains("COPYBASE"))
            {
                _isCopyClip = false;
            }
        }
 
        private static void Dwg_CommandWillStart(object senderCommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper().Contains("COPYCLIP") ||
                e.GlobalCommandName.ToUpper().Contains("COPYBASE"))
            {
                _isCopyClip = true;
            }
            else if (e.GlobalCommandName.ToUpper().Contains("PASTECLIP") ||
                e.GlobalCommandName.ToUpper().Contains("PASTEORIG"))
            {
                var sourceFile = "";
                foreach (Document dwg in CadApp.DocumentManager)
                {
                    if (dwg.UnmanagedObject == _copySourceDoc)
                    {
                        sourceFile = dwg.Name;
                        break;
                    }
                }
 
                CadApp.DocumentManager.MdiActiveDocument.Editor.WriteMessage(
                    $"\nYou are to paste objects copied from:\n{sourceFile}...\n");
            }
        }
 
        #endregion
    }
}

One thing to pay attention here. As explained, Ctrl+C exports the selected entities as a temporary drawing file linked to Windows Clipboard, which can be pasted to an open drawing in any running AutoCAD session. So, if the copied source is pasted to a different AutoCAD session, my code will not be able to tell where the source is from. Here my focus is to explain how Ctrl+C/Ctrl+V works in AutoCAD under the hood, and based on the understanding, I show that it is not too difficult to tell which file is the source of the copying, assuming the COPY/PASTE is operated in the same AutoCAD session.

I am no sure what the practical use of knowing which drawing file name is the copy source. Considering a drawing file can be renamed as many different file names while their contents remain the same, if knowing where the copy source is from is very critical, relying on file name might not be a good choice. It might be better to intercept into the COPYCLIP/BASE command (when command started and/or when selection is made) and add flags to the selected entities (XData, for example) before the COPYCLIP/BASE command does the WBlock to generate the temporary drawing. Then after pasting the source into a drawing, we can use code to check up the flag... Not sure if it is doable, just a thought. But one just want to casually know if a copy source is from the same drawing or from different open drawing in the AutoCAD session, the code here just does that. With minor modification by handling DocumentLockModeChanged event, the pasting can be vetoed if the copy source is not from the desired drawing (the same drawing or the different drawing).

See video clip below and pay attention to the command line when copied source is pasted:







Monday, December 30, 2024

Clipping Blocks

Can we clip a block (BlockReference) by using API code, as we use "CLIP" command? A recent post in AutoCAD .NET discussion forum asked this question. For whatever reasons, the post stayed quite a while without response.

There are 2 built-in commands in AutoCAD - "XClip" and "Clip", they basically does the same thing under the hood with slight differences of command-line options. In this post, I'll focus on clipping single block (BlockReference).

While this would be my first time coding "Clipping", there was a couple of articles from the most famous AutoCAD .NET API blogger Kean Walmsley looong time ago (more than 12 years ago!):

Adding a 2D spatial filter to perform a simple xclip on an external reference in AutoCAD using .NET

Querying for XCLIP information inside AutoCAD using .NET

So, my code here is mostly simple copy/paste of Kean's, with some necessary updates/changes. The code includes 2 CommandMethods: "ClipBlk" and "RemoveClip":

using Autodesk.AutoCAD.DatabaseServices.Filters;
using DriveCadWithCode2025.AutoCADUtilities;
 
[assemblyCommandClass(typeof(AcadMicsTests.MyCommands))]
 
namespace AcadMicsTests
{
    public class MyCommands 
    {
        private const string FILTER_DICT_NAME = "ACAD_FILTER";
        private const string SPATIAL_DICT_NAME = "SPATIAL";
 
        [CommandMethod("ClipBlk")]
        public static void ClipBlockReference()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect block reference to clip:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
 
            var opt = new PromptKeywordOptions("\nChoose clip boundary:");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Rectangle");
            opt.Keywords.Add("Polygon");
            opt.Keywords.Default = "Rectangle";
            var kres=ed.GetKeywords(opt);
            if (kres.Status != PromptStatus.OK) return;
 
            List<Point3d>? points = null;
            if (kres.StringResult == "Rectangle")
            {
                if (SelectClipWindow(edout Point3d pt1out Point3d pt2))
                {
                    points=new List<Point3d> { pt1pt2 };
                }
            }
            else
            {
                if (SelectClipPolygon(edout List<Point3d>? pts))
                {
                    if (pts != null)
                    {
                        points = pts;
                    }
                }
            }
 
            if (points == nullreturn;
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                if (blk.ExtensionDictionary.IsNull)
                {
                    blk.UpgradeOpen();
                    blk.CreateExtensionDictionary();
                }
 
                var extDict = (DBDictionary)tran.GetObject(
                    blk.ExtensionDictionary, OpenMode.ForWrite);
                DBDictionary filterDict;
                if (!extDict.Contains(FILTER_DICT_NAME))
                {
                    filterDict = new DBDictionary();
                    extDict.SetAt(FILTER_DICT_NAME, filterDict);
                    tran.AddNewlyCreatedDBObject(filterDicttrue);
                }
                else
                {
                    filterDict=(DBDictionary)tran.GetObject(
                        extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                }
 
                if (filterDict.Contains(SPATIAL_DICT_NAME))
                {
                    var id = filterDict.GetAt(SPATIAL_DICT_NAME);
                    filterDict.Remove(SPATIAL_DICT_NAME);
                    var spFilter = tran.GetObject(idOpenMode.ForWrite);
                    spFilter.Erase();
                }
 
                Point2dCollection clipPoints = GetPolygonBoundary(pointsblk.BlockTransform);
 
                var definition = new SpatialFilterDefinition(
                    clipPointsVector3d.ZAxis, 0.0,
                    double.PositiveInfinity, double.NegativeInfinity, true);
                var filter = new SpatialFilter();
                filter.Definition = definition;
                filterDict.SetAt(SPATIAL_DICT_NAME, filter);
                tran.AddNewlyCreatedDBObject(filtertrue);
 
                tran.Commit();
            }
 
            ed.Regen();
            ed.UpdateScreen();
        }
 
        [CommandMethod("RemoveClip")]
        public static void RemoveClip()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect a clipped block reference:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
        }
 
        private static bool SelectClipWindow(Editor edout Point3d pt1out Point3d pt2)
        {
            pt1 = Point3d.Origin;
            pt2 = Point3d.Origin;
 
            var res1 = ed.GetPoint("\nSelect lower-left corner:");
            if (res1.Status == PromptStatus.OK)
            {
                var res2 = ed.GetCorner("\nSelect upper-right corner:"res1.Value);
                if (res2.Status == PromptStatus.OK)
                {
                    pt1 = res1.Value;
                    pt2 = res2.Value;
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool SelectClipPolygon(Editor edout List<Point3d>? points)
        {
            var picker = new PolygonAreaPicker(ed, 1, 25);
            if (picker.PickPolygon())
            {
                points = picker.BoundaryPoints;
                return true;
            }
            else
            {
                points = null;
                return false;
            }
        }
 
        private static bool RemoveClipFromBlockReference(ObjectId blkId)
        {
            var removed = false;
            var db = blkId.Database;
            using (var tran=db.TransactionManager.StartTransaction())
            {
                var blk = tran.GetObject(blkIdOpenMode.ForRead);
                if (!blk.ExtensionDictionary.IsNull)
                {
                    var extDict = (DBDictionary)tran.GetObject(
                        blk.ExtensionDictionary, OpenMode.ForRead);
                    if (extDict.Contains(FILTER_DICT_NAME))
                    {
                        var filterDict = (DBDictionary)tran.GetObject(
                            extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                        if (filterDict.Contains(SPATIAL_DICT_NAME))
                        {
                            var filter = tran.GetObject(
                                filterDict.GetAt(SPATIAL_DICT_NAME), OpenMode.ForWrite);
                            filter.Erase();
                            filterDict.Remove(SPATIAL_DICT_NAME);
                        }
 
                        extDict.UpgradeOpen();
                        extDict.Remove(FILTER_DICT_NAME);
 
                        removed = true;
                    }
                }
 
                tran.Commit();
            }
            return removed;
        }
 
        private static Point2dCollection GetPolygonBoundary(
            IEnumerable<Point3dpointsMatrix3d blkTransform)
        {
            var pts = points
                .Select(pt => pt.TransformBy(blkTransform.Inverse()))
                .Select(pt => new Point2d(pt.X, pt.Y));
            Point2dCollection pt2ds = new Point2dCollection(pts.ToArray());
            return pt2ds;
        }
    }
}

The the private method SelectClipPolygon(), I used a class PolygonAreaPicker for selecting a polygon area. I posted the code of this class in my previously posted article here.

See the video below showing how the code works:



Here are somethings to point out:

1. While the clip boundary seems being created correctly, the boundary created by the code has a noticeable difference from the one created by "CLIP" command: the clip boundary generated by "CLIP" command has a grip, similar to the ones used for control dynamic properties of a dynamic BlockReference. By clicking this grip, the clipping can be flipped to show either external clipping, or internal clipping. However, the clip boundary created by my code does not have this "clipping-flip" grip. Obviously, command "CLIP" does more than just clipping the target BlockReference. Fortunately, the SpatialFilter has a read-write property "Inverted" that we can set by code. Maybe, it is possible to define a grip overrule to mimic the clip boundary created by "CLIP" command. I am not going to dig into it for now.

2. When a clip is applied to a BlockReference, the SpatialFilter that creates the clipping effect is persisted as ExtensionDictionary of the Blockreference. That is, we can examine a BlockReference's ExtensionDictionary to see if it contains a "ACAD_FILTER" dictionary and a DBDictionaryEntry keyed as "SPATIAL". So, theoretically, we can open the existing SpatialFilter object in a Transaction with the SpatialFilter's ObjectId stored in the DBDictionaryEntry. Then I thought, if I want to change the boundary of an existing clipped BlockReference, I could simply open the SpatialFilter in transaction and define a new SpatialFilterDefinition and set it to the SpatialFilter.Definition property, which is set/write-able. Well, as it turns out: yes, I can open the SpatialFilter object for read/write with the ObjectId from the ExtensionDictionary, but it seems the opened SpatialFilter object is read-only, that is, we can look up its "Definition" property to get the boundary's points; as soon as we try to assign the property a newly defined SpatialFilterDefinition, AutoCAD crashes. Therefore, if I want to updated an existing BlockReference's clipping boundary, I need to first delete the attached "ACAD_FILTER" ExtensionDictionary, and then create a new one with a newly defined SpatialFilterDefinition.

3. As described in 2, when updating an existing BlockReference clipping boundary, we need to remove existing one and then create a new one. I found out the 2 steps have to be done in separate Transactions. Or, the clipped BlockReference may not displayed properly.

All in all, the code of the 2 commands showed here basically demonstrates how to use spatial filter API to clip a BlockReference.











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:



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.