Friday, August 23, 2019

Selecting Multiple Nested Entities - 2 of 2

This the second post on the topic of selecting multiple nested entities (from a block reference). The first one is here, in which I demonstrated how to select multiple nested entities by one mouse click at a time. In this article, I show how to do a window-selecting, following AutoCAD's window-selecting convention: if selecting window is picked from left to right, one entities entirely enclosed inside the window would be selected, while window is picked from right to left, not only entities being enclosed inside the window, but also entities being crossed by the window would all be selected.

When I worked on code for this article, to create a new class MultiNestedEntSelector, I saw there is a lots of common code that could be shared with the class NestedEntSelector in previous article, so I decide to create a base class and derive these 2 classes on top of it. At the end of this article, I also re-post the updated NestedEntSelector class used in previous article.

First, the base class, which is an abstract class, NestedEntSeelctorBase:

using System;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace SelectMultiNestedEnts
{
    public abstract class NestedEntSelectorBase : IDisposable
    {
        protected Editor Editor { set; get; }
        protected bool Highlight { set; get; }
        protected int HighlightColorIndex { set; get; }
        protected TransientManager TransientManager
        {
            get
            {
                return TransientManager.CurrentTransientManager;
            }
        }
 
        public DBObjectCollection SelectedClones { set; get; }
 
        public abstract void SelectNestedEntities(
            bool highlight = true, int highlightColorIndex = 1);
 
        public void CleanUpClonedEntities()
        {
            if (Highlight)
            {
                foreach (DBObject obj in SelectedClones)
                {
                    this.TransientManager.EraseTransient(
                        obj as Drawable, new IntegerCollection());
                }
            }
 
            foreach (DBObject obj in SelectedClones)
            {
                // dispose the cloned entities
                // if it is not added into database
                if (obj.ObjectId.IsNull) obj.Dispose();
            }
 
            SelectedClones.Clear();
            SelectedClones.Dispose();
            SelectedClones = null;
        }
 
        public void Dispose()
        {
            CleanUpClonedEntities();
        }
    }
}

Now, something on the class for selecting multiple nested entities in a block reference MultiNestedEntSelector:

1. To make things simple, the code makes sure only 1 BlockReference is selected by the selecting window.

2. The code need to determine if an entity is inside of a selecting window, or is crossed by the selecting window. This is one of very common AutoCAD programming tasks. I believe many of us programmers have done it many times and likely have our own favorite, re-usable algorithm/code ready available. So I decide to design the class' contructor to take a Func to allow the existing/favorite code of doing "is-inside"/"is-crossing" to be injected. Of course for the completion of this article, I have my simplified "is-inside"/"is-crossing" code in a separate class Helper.

3. In order to see if entity is crossed by the selecting window, I create a closed Polyline (a rectangle), and use Polyline.InteractionWith().

4. I always test if "Is-crossing" first. If an entity is not crossed by the selecting window, it would either entirely outside the window, or entirely inside. Being entirely inside the window means any point on the entity must be inside the window. So, the "is-inside" testing, after "is-crossing" test, becomes get a point from the entity, and test if the point is inside a closed polyline. I used MPolygon for testing if a point is inside. As for getting a point from an entity, it would depend what the entity is. For any curve type, I use "StartPoint"; for DBText, I use AlignmentPoint; for MText, I use Location, which is upper left corner of MText.. For the sake of simplicity, I omitted other types of entity.

Enough explanations. Here is the class MultiNestedEntSelector:

using System;
using System.Collections.Generic;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectMultiNestedEnts
{
    public enum WindowSelectionState
    {
        Crossing = 0,
        Inside = 1,
        Outside = 2
    }
 
    public class MultiNestedEntSelector : NestedEntSelectorBase
    {
        private bool _crossingSelection = false;
        private Point3dCollection _window = null;
        private ObjectId _blockId = ObjectId.Null;
        private Func<Entity, Point2dCollection, WindowSelectionState> _winSelectionStateFunction = null;
 
        public MultiNestedEntSelector(
            Func<Entity, Point2dCollection, WindowSelectionState> winSelectionStateFunction)
        {
            _winSelectionStateFunction = winSelectionStateFunction;
        }
 
        public override void SelectNestedEntities(
            bool highlight = true, int highlightColorIndex = 1)
        {
            Editor = CadApp.DocumentManager.MdiActiveDocument.Editor;
            SelectedClones = null;
 
            if (!GetSelectionWindowAndBlockReference())
            {
                Editor.WriteMessage("\nCancel*");
                return;
            }
 
            Highlight = highlight;
            HighlightColorIndex = highlightColorIndex;
 
            var clones = FindEntitiesByWindow();
            if (clones.Count > 0)
            {
                SelectedClones = new DBObjectCollection();
                foreach (var clone in clones)
                {
                    SelectedClones.Add(clone);
                }
            }
            clones.Clear();
 
            if (SelectedClones !=null && SelectedClones.Count>0)
            {
                if (Highlight)
                {
                    HighlightSelelcted();
                }
            }
        }
 
        #region private methods
 
        private void HighlightSelelcted()
        {
            foreach (Entity ent in SelectedClones)
            {
                this.TransientManager.AddTransient(
                    ent, TransientDrawingMode.Highlight, 128, new IntegerCollection());
            }
        }
 
        private bool GetSelectionWindowAndBlockReference()
        {
            bool done = false;
 
            // Make sure the selecting window only covers 1 block reference
            ObjectId blkId = ObjectId.Null;
            Point3dCollection window = null;
            bool isCrossing = false;
 
            while (blkId.IsNull)
            {
                if (!PickSelectionWindow(out window, out isCrossing))
                {
                    Editor.WriteMessage("\nCancel*");
                    break;
                }
 
                _window = window;
                _crossingSelection = isCrossing;
 
                blkId = IsBlockReferenceSelected(_window);
                if (blkId.IsNull)
                {
                    var kOpt = new PromptKeywordOptions(
                        "\nInvalid selecting window: no block or too many blocks covered.");
                    kOpt.AppendKeywordsToMessage = true;
                    kOpt.Keywords.Add("Window");
                    kOpt.Keywords.Add("Cancel");
                    kOpt.Keywords.Default = "Window";
 
                    var res = Editor.GetKeywords(kOpt);
                    if (res.Status== PromptStatus.OK)
                    {
                        if (res.StringResult != "Window")
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                else
                {
                    done = true;
                    break;
                }
            }
 
            if (done)
            {
                _blockId = blkId;
            }
 
            return done;
        }
 
        private bool PickSelectionWindow(
            out Point3dCollection windowPoints, out bool crossingSelection)
        {
            bool picked = false;
 
            windowPoints = null;
            crossingSelection = false;
 
            var pRes = Editor.GetPoint("\nSelect first corner of selecting window:");
            if (pRes.Status== PromptStatus.OK)
            {
                var cOpt = new PromptCornerOptions(
                    "\nSelect a corner of picking window:", pRes.Value);
                cOpt.UseDashedLine = true;
                var cRes = Editor.GetCorner(cOpt);
                if (cRes.Status== PromptStatus.OK)
                {
                    SetSelectionWindow(
                        pRes.Value, cRes.Value, out windowPoints, out crossingSelection);
                    picked = true;
                }
            }
 
            return picked;
        }
 
        private void SetSelectionWindow(
            Point3d firstPt, Point3d secondPt, out Point3dCollection window, out bool isCrossing)
        {
            var pts = new Point3d[]
            {
                new Point3d(firstPt.X,firstPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(firstPt.X,secondPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(secondPt.X,secondPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(secondPt.X,firstPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse())
            };
 
            window = new Point3dCollection(pts);
            isCrossing = firstPt.X > secondPt.X;
        }
 
        private ObjectId IsBlockReferenceSelected(Point3dCollection selectWin)
        {
            var blkId = ObjectId.Null;
 
            var filter = new SelectionFilter(new TypedValue[] { new TypedValue((int)DxfCode.Start, "INSERT") });
            var res = Editor.SelectCrossingWindow(_window[0], _window[2], filter);
 
            if (res.Status== PromptStatus.OK)
            {
                if (res.Value.Count == 1)
                {
                    blkId = res.Value[0].ObjectId;
                }
            }
 
            return blkId;
        }
 
        private List<Entity> FindEntitiesByWindow()
        {
            var ents = new List<Entity>();
 
            using (var tran = _blockId.Database.TransactionManager.StartTransaction())
            {
                var bref = (BlockReference)tran.GetObject(_blockId, OpenMode.ForRead);
                var bdef = (BlockTableRecord)tran.GetObject(bref.BlockTableRecord, OpenMode.ForRead);
 
                // Clone all entities in the block definition, except
                // AttributeDefinition that is not constant
                foreach (ObjectId entId in bdef)
                {
                    var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
 
                    bool skip = false;
                    if (ent is AttributeDefinition)
                    {
                        var att = ent as AttributeDefinition;
                        if (!att.Constant) skip = true;
                    }
                    if (skip) continue;
 
                    var clone = ((Entity)ent.Clone());
                    clone.TransformBy(bref.BlockTransform);
 
                    if (IsSelectedByWindow(clone))
                    {
                        clone.ColorIndex = HighlightColorIndex;
                        ents.Add(clone);
                    }
                    else
                    {
                        clone.Dispose();
                    }
                }
 
                // Clone all AttributeReference iin the block reference
                foreach (ObjectId id in bref.AttributeCollection)
                {
                    var att = (AttributeReference)tran.GetObject(id, OpenMode.ForRead);
                    if (!att.Invisible && att.Visible)
                    {
                        if (IsSelectedByWindow(att))
                        {
                            var clone = ((Entity)att.Clone());
                            clone.ColorIndex = HighlightColorIndex;
 
                            ents.Add(clone);
                        }
                    }
                }
 
                tran.Commit();
            }
 
            return ents;
        }
 
        private bool IsSelectedByWindow(Entity ent)
        {
            var pts = new Point2dCollection();
            foreach (Point3d p in _window)
            {
                pts.Add(new Point2d(p.X, p.Y));
            }
 
            var selectionState = _winSelectionStateFunction(ent, pts);
            if (_crossingSelection)
            {
                return selectionState != WindowSelectionState.Outside;
            }
            else if (!_crossingSelection)
            {
                return selectionState == WindowSelectionState.Inside;
            }
 
            return false;
        }
 
        #endregion
    }
}

Here is the my "Is-inside"/"is-crossing" code in class Helper that is injected into the MultiNestedEntSelector class:

using System;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace SelectMultiNestedEnts
{
    public class Helper
    {
        public static WindowSelectionState GetWindowSelectionState(
            Entity entity, Point2dCollection window)
        {
            var state = WindowSelectionState.Outside;
 
            using (var poly = CreatePolyline(window))
            {
                var interPoints = new Point3dCollection();
 
                poly.IntersectWith(
                    entity, Intersect.ExtendThis, interPoints, IntPtr.Zero, IntPtr.Zero);
                if (interPoints.Count>0)
                {
                    state = WindowSelectionState.Crossing;
                }
                else
                {
                    if (IsInsideWindow(poly, entity))
                    {
                        state = WindowSelectionState.Inside;
                    }
                }
            }
 
            return state;
        }
 
 
        #region private methods
 
        private static CadDb.Polyline CreatePolyline(Point2dCollection points)
        {
            var poly = new Autodesk.AutoCAD.DatabaseServices.Polyline(points.Count);
            for (int i=0; i < points.Count; i++)
            {
                poly.AddVertexAt(i, new Point2d(points[i].X, points[i].Y), 0.0, 0.0, 0.0);
            }
 
            poly.Closed = true;
 
            return poly;
        }
 
        private static bool IsInsideWindow(CadDb.Polyline poly, Entity entity)
        {
           if (GetPointFromEntity(entity, out Point2d point))
            {
                return IsPointInside(poly, point);
            }
 
            return false;
        }
 
        private static bool GetPointFromEntity(Entity entity, out Point2d point)
        {
            point = Point2d.Origin;
 
            if (entity is CadDb.Curve)
            {
                var pt = ((Curve)entity).StartPoint;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is DBText)
            {
                var pt = ((DBText)entity).AlignmentPoint;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is MText)
            {
                var pt = ((MText)entity).Location;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is DBPoint)
            {
                var pt = ((DBPoint)entity).Position;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
 
            // for the simplicity, I ignore other possible
            // entity types, such as BlockReference, Hatch...
 
            return false;
        }
 
        private static bool IsPointInside(CadDb.Polyline poly, Point2d point)
        {
            var pts = new Point2dCollection();
            for (int i=0; i < poly.NumberOfVertices; i++)
            {
                pts.Add(poly.GetPoint2dAt(i));
            }
 
            var inside = IsInside(point, pts);
            return inside;
        }
 
        private static bool IsInside(
            Point2d point, Point2dCollection polygonPoints, double tolerance = 0.001)
        {
            bool inside = false;
 
            if (polygonPoints.Count > 2)
            {
                var poly = new CadDb.Polyline(polygonPoints.Count);
                for (int i = 0; i < polygonPoints.Count; i++)
                {
                    poly.AddVertexAt(i, polygonPoints[i], 0.0, 0.0, 0.0);
                }
                poly.Closed = true;
 
                using (poly)
                {
                    using (var polygon = new MPolygon())
                    {
                        polygon.AppendLoopFromBoundary(poly, false, tolerance);
 
                        inside = polygon.IsPointInsideMPolygon(
                            new Point3d(point.X, point.Y, 0.0), tolerance).Count == 1;
                    }
                }
            }
 
            return inside;
        }
 
        #endregion
    }
}


Here is the updated class NestedEntSelector used in previous article, which is now derived from NestedEntSelectorBase:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectMultiNestedEnts
{
    public class NestedEntSelector : NestedEntSelectorBase
    {
        public override void SelectNestedEntities(
            bool highlight=true, int highlightColorIndex=1)
        {
            Editor = CadApp.DocumentManager.MdiActiveDocument.Editor;
            SelectedClones = new DBObjectCollection();
 
            int count = 0;
            Highlight = highlight;
            HighlightColorIndex = highlightColorIndex;
 
            while (true)
            {
                var msg = $"Select a nested entity in a block ({count} selected):";
                if (SelectNestedEntity(msg, count == 0, out Entity ent))
                {
                    if (ent != null)
                    {
                        if (Highlight)
                        {
                            TransientManager.AddTransient(
                                ent, 
                                TransientDrawingMode.Highlight, 
                                128, 
                                new IntegerCollection());
                        }
                        SelectedClones.Add(ent);
 
                        count++;
                    }
                    else
                    {
                        return;
                    }
                }
                else
                {
                    if (SelectedClones.Count>0)
                    {
                        CleanUpClonedEntities();
                    }
 
                    return;
                }
            }
        }
 
        #region private methods: using Editor.GetNestedEntity()
 
        private bool SelectNestedEntity(
            string msg, bool isFirstPick, out Entity nestedEntity)
        {
            nestedEntity = null;
            var oked = false;
 
            var opt = new PromptNestedEntityOptions($"\n{msg}:");
            
            opt.AllowNone = true;
            opt.AppendKeywordsToMessage = true;
            if (!isFirstPick)
            {
                opt.Keywords.Add("Done");
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Done";
            }
            else
            {
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Cancel";
            }
 
            var res = this.Editor.GetNestedEntity(opt);
            if (res.Status== PromptStatus.OK || res.Status== PromptStatus.Keyword)
            {
                if (res.Status== PromptStatus.OK)
                {
                    var entId = res.ObjectId;
                    using (var tran = 
                        entId.Database.TransactionManager.StartTransaction())
                    {
                        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
                        var clone = ent.Clone() as Entity;
                        if (entId.ObjectClass.DxfName.ToUpper() != "ATTRIB")
                        {
                            var ids = res.GetContainers();
                            if (ids.Length>0)
                            {
                                var bref = (BlockReference)tran.GetObject(
                                    ids[0], OpenMode.ForRead);
                                clone.TransformBy(bref.BlockTransform);
                            }
                        }
 
                        nestedEntity = clone;
                        nestedEntity.ColorIndex = HighlightColorIndex;
 
                        tran.Commit();
                    }
 
                    oked = true;
                }
                else
                {
                    if (res.StringResult == "Done") oked = true;
                }
            }
 
            return oked;
        }
 
        #endregion
    }
}

The CommandClass to actually do the nested entity selecting work:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(SelectMultiNestedEnts.MyCommands))]
 
namespace SelectMultiNestedEnts
{
    public class MyCommands
    {
        [CommandMethod("GetNested")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                using (var selector = new NestedEntSelector())
                {
                    selector.SelectNestedEntities(true);
                    if (selector.SelectedClones==null)
                    {
                        ed.WriteMessage("\n*Cancel*");
                    }
                    else
                    {
                        // Now that the cloned entities, at the place as selected,
                        // are available for the calling code to do whatever needed
                        // here: adding to database, or only being used as visual hints
                        ed.WriteMessage($"\n{selector.SelectedClones.Count} selected.");
                    }
                    
                    ed.GetString("\nPress Enter to continue...");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        [CommandMethod("MultiNested")]
        public static void DoMultiNestedEntitySelection()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                using (var selector = new MultiNestedEntSelector(
                    Helper.GetWindowSelectionState))
                {
                    selector.SelectNestedEntities();
                    if (selector.SelectedClones == null)
                    {
                        ed.WriteMessage("\n*Cancel*");
                    }
                    else
                    {
                        // Now that the cloned entities, at the place as selected,
                        // are available for the calling code to do whatever needed
                        // here: adding to database, or only being used as visual hints
                        ed.WriteMessage($"\n{selector.SelectedClones.Count} selected.");
                        ed.GetString("\nPress Enter to continue...");
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
    }
}

Watch this video clip for the code action.

I  emphasize it again: the result of running the code is a collection of non-database-residing entities are created, which serve as Drawable objects for Transient Graphics, so that user sees highlighted entities as visual hint of the selection. It is up to the calling procedure to decide what to do with these entities. To easy the burden of calling code, I implement the base class as IDisposable, so that as long as the [Multi]NestedEntSelector is used with using(){...} block, these non-database-residing entities will be disposed automatically.


Tuesday, August 20, 2019

Selecting Multiple Nested Entities - 1 of 2

The question on how to select multiple nested entities (from a block reference, of course) has been  raised a while ago without any reply here and and been raised recently again from a much older post here. When I saw this question, I though I may give it a try to see what would come out, considering I did post an article on highlighting attribute (a nested entity in block reference) a few years ago.

I finally found a bit of time to write some code on this topic.

But let me first define what "Selection" here. To AutoCAD user, "Selection" is entities being clicked on, or enclosed/crossed by window/polygon/fence and AutoCAD makes them highlighted, so that user knows which entities are "selected". But "Selection" to us, programmer, being visually highlighted or not is just an extra option; we need to actually know which entities is selected by knowing it ObjectId, so that after the selection, we can do something with the entity's information.

I decided to try 2 different approaches:
  • user selects nested entity with one click a time (in this article)
  • user does a window-selecting on 1 single block reference. It can be crossing-window/polygon, or even fence selecting. But for the purpose of simplicity, I'll only do window-selecting (in next article, hopefully not wait too long before I can find time for it)
In this first try, I decided to use PromptNestedEntityOptions with Editor.GetNestedEntity(), which returns PromptNestedEntityResult. There are a few interesting things about PromptNestedEntityResult that effectively impacts how the code work:

  • Since it is meant for selecting nested entity, PromptNestedEntityResult has a method GetContainers(), which returns an array of ObjectId. When a regular nested entity (i.e. not an AttributeReference in the block reference), the returned array only has one ObjectId in it, which is the block reference, not the block definition of this block reference. So, "Container" here is bit confusing: the block reference does not actually contains any entity definition in block definition, except for attribute reference. Even more "shocking" fact is that if the selected nested entity is an AttributeReference, the GetContainer() method returns 0-length array, meaning no container! 
  • The PromptNestedEntityResult.ObjectId returns selected nested entity's ObjectId, as expected. However, we need to keep it in mind that if the nested entity is AttributeReference, then the ObjectId is the AttributeReference's Id, if it is any entity defined in block definition, it is the Id of that entity inside block definition. So, if you do nested entity selection on 2 block references (out of the same block definition), you would get the same nested entity Id, even you clicked different block reference instances.
  • With PromptNestedEntityOption/Editor.GetNestedEntity(), we can still entities that are not BlockReference, such as Line, Circle... In this case, the PromptNestedEntityResult.GetContainer() always return 0-length array of ObjectId, meaning no "container".
Because the nested entities selected by Editor.GetNestedEntity() are actually entities in block definition (except for AttributeReference), I decided to make the class NestedEntSelector use a DBObjectCollection to hold a set of cloned entities in the place of block reference, which a not database-residing objects. This way, the calling code decides what to do with them, possibly adding to database, or disposing them; this is similar to Explode() method. Also, since it is most likely in real AutoCAD use that we need to provide visual hint to indicate the selecting occurs, these cloned entities are used to generate highlighting effect with Transient Graphics. To make it easier for the calling code to handle non-database-residing entities, I also implemented IDisposable interface, so that the calling code can simply use NestedEntSelector in a using(){...} block to have those cloned entities disposed correctly, regardless them being added into database or not.

Here is the code of class NestedEntSelector:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectMultiNestedEnts
{
    public class NestedEntSelector : IDisposable
    {
        private Editor _ed = null;
        private bool _highlight = false;
        private int _color = 1;
        private TransientManager tsManager = TransientManager.CurrentTransientManager;
 
        public DBObjectCollection SelectedClones { private set; get; }
 
        public void SelectNestedEntities(bool highlight=true, int highlightColorIndex=1)
        {
            _ed = CadApp.DocumentManager.MdiActiveDocument.Editor;
            SelectedClones = new DBObjectCollection();
 
            int count = 0;
            _highlight = highlight;
            _color = highlightColorIndex;
 
            while (true)
            {
                var msg = $"Select a nested entity in a block ({count} selected):";
                if (SelectNestedEntity(msg, count == 0, out Entity ent))
                {
                    if (ent != null)
                    {
                        if (_highlight)
                        {
                            tsManager.AddTransient(
                                ent, 
                                TransientDrawingMode.Highlight, 
                                128, 
                                new IntegerCollection());
                        }
                        SelectedClones.Add(ent);
 
                        count++;
                    }
                    else
                    {
                        return;
                    }
                }
                else
                {
                    if (SelectedClones.Count>0)
                    {
                        CleanUpClonedEntities();
                    }
 
                    return;
                }
            }
        }
 
        public void Dispose()
        {
            CleanUpClonedEntities();
        }
 
        #region private methods
 
        private void CleanUpClonedEntities()
        {
            if (_highlight)
            {
                foreach (DBObject obj in SelectedClones)
                {
                    tsManager.EraseTransient(
                        obj as Drawable, new IntegerCollection());
                }
            }
 
            foreach (DBObject obj in SelectedClones)
            {
                obj.Dispose();
            }
 
            SelectedClones.Clear();
            SelectedClones.Dispose();
            SelectedClones = null;
        }
 
        private bool SelectNestedEntity(
            string msg, bool isFirstPick, out Entity nestedEntity)
        {
            nestedEntity = null;
            var oked = false;
 
            var opt = new PromptNestedEntityOptions($"\n{msg}:");
            
            opt.AllowNone = true;
            opt.AppendKeywordsToMessage = true;
            if (!isFirstPick)
            {
                opt.Keywords.Add("Done");
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Done";
            }
            else
            {
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Cancel";
            }
 
            var res = _ed.GetNestedEntity(opt);
            if (res.Status== PromptStatus.OK || res.Status== PromptStatus.Keyword)
            {
                if (res.Status== PromptStatus.OK)
                {
                    var entId = res.ObjectId;
                    using (var tran = 
                        entId.Database.TransactionManager.StartTransaction())
                    {
                        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
                        var clone = ent.Clone() as Entity;
                        if (entId.ObjectClass.DxfName.ToUpper() != "ATTRIB")
                        {
                            var ids = res.GetContainers();
                            if (ids.Length>0)
                            {
                                var bref = (BlockReference)tran.GetObject(
                                    ids[0], OpenMode.ForRead);
                                clone.TransformBy(bref.BlockTransform);
                            }
                        }
 
                        nestedEntity = clone;
                        nestedEntity.ColorIndex = _color;
 
                        tran.Commit();
                    }
 
                    oked = true;
                }
                else
                {
                    if (res.StringResult == "Done") oked = true;
                }
            }
 
            return oked;
        }
 
        #endregion
    }
}

As usual, here is a CommandClass where the NestedEntSelector is used:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(SelectMultiNestedEnts.MyCommands))]
 
namespace SelectMultiNestedEnts
{
    public class MyCommands
    {
        [CommandMethod("GetNested")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                using (var selector = new NestedEntSelector())
                {
                    selector.SelectNestedEntities(true);
                    if (selector.SelectedClones==null)
                    {
                        ed.WriteMessage("\n*Cancel*");
                    }
                    else
                    {
                        // Now that the cloned entities, at the place as selected,
                        // are available for the calling code to do whatever needed
                        // here: adding to database, or only being used as visual hints
                        ed.WriteMessage($"\n{selector.SelectedClones.Count} selected.");
                    }
                    
                    ed.GetString("\nPress Enter to continue...");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
    }
}

Watch this video clip to see how the code works:

In next article I'll try to allow user to window-select multiple nested entities from a block reference. Stay tuned.

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.