Sunday, November 21, 2021

Use COM API/VBA to Open Viewport on Layout to Show Given Content in ModelSpace

Since I moved to AutoCAD .NET API more than 15 years ago, I dealt with AutoCAD VBA less and less, yet, still not completely left it behind. There was a recent discussion in the VBA discussion forum on the topic of opening viewport on layout to show ModelSpace content within a given area/scope. While there are a few useful replies, the OP still expects/prefers VBA code sample. According to the OP's request, the VBA code should not be too complicated, so I gave it a try. Once again, instead of posting the code sample as reply in the discussion thread, it would be better to post here with better readability.

First, let me assume the conditions of running the code:

A rectangle block is used as content boundary in the ModelSpace to be shown in a layout's viewport with given scale.

This picture shows the ModelSpace contents to be shown:


The coding process would be:

1. Determine the rectangle's size (width/height), rotation angle, and the coordinates of its corners;

2. Open a Viewport on a layout with the same size as the rectangle (scaled, of course);

3. Activate the Viewport in MSpace mode and zoom the view to the rectangle.

Note, ideally, based on the rectangle's size and required scale, the code could do calculation to decide what paper size is needed for the layout. But I omitted this calculation and simply manually chose a paper size for the layout ("Layout1") and choose the open the AcadPViewport at point (200, 150) as its center, which is based on the boundary block's size and a scale of 1/20 (0.05).

Here is the class BorderBlock, which holds all geometric information required to create the Viewport (e.g. in VBA Editor, create a class, name it as BorderBlock):

Option Explicit

Private mBlkName As String
Private mHeight As Double
Private mWidth As Double
Private mRotation As Double
Private mScale As Double
Private mLLCorner(0 To 2) As Double
Private mLRCorner(0 To 2) As Double
Private mULCorner(0 To 2) As Double
Private mURCorner(0 To 2) As Double

Private Sub Class_Initialize()
    mBlkName = "VPORT_BOUNDARY"
    mScale = 0.05
End Sub

Public Property Get VPortTweestAngle() As Double
    VPortTweestAngle = mRotation
End Property
Public Property Get Height() As Double
    Height = mHeight
End Property
Public Property Get Width() As Double
    Width = mWidth
End Property
Public Property Get VPortWidth() As Double
    VPortWidth = mWidth * mScale
End Property
Public Property Get VPortHeigth() As Double
    VPortHeigth = mHeight * mScale
End Property
Public Property Get VPortScale() As Double
    VPortScale = mScale
End Property

Public Property Get LLCorner() As Variant
    LLCorner = mLLCorner
End Property

Public Property Get URCorner() As Variant
    URCorner = mURCorner
End Property

Public Sub GetViewportData(blk As AcadBlockReference)

    '' Get border block's width and height
    Dim oldRotation As Double
    Dim minPt As Variant
    Dim maxPt As Variant
    If blk.Rotation <> 0 Then
        oldRotation = blk.Rotation
        blk.Rotation = 0#
    End If
    blk.GetBoundingBox minPt, maxPt
    mWidth = maxPt(0) - minPt(0)
    mHeight = maxPt(1) - minPt(1)
    blk.Rotation = oldRotation

    '' View port tweest angle
    mRotation = 2 * 3.1415926 - blk.Rotation
    
    '' Get 4 corners of the border block
    mLLCorner(0) = blk.InsertionPoint(0)
    mLLCorner(1) = blk.InsertionPoint(1)
    mLLCorner(2) = blk.InsertionPoint(2)
    
    Dim angle As Double
    Dim nextCorner As Variant
    
    angle = blk.Rotation
    nextCorner = CalculateNextCorner(mLLCorner, angle, mWidth)
    mLRCorner(0) = nextCorner(0)
    mLRCorner(1) = nextCorner(1)
    mLRCorner(2) = nextCorner(2)
    
    angle = angle + 3.1415926 / 2#
    nextCorner = CalculateNextCorner(mLRCorner, angle, mHeight)
    mURCorner(0) = nextCorner(0)
    mURCorner(1) = nextCorner(1)
    mURCorner(2) = nextCorner(2)
    
    angle = angle + 3.1415926 / 2#
    nextCorner = CalculateNextCorner(mURCorner, angle, mWidth)
    mULCorner(0) = nextCorner(0)
    mULCorner(1) = nextCorner(1)
    mULCorner(2) = nextCorner(2)
    
End Sub

Private Function CalculateNextCorner(corner As Variant, angle As Double, distance As Double) As Variant
    
    Dim nextCorner(0 To 2) As Double
    Dim x As Double
    Dim y As Double
    
    x = distance * Cos(angle)
    x = x + corner(0)
    
    y = distance * Sin(angle)
    y = y + corner(1)
    
    nextCorner(0) = x: nextCorner(1) = y: nextCorner(2) = corner(2)
    
    CalculateNextCorner = nextCorner
    
End Function


There is the VBA module with a public method OpenPVport() as a macro:


Option Explicit

Public Sub OpenPVport()
    
    Dim border As BorderBlock
    Set border = SelectBorderBlock()
    If border Is Nothing Then Exit Sub
    
    ThisDrawing.ActiveSpace = acPaperSpace
    ThisDrawing.ActiveLayout = ThisDrawing.Layouts("Layout1")
    
    CreatePVort border
    
End Sub

Private Function SelectBorderBlock() As BorderBlock
    If ThisDrawing.ActiveSpace <> acModelSpace Then
        ThisDrawing.ActiveSpace = acModelSpace
    End If
    Dim blk As AcadBlockReference
    Dim ent As AcadEntity
    Dim pt As Variant
    On Error Resume Next
    ThisDrawing.Utility.GetEntity ent, pt, vbCr & "Select vport boundary block:"
    If ent Is Nothing Then Exit Function
    If TypeOf ent Is AcadBlockReference Then
        Set blk = ent
        If UCase(blk.Name) <> "VPORT_BOUNDARY" Then
            MsgBox "Selected wrong block: not VPORT_BOUNDARY block!"
            Exit Function
        End If
    Else
        MsgBox "Selected entity is not a block reference!"
        Exit Function
    End If
    
    Dim border As BorderBlock
    Set border = New BorderBlock
    border.GetViewportData blk
    Set SelectBorderBlock = border
End Function

Private Sub CreatePVort(border As BorderBlock)

    Dim vport As AcadPViewport
    Dim center(0 To 2) As Double
    center(0) = 200: center(1) = 150: center(2) = 0
    Set vport = ThisDrawing.PaperSpace.AddPViewport( _
        center, border.VPortWidth, border.VPortHeigth)
    vport.Display True
    vport.TwistAngle = border.VPortTweestAngle
    
    '' zoom properly
    ThisDrawing.MSpace = True
    ThisDrawing.ActivePViewport = vport
    ThisDrawing.MSpace = True
    ZoomWindow border.LLCorner, border.URCorner
    ThisDrawing.MSpace = False
    
    '' Lock the viewport's display
    vport.DisplayLocked = True
    vport.Update
      
End Sub

See following video clip for the action of the code:





Friday, September 10, 2021

Using EntityJig with Keyword Options for Copying Entity

 A question was asked in Autodesk's AutoCAD .NET forum. There are actually 2 issues raised in the question: 1) during jig's dragging process, can/how we use keyword options; 2) which jig (EntityJig or DrawJig) should be used for the copying.

I could post my reply directly to offer my opinion on how I would do it. But as programmer, I thought code often provides more helpful answer than words. So, as I usually do, I quickly put some code together to demonstrate what I do in this situation and publish the code here for better readability.

Some thoughts here:

  • Jig is a nice way in AutoCAD to visually show the possible outcome of user input. That is, when Jig begins, AutoCAD is effectively waiting for user input, either from mouse click, or keyboard press. That is, once a user input occurs, jig ends, and a PromptResult object is returned. So, if you use keyword options in jig, which might change the jig's initial condition (in the case of the posted question, the OP want to change to the jig's target entity), the current jig is ended once a keyword is entered. Based on the keyword option's logic, we may want to loop back to do another round jig with new condition.
  • For copying, I can see both DrawJig and EntityJig can be used. But in this sample project, I choose to create a class MyJigCopier, which uses an EntityJig inside to encapsulate the entire copying process.
OK, see code below.

First, the class MyEntityJig. The purpose of using this jig is to move around the non-database-residing entity before it being added to the database, or obtain user's other inputs other than entity copy's position (i.e. cancellation, keyword selected). So, the code is pretty simple:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
 
namespace KeywordsInJig
{
    public enum JigResult
    {
        JigDone = 0,
        RepickSource = 1,
        RepickBasePoint = 2,
        JigCancelled = 3,
    }
 
    public class MyEntityJig : EntityJig
    {
        private readonly Editor _ed;
        private readonly Point3d _basePoint;
        private Point3d _currPoint;
        private Point3d _prevPoint;
 
        public MyEntityJig(Editor ed,Entity ent, Point3d basePt) : base(ent)
        {
            _ed = ed;
            _basePoint = basePt;
            _currPoint = basePt;
            _prevPoint = basePt;
        }
 
        public Point3d CopyLocation { private setget; }
 
        public JigResult Drag()
        {
            JigResult result; 
 
            var res = _ed.Drag(this);
            if (res.Status== PromptStatus.OK)
            {
                CopyLocation = _currPoint;
                result = JigResult.JigDone;
            }
            else if (res.Status== PromptStatus.Keyword)
            {
                if (res.StringResult=="Source")
                {
                    result = JigResult.RepickSource;
                }
                else
                {
                    result = JigResult.RepickBasePoint;
                }
            }
            else
            {
                result = JigResult.JigCancelled;
            }
 
            return result;
        }
 
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var options = new JigPromptPointOptions(
                "\nSelect position");
            options.UseBasePoint = true;
            options.BasePoint = _basePoint;
            options.Cursor = CursorType.RubberBand;
            options.Keywords.Add("Source");
            options.Keywords.Add("Basepoint");
            options.Keywords.Default = "Source";
            options.UserInputControls =
                UserInputControls.NullResponseAccepted |
                UserInputControls.Accept3dCoordinates;
 
            var res = prompts.AcquirePoint(options);
            if (res.Status== PromptStatus.OK)
            {
                if (res.Value.Equals(_currPoint))
                {
                    return SamplerStatus.NoChange;
                }
                else
                {
                    _currPoint = res.Value;
                    return SamplerStatus.OK;
                }
            }
            else if (res.Status== PromptStatus.Keyword)
            {
                return SamplerStatus.OK;
            }
            else
            {
                return SamplerStatus.Cancel;
            }
        }
 
        protected override bool Update()
        {
            var mt = Matrix3d.Displacement(_prevPoint.GetVectorTo(_currPoint));
            Entity.TransformBy(mt);
            _prevPoint = _currPoint;
            return true;
        }
    }
}

Here is the class MyJigCopier, which uses MyEntityJig inside, performing the entity copying work. Note, for simplicity, I call Entity.Clone() to create a copy of the selected entity, which is used in MyEntityJig class. Because user could choose keyword options during jig process, a while(){...} loop is used to keep the jig going until either the copy location is picked, or the jig is cancelled.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
 
namespace KeywordsInJig
{
    public class MyJigCopier
    {
        private ObjectId _sourceEntId = ObjectId.Null;
        private Point3d _basePoint;
 
        private Document _dwg;
        private Database _db;
        private Editor _ed;
 
        public void DoCopy(Document dwg)
        {
            _dwg = dwg;
            _db = _dwg.Database;
            _ed = _dwg.Editor;
 
            JigResult result = JigResult.JigDone;
 
            _sourceEntId = SelectSourceEntity(_ed);
            if (_sourceEntId.IsNull)
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            if (!PickBasePoint(_ed, out _basePoint))
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            while (true)
            {
                if (result == JigResult.RepickSource)
                {
                    _sourceEntId = SelectSourceEntity(_ed);
                    if (_sourceEntId.IsNull)
                    {
                        _ed.WriteMessage("\n*Cancel*\n");
                        return;
                    }
                }
 
                if (result== JigResult.RepickBasePoint || result == JigResult.RepickSource)
                {
                    if (!PickBasePoint(_ed, out _basePoint))
                    {
                        _ed.WriteMessage("\n*Cancel*\n");
                        return;
                    }
                }
 
                using (var tran = _db.TransactionManager.StartTransaction())
                {
                    var ent = (Entity)tran.GetObject(_sourceEntId, OpenMode.ForRead);
                    ent.Highlight();
                    var clone = ent.Clone() as Entity;
 
                    var jig = new MyEntityJig(_ed, clone, _basePoint);
                    result = jig.Drag();
 
                    ent.Unhighlight();
 
                    switch(result)
                    {
                        case JigResult.JigDone:
                            AddEntityCopyToDb(clone, tran);
                            tran.Commit();
                            break;
                        case JigResult.RepickBasePoint:
                        case JigResult.RepickSource:
                            // go back to next loop
                            break;
                        default:
                            clone.Dispose();
                            tran.Abort();
                            _ed.WriteMessage("\n*Cancel*\n");
                            break;
                    }
                }
 
                if (result == JigResult.JigDone || result == JigResult.JigCancelled) return;
            }
        }
 
        #region private methods
 
        private ObjectId SelectSourceEntity(Editor ed)
        {
            var res = ed.GetEntity("\nSelect source entity to copy from:");
            if (res.Status == PromptStatus.OK)
                return res.ObjectId;
            else
                return ObjectId.Null;
        }
 
        private bool PickBasePoint(Editor edout Point3d pt)
        {
            pt = new Point3d();
            
            var res = ed.GetPoint("\nSelect base point:");
            if (res.Status== PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void AddEntityCopyToDb(Entity ent, Transaction tran)
        {
            var space = (BlockTableRecord)tran.GetObject(_db.CurrentSpaceId, OpenMode.ForWrite);
            space.AppendEntity(ent);
            tran.AddNewlyCreatedDBObject(ent, true);
        }
 
        #endregion
    }
}

To use the class MyJigCopier is really simple:
namespace KeywordsInJig
{
    public class MyCommands
    {
        [CommandMethod("DoCopy")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var copier = new MyJigCopier();
            copier.DoCopy(dwg);
        }
    }
}

See the video clip below showing how the code works:



Saturday, August 14, 2021

An EntityJig For MLine Creation

 I recently involved a discussion in Autodesk's .NET forum, which is about to create double-lines. The OP chose to create a MLine and then explode it to achieve the goal of drawing line segments continuously with double offsets. While this is an viable way of doing this, with the help of .NET API's Curve.GetOffsetCurves(), the goal of drawing double (or triple) lines is rather easier to achieve than drawing a MLine and then explode it. 

But this post is not about which way is better/easier to draw double lines. The OP somehow ran in to issues that the "Draw MLine -> Explode it" code that generates extra lines after explosion. I did not spent too much time to troubleshoot the code posted. Rather I simply write some code to prove that drawing MLine and then exploding it should be fairly easy (many programmers, including myself, have less patient to fix others' code and choose to rewrite their onw for the same process). So, once my code drew the MLine and exploded it correctly (as expected, I could not see why OP ran into that issue, as posted), I just added a bit extra code to make it fully functional MLine jig, so that I can post a working code sample here for anyone who may be interested in. 

This is the class MLineJig:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace C3DMiscTest
{
    public class MLineJig : EntityJig
    {
        private Point3d _currPoint;
        private Point3d _prevPoint;
 
        private readonly Document _dwg;
        private readonly Editor _ed;
        private readonly ObjectId _styleId;
 
        private readonly TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
 
        private Mline _mline;
        private Mline _ghost = null;
 
        public MLineJig(string mlineStyleName = null) : base(new Mline())
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            _ed = _dwg.Editor;
 
            _styleId = GetMLineStyle(_dwg.Database, mlineStyleName);
            if (_styleId.IsNull)
            {
                throw new InvalidCastException("No MLine style available!");
            }
 
            _mline = Entity as Mline;
        }
 
        public void Draw()
        {
            var res = _ed.GetPoint("\nSelect start point:");
            if (res.Status != PromptStatus.OK) return;
            _currPoint = res.Value;
            _prevPoint = res.Value;
            
            var cancelled = false;
            ObjectId explodeId = ObjectId.Null;
 
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                _mline.SetDatabaseDefaults(_dwg.Database);
                _mline.Style = _styleId;
                _mline.Justification = MlineJustification.Zero;
                _mline.Normal = new Vector3d(0, 0, 1);
 
                _mline.AppendSegment(_currPoint);
                var space = (BlockTableRecord)tran.GetObject(
                    _dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                explodeId = space.AppendEntity(_mline);
                tran.AddNewlyCreatedDBObject(_mline, true);
                _dwg.TransactionManager.QueueForGraphicsFlush();
 
                while (true)
                {
                    ClearGhostImage();
 
                    var jigRes = _ed.Drag(this);
                    if (jigRes.Status == PromptStatus.OK)
                    {
                        _mline.AppendSegment(_currPoint);
                        _prevPoint = _currPoint;
                        _dwg.TransactionManager.QueueForGraphicsFlush();
                    }
                    else if (jigRes.Status == PromptStatus.Keyword)
                    {
                        if (jigRes.StringResult=="Cancel")
                        {
                            cancelled = true;
                        }
                        break;
                    }
                    else
                    {
                        cancelled = true;
                        break;
                    }
                }
 
                if (!cancelled)
                {
                    tran.Commit();
                    _ed.Regen();
                }
                else
                {
                    tran.Abort();
                }
 
                ClearGhostImage();
            }
 
            if (cancelled || _mline.NumberOfVertices < 2)
            {
                _mline.Dispose();
            }
            else
            {
                if (PromptForExplosion())
                {
                    ExplodeMLine(explodeId);
                }
            }
        }
 
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var jigOpt = new JigPromptPointOptions(
                "\nSelect next point:");
            jigOpt.UseBasePoint = true;
            jigOpt.BasePoint = _prevPoint;
            jigOpt.Cursor = CursorType.RubberBand;
            if (_mline.NumberOfVertices >= 2)
            {
                jigOpt.Keywords.Add("Done");
                jigOpt.Keywords.Add("Cancel");
                jigOpt.Keywords.Default = "Done";
                jigOpt.AppendKeywordsToMessage = true;
                jigOpt.UserInputControls = UserInputControls.NullResponseAccepted;
            }
 
            var jigRes = prompts.AcquirePoint(jigOpt);
            if (jigRes.Status== PromptStatus.OK)
            {
                if (jigRes.Value==_prevPoint)
                {
                    return SamplerStatus.NoChange;
                }
                else
                {
                    _currPoint = jigRes.Value;
                    return SamplerStatus.OK;
                }
            }
            else if (jigRes.Status== PromptStatus.Keyword)
            {
                return jigRes.StringResult == "Done" ?
                    SamplerStatus.NoChange : SamplerStatus.Cancel;
            }
            else
            {
                return SamplerStatus.Cancel;
            }
        }
 
        protected override bool Update()
        {
            CreateGhostImage();
            return true;
        }
 
        private ObjectId GetMLineStyle(Database dbstring styleName)
        {
            var id = ObjectId.Null;
            
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var dict = (DBDictionary)tran.GetObject(
                    db.MLStyleDictionaryId, OpenMode.ForRead);
                if (!string.IsNullOrEmpty(styleName) &&
                    dict.Contains(styleName))
                {
                    id = dict.GetAt(styleName);
                }
                else
                {
                    foreach (DBDictionaryEntry entry in dict)
                    {
                        id = entry.Value;
                        break;
                    }
                }
                tran.Commit();
            }
 
            return id;
        }
 
        private void CreateGhostImage()
        {
            ClearGhostImage();
 
            _ghost = new Mline();
            _ghost.SetDatabaseDefaults(_dwg.Database);
            _ghost.Style = _styleId;
            _ghost.Justification = MlineJustification.Zero;
            _ghost.Normal = new Vector3d(0, 0, 1);
            _ghost.ColorIndex = 2;
 
            _ghost.AppendSegment(_prevPoint);
            _ghost.AppendSegment(_currPoint);
 
            _tsManager.AddTransient(
                _ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearGhostImage()
        {
            if (_ghost != null)
            {
                _tsManager.EraseTransient(
                    _ghost, new IntegerCollection());
                _ghost.Dispose();
            }
        }
 
        private bool PromptForExplosion()
        {
            var kOpt = new PromptKeywordOptions(
                "\nDo you want to explode the MLine:");
            kOpt.Keywords.Add("Yes");
            kOpt.Keywords.Add("No");
            kOpt.Keywords.Default = "No";
            var res = _ed.GetKeywords(kOpt);
            if (res.Status== PromptStatus.OK &&
                res.StringResult=="Yes")
            {
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void ExplodeMLine(ObjectId entId)
        {
            using (var tran = entId.Database.TransactionManager.StartTransaction())
            {
                var entity = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
 
                var objects = new DBObjectCollection();
                entity.Explode(objects);
 
                var space = (BlockTableRecord)tran.GetObject(
                    entId.Database.CurrentSpaceId, OpenMode.ForWrite);
                foreach (DBObject obj in objects)
                {
                    space.AppendEntity(obj as Entity);
                    tran.AddNewlyCreatedDBObject(obj, true);
                }
                entity.Erase();
                tran.Commit();
            }             
        }
    }
}
To use MLineJig class:

[CommandMethod("DoubleLine")]
public static void DrawDoubleLine()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    try
    {
        var jig = new MLineJig();
        jig.Draw();
    }
    catch(System.Exception ex)
    {
        CadApp.ShowAlertDialog($"Error:\n{ex.Message}");
    }
}

See this video clip for the code action:




Monday, March 15, 2021

Highlight Entity In Block - Upated: Added VB.NET Code

 A question on how to highlight Polyline in a BlockReference was posted in Autodesk's .NET discussion forum. In my reply, I proposed to use TransientGraphics to render the highlight. Here I put together a quick project to show how easy to achieve that.

By the way, in the past I posted 2 articles on highlighting attributes in BlockReference here and here. However, because attribute (AttributeReference) is owned by BlockReference, while entity we see in BlockReference is actually part of block definition (BlockTableRecord), we cannot highlight it individually. That is where TransientGraphics comes into play: we create a clone of the target entity in block definition, transform its location to where the BlockReference is, and then use the cloned entity as Drawable to draw TransientGraphics

Here is the class BlockNestedEntityHighlighter:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace HighlightEntityInBlock
{
    public class BlockNestedEntityHighlighter : IDisposable
    {
        private Entity _entClone = null;
        private readonly TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
        private int _colorIndex = 2;
 
        public void HighlightEntityInBlock(ObjectId nestedEntId, Matrix3d transform)
        {
            ClearHighlight();
            using (var tran = nestedEntId.Database.TransactionManager.StartTransaction())
            {
                var ent = (Entity)tran.GetObject(nestedEntId, OpenMode.ForRead);
                _entClone = ent.Clone() as Entity;
                tran.Commit();
            }
 
            _entClone.ColorIndex = _colorIndex;
            _entClone.TransformBy(transform);
 
            _tsManager.AddTransient(
                _entClone, 
                TransientDrawingMode.Highlight, 
                128, 
                new IntegerCollection());
        }
 
        public void Dispose()
        {
            ClearHighlight();
        }
 
        private void ClearHighlight()
        {
            if (_entClone != null)
            {
                _tsManager.EraseTransient(
                    _entClone, new IntegerCollection());
                _entClone.Dispose();
                _entClone = null;
            }
        }
    }
}

Here is the code running to highlight a selected nested entity from a BlockReference:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(HighlightEntityInBlock.MyCommands))]
 
namespace HighlightEntityInBlock
{
    public class MyCommands
    {
        [CommandMethod("HlEntInBlk")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (SelectNestedEntityInBlock(
                ed, out ObjectId nestedEntId, out Matrix3d blkTransform))
            {
                using (var highlighter = new BlockNestedEntityHighlighter())
                {
                    highlighter.HighlightEntityInBlock(nestedEntId, blkTransform);
                    ed.GetString("\nPress Enter to continue...");
                }
                ed.PostCommandPrompt();
            }
            else
            {
                ed.WriteMessage("\n*Cancel*\n");
            }
        }
 
        private static bool SelectNestedEntityInBlock(Editor ed,
            out ObjectId entId, out Matrix3d blkTransform)
        {
            entId = ObjectId.Null;
            blkTransform = Matrix3d.Identity;
 
            var res = ed.GetNestedEntity("\nPick an entity in a block:");
            if (res.Status== PromptStatus.OK)
            {
                entId = res.ObjectId;
                blkTransform = res.Transform;
                ed.WriteMessage($"\nentId: {entId}");
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

See this video clip for the effect:


For simplicity, I hard-coded the highlight color as yellow. If the color of the nested entity is yellow (whether is ByBlock, or ByLayer, or by itself) , the code could be enhanced to choose a different color automatically to make the highlight stand out; f the nested entity is a Polyline, the code could make its global width thicker; the code could also use different line type, or line weight.

Update

Since the author of the original post in Autodesk .NET discussion asked for a VB.NET version of the code shown here, I did a quick conversion and now also post it here:

Class BlockNestedEntityHighlighter:

Imports System
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.GraphicsInterface
 
Namespace HighlightEntityInBlock
    Public Class BlockNestedEntityHighlighter
        Implements IDisposable
 
        Private _entClone As Entity = Nothing
        Private ReadOnly _tsManager As TransientManager = TransientManager.CurrentTransientManager
        Private _colorIndex As Integer = 2
 
        Public Sub HighlightEntityInBlock(ByVal nestedEntId As ObjectIdByVal transform As Matrix3d)
            ClearHighlight()
 
            Using tran = nestedEntId.Database.TransactionManager.StartTransaction()
                Dim ent = CType(tran.GetObject(nestedEntId, OpenMode.ForRead), Entity)
                _entClone = TryCast(ent.Clone(), Entity)
                tran.Commit()
            End Using
 
            _entClone.ColorIndex = _colorIndex
            _entClone.TransformBy(transform)
            _tsManager.AddTransient(_entClone, TransientDrawingMode.Highlight, 128, New IntegerCollection())
        End Sub
 
        Public Sub Dispose() Implements IDisposable.Dispose
            ClearHighlight()
        End Sub
 
        Private Sub ClearHighlight()
            If _entClone IsNot Nothing Then
                _tsManager.EraseTransient(_entClone, New IntegerCollection())
                _entClone.Dispose()
                _entClone = Nothing
            End If
        End Sub
 
    End Class
End Namespace

CommandClass MyCommands:

Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.Runtime
Imports CadApp = Autodesk.AutoCAD.ApplicationServices.Application
Imports System.Runtime.InteropServices
 
<AssemblyCommandClass(GetType(HighlightEntityInBlock.MyCommands))>
Namespace HighlightEntityInBlock
    Public Class MyCommands
        <CommandMethod("HlEntInBlk")>
        Public Shared Sub RunMyCommand()
            Dim dwg = CadApp.DocumentManager.MdiActiveDocument
            Dim ed = dwg.Editor
            Dim nestedEntId As ObjectId = Nothing, blkTransform As Matrix3d = Nothing
 
            If SelectNestedEntityInBlock(ed, nestedEntId, blkTransform) Then
 
                Using highlighter = New BlockNestedEntityHighlighter()
                    highlighter.HighlightEntityInBlock(nestedEntId, blkTransform)
                    ed.GetString(vbLf & "Press Enter to continue...")
                End Using
 
                ed.PostCommandPrompt()
            Else
                ed.WriteMessage(vbLf & "*Cancel*" & vbLf)
            End If
        End Sub
 
        Private Shared Function SelectNestedEntityInBlock(ByVal ed As Editor, <OutByRef entId As ObjectId, <OutByRef blkTransform As Matrix3dAs Boolean
            entId = ObjectId.Null
            blkTransform = Matrix3d.Identity
            Dim res = ed.GetNestedEntity(vbLf & "Pick an entity in a block:")
 
            If res.Status = PromptStatus.OK Then
                entId = res.ObjectId
                blkTransform = res.Transform
                ed.WriteMessage($"\nentId: {entId}")
                Return True
            Else
                Return False
            End If
        End Function
    End Class
End Namespace

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.