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
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.
Nice post content is impressive to read ,Thanks for proving some helpful information.Hope you will keep on sharing articles.
ReplyDeleteThis provides good insight. You might also be interested to know more about generating more leads and getting the right intelligence to engage prospects. Techno Data Group implements new lead gen ideas and strategies for generating more leads and targeting the right leads and accounts.
TECHNO DATA GROUP
thanks ... that helps me a lot
ReplyDelete