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 { setget; }
        protected bool Highlight { setget; }
        protected int HighlightColorIndex { setget; }
        protected TransientManager TransientManager
        {
            get
            {
                return TransientManager.CurrentTransientManager;
            }
        }
 
        public DBObjectCollection SelectedClones { setget; }
 
        public abstract void SelectNestedEntities(
            bool highlight = trueint highlightColorIndex = 1);
 
        public void CleanUpClonedEntities()
        {
            if (Highlight)
            {
                foreach (DBObject obj in SelectedClones)
                {
                    this.TransientManager.EraseTransient(
                        obj as Drawablenew 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<EntityPoint2dCollectionWindowSelectionState> _winSelectionStateFunction = null;
 
        public MultiNestedEntSelector(
            Func<EntityPoint2dCollectionWindowSelectionStatewinSelectionStateFunction)
        {
            _winSelectionStateFunction = winSelectionStateFunction;
        }
 
        public override void SelectNestedEntities(
            bool highlight = trueint 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(
                    entTransientDrawingMode.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 windowout 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 windowPointsout 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 windowPointsout crossingSelection);
                    picked = true;
                }
            }
 
            return picked;
        }
 
        private void SetSelectionWindow(
            Point3d firstPtPoint3d secondPtout Point3dCollection windowout 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<EntityFindEntitiesByWindow()
        {
            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(entIdOpenMode.ForRead);
 
                    bool skip = false;
                    if (ent is AttributeDefinition)
                    {
                        var att = ent as AttributeDefinition;
                        if (!att.Constant) skip = true;
                    }
                    if (skipcontinue;
 
                    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(idOpenMode.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(entpts);
            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 entityPoint2dCollection window)
        {
            var state = WindowSelectionState.Outside;
 
            using (var poly = CreatePolyline(window))
            {
                var interPoints = new Point3dCollection();
 
                poly.IntersectWith(
                    entityIntersect.ExtendThis, interPointsIntPtr.Zero, IntPtr.Zero);
                if (interPoints.Count>0)
                {
                    state = WindowSelectionState.Crossing;
                }
                else
                {
                    if (IsInsideWindow(polyentity))
                    {
                        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(inew Point2d(points[i].X, points[i].Y), 0.0, 0.0, 0.0);
            }
 
            poly.Closed = true;
 
            return poly;
        }
 
        private static bool IsInsideWindow(CadDb.Polyline polyEntity entity)
        {
           if (GetPointFromEntity(entityout Point2d point))
            {
                return IsPointInside(polypoint);
            }
 
            return false;
        }
 
        private static bool GetPointFromEntity(Entity entityout 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 polyPoint2d point)
        {
            var pts = new Point2dCollection();
            for (int i=0; i < poly.NumberOfVertices; i++)
            {
                pts.Add(poly.GetPoint2dAt(i));
            }
 
            var inside = IsInside(pointpts);
            return inside;
        }
 
        private static bool IsInside(
            Point2d pointPoint2dCollection polygonPointsdouble 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(ipolygonPoints[i], 0.0, 0.0, 0.0);
                }
                poly.Closed = true;
 
                using (poly)
                {
                    using (var polygon = new MPolygon())
                    {
                        polygon.AppendLoopFromBoundary(polyfalsetolerance);
 
                        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=trueint 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(msgcount == 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 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 = 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(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 = 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;
 
[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();
        }
 
        [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.