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 setget; }
 
        public void SelectNestedEntities(bool highlight=trueint 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(msgcount == 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 Drawablenew IntegerCollection());
                }
            }
 
            foreach (DBObject obj in SelectedClones)
            {
                obj.Dispose();
            }
 
            SelectedClones.Clear();
            SelectedClones.Dispose();
            SelectedClones = null;
        }
 
        private bool SelectNestedEntity(
            string msgbool isFirstPickout 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(entIdOpenMode.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;
 
[assemblyCommandClass(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.

1 comment: