Friday, May 20, 2022

Assign a Custom Action to a Keyboard Key by Capturing Windows' Key-Press Message

This article is inspired by this question recently asked in AutoCAD .NET user discussion forum. As usual, I chose to post sample code regarding the topic here for better readability.

One of the famous posts on AutoCAD .NET from Kean Walmsley (March, 2008, 14 years ago!) talked about the topic of filtering Windows message inside AutoCAD.  Because the .NET API provides a way to look up the Windows message, so we, as .NET API programmer, can rather easily capture the Windows message (in this particular discussion, it is the WM_KEYDOWN message) and do things accordingly.

The Application class (Autodesk.AutoCAD.ApplicationServices.Core namespace) exposes PreTranslateMessage event with PreTranslateMessageEventArgs, which supplies a raw Windows meessage. 

Following code sample does what was asked in the question from the .NET forum: using "Delete" key to erase entities. 

First, DeleteKeyHandler class:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace EditorGetMethods
{
    public class DeleteKeyHandler
    {
        const int WM_KEYDOWN = 256;
        const int DELETE_KEY_CODE = 46;
 
        private bool _enabled = false;
        public bool Enabled => _enabled;
        public void EnableMyDelKey(bool enable)
        {
            if (enable)
            {
                CadApp.PreTranslateMessage += CadApp_PreTranslateMessage;
                _enabled = true;
            }
            else
            {
                CadApp.PreTranslateMessage -= CadApp_PreTranslateMessage;
                _enabled=false;
            }
        }
 
        private void CadApp_PreTranslateMessage(object sender, PreTranslateMessageEventArgs e)
        {
            if (e.Message.message==WM_KEYDOWN &&
                e.Message.wParam.ToInt32()==DELETE_KEY_CODE)
            {
                DeleteKeyPressed?.Invoke(this, EventArgs.Empty);
            }
        }
 
        public event EventHandler DeleteKeyPressed;
    }
}

To use it, here is the CommandClass/Method:

sing Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(EditorGetMethods.MyCommands))]
 
namespace EditorGetMethods
{
    public class MyCommands
    {
        private static DeleteKeyHandler _myDelKey = null;
 
        [CommandMethod("MyDelKey")]
        public static void EraseEntitiesWithDelKey()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (_myDelKey==null)
            {
                _myDelKey = new DeleteKeyHandler();
                _myDelKey.DeleteKeyPressed += DelKeyPressed;
            }
            if (!_myDelKey.Enabled)
            {
                _myDelKey.EnableMyDelKey(true);
                ed.WriteMessage("\nDeleteKeyHandler is enabled.");
            }
            else
            {
                _myDelKey.EnableMyDelKey(false);
                ed.WriteMessage("\nDeleteKeyHandler is disabled.");
            }
        }
 
        private static void DelKeyPressed(object sender, EventArgs e)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            if (dwg == null) return;
 
            var ed = dwg.Editor;
            ed.WriteMessage("\nDELETE key was pressed.");
 
            var res = ed.SelectImplied();
            if (res.Status != PromptStatus.OK) return;
            var ids = res.Value.GetObjectIds();
            using (dwg.LockDocument())
            {
                EraseSelectedEntities(ids);
            }
        }
 
        private static void EraseSelectedEntities(ObjectId[] ids)
        {
            using (var tran = ids[0].Database.TransactionManager.StartTransaction())
            {
                foreach (ObjectId id in ids)
                {
                    var ent = tran.GetObject(id, OpenMode.ForWrite);
                    ent.Erase();
                }
                tran.Commit();
            }
        }
    }
}

The code is rather simple and straightforward.

While the code shows how easy we can assign some keys from the keyboard to do particular things quite easily, I would be extremely cautious to do it. For example, in this particular case, it is obvious that erasing work triggered by pressing DELETE key would only work when there are entities in AutoCAD editor are preselected; when DELETE key is pressed, there could be another command is active, which may cause unexpected/unwanted result. Just think: what is the advantage here by pressing DELETE key over pressing "E" (the command ERASE)? None, not to mention the potential unwanted result.

Anyway, with the sample code showed here, we have seen how to capture Windows message sent to AutoCAD and to handle it accordingly for our business need.

Since the code is very simple and works as expected in my a few tests, so I omitted my usual companion video clip.


Thursday, February 24, 2022

Where The Tangent Point Is - Show It When User Moves Mouse

Frequent visitors here must have noticed that many of my articles talk about Transient Grphics. It is one of my favorite AutoCAD .NET API features. You see, if you develop AutoCAD application to help user interact with AutoCAD easier, more efficiently, CAD users would appreciate if your application provides as much visual assistance as possible. A while ago, a question posted in .NET API forum about finding tangent point of a curve from a point, I though it would be an interesting exercise to write a bit of code to give user visual feedback of potential tangent point when mouse is dragged.

The visual part, obviously by using Transient Graphics, is rather simple, as long as the tangent point is calculated correctly. The calculation can be purely done with trigonometric methods/formula. However, since I need to use AutoCAD entities to generate transient image, I took a easier way of using AutoCAD objects for the calculation. One may argue that using trigonometric math would use much less computer power then using AutoCAD object for such calculation. But considering how the code runs: while moving mouse to show the visual effect, AutoCAD is basically waiting for user to decide his/here input and doing nothing else. So, the extra computing power burden would not be an issue, IMO.

Anyway, here is the code:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace TangentTrail
{
    public class TangentFinder : IDisposable
    {
        private readonly Document _dwg;
        private readonly Editor _ed;
        private CircularArc3d _circularArc = null;
        private TransientManager tsManager = TransientManager.CurrentTransientManager;
 
        private Point3d? _tangent1 = null;
        private Point3d? _tangent2 = null;
 
        private Line _ghostLine1 = null;
        private Line _ghostLine2 = null;
        private DBPoint _ghostPoint1 = null;
        private DBPoint _ghostPoint2 = null;
 
        private readonly int _colorIndex;
        private bool _transientAdded = false;
 
        public TangentFinder(Document dwg, int ghostColorIndex=1)
        {
            _dwg= dwg;
            _ed = _dwg.Editor;
            _colorIndex = ghostColorIndex;
        }
 
        public Point3d? TangentPoint1 => _tagent1;
        public Point3d? TangentPoint2 => _tagent2;
        public bool DragForTangent()         {             if (!SelectCurve()) return false;             var pdMode = _dwg.Database.Pdmode;             try             {                 _dwg.Database.Pdmode = 34;                 _ed.PointMonitor += Editor_PointMonitor;                 var res = _ed.GetPoint(                     "\nSelect point to form a tangent line:");                 if (res.Status== PromptStatus.OK &&                      (_tangent1.HasValue || _tangent2.HasValue))                 {                     return true;                 }                 else                 {                     return false;                 }             }             finally             {                 _ed.PointMonitor-=Editor_PointMonitor;                 _dwg.Database.Pdmode = pdMode;             }         }         public void Dispose()         {             ClearGhost();         }         #region private methods         private bool SelectCurve()         {             var opt = new PromptEntityOptions(                 "\nSelect an ARC or a CIRCLE:");             opt.SetRejectMessage("\nInvalid: must be an ARC or a CIRCLE!");             opt.AddAllowedClass(typeof(Arc), true);             opt.AddAllowedClass(typeof(Circle), true);             var res = _ed.GetEntity(opt);             if (res.Status== PromptStatus.OK)             {                 using (var tran = new OpenCloseTransaction())                 {                     var curve = (Curve)tran.GetObject(res.ObjectId, OpenMode.ForRead);                     _circularArc = curve.GetGeCurve() as CircularArc3d;                 }                 return true;             }             else             {                 return false;             }         }         private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)         {             ClearGhost();             var pt = e.Context.RawPoint;                          CalculateTangentPoint(                 _circularArc, pt, out _tangent1, out _tangent2);             if (_tangent1.HasValue)             {                 ShowGhost(_ghostLine1, _ghostPoint1, pt, _tangent1.Value);             }             if (_tangent2.HasValue)             {                 ShowGhost(_ghostLine2, _ghostPoint2, pt, _tangent2.Value);             }         }         private void ClearGhost()         {             ClearGhost(_ghostLine1);             ClearGhost(_ghostLine2);             ClearGhost(_ghostPoint1);             ClearGhost(_ghostPoint2);         }         private void ClearGhost(Drawable ghost)         {             if (ghost != null)             {                 tsManager.EraseTransient(ghost, new IntegerCollection());                 ghost.Dispose();             }         }         private void ShowGhost(             Line ghostLine, DBPoint ghostPoint,              Point3d startPoint, Point3d endPoint)         {             if (ghostLine != null && !ghostLine.IsDisposed)             {                 ghostLine.Dispose();             }             ghostLine = null;             if (ghostPoint != null && !ghostPoint.IsDisposed)             {                 ghostPoint.Dispose();             }             ghostPoint = null;             ghostLine = new Line();             ghostLine.ColorIndex = _colorIndex;                          ghostLine.StartPoint = startPoint;             ghostLine.EndPoint = endPoint;             tsManager.AddTransient(                 ghostLine,                  TransientDrawingMode.DirectTopmost,                  128,                  new IntegerCollection());             ghostPoint = new DBPoint(endPoint);             ghostPoint.ColorIndex = _colorIndex;             tsManager.AddTransient(                 ghostPoint,                 TransientDrawingMode.DirectTopmost,                 128,                 new IntegerCollection());         }         private void CalculateTangentPoint(             CircularArc3d arc, Point3d point,             out Point3d? tangent1, out Point3d? tangent2)         {             tangent1 = null;             tangent2 = null;             var dist = point.DistanceTo(arc.Center);             if (dist < arc.Radius) return;             var angle = Math.Acos(arc.Radius / dist);             using (var line = new Line(arc.Center, point))             {                 var angle1 = line.Angle + angle;                 var angle2 = line.Angle - angle;                 using (var circle=new Circle(arc.Center, arc.Normal, arc.Radius))                 {                     var arcLen = angle1 * arc.Radius;                     var pt = circle.GetPointAtDist(arcLen);                     if (_circularArc.IsOn(pt))                     {                         tangent1 = pt;                     }                     arcLen = angle2 * arc.Radius;                     pt = circle.GetPointAtDist(arcLen);                     if (_circularArc.IsOn(pt))                     {                         tangent2 = pt;                     }                 }             }         }         #endregion     } }

The CommandClass/Method to run it:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(TangentTrail.MyCommands))]
 
namespace TangentTrail
{
    public class MyCommands 
    {
        [CommandMethod("GetTangent")]
        public static void GetTangent()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            using (var tangentTool = new TangentFinder(dwg))
            {
                if (tangentTool.DragForTangent())
                {
 
                }
            }
        }
 
    }
}


The video below shows the code in action:







Tuesday, February 15, 2022

Using Transient Graphics to Prompt User on Screen

I have been quite busy these days that I have not post an article for quite a while. Today, as I often do, based on my interaction in a discussion thread in the .NET forum, I put together some code quickly as the result of the discussion. To save time to repeat the topic that was discussed, please refer the link to the discussion there to see what the code is about. I only post the code and a companion video clip that shows the code execution result.

First a class TransientPrompt:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Geometry;
using System;
 
namespace TransintTextPrompt
{
    public class TransientPrompt : IDisposable
    {
        private readonly Document _dwg;
        private DBText _txtEntity = null;
        private string _promptText = "";
        private bool _enabled = false;
 
        private TransientManager tsManager = TransientManager.CurrentTransientManager;
 
        public TransientPrompt(Document dwg, string initialText)
        {
            _dwg = dwg;
            _promptText = initialText;
        }
 
        public void Enable(bool enable)
        {
            _enabled = enable;
            if (_enabled)
            {
                _dwg.ViewChanged += ViewChanged;
                if (_txtEntity != null) _txtEntity.Dispose();
                _txtEntity = new DBText();
                _txtEntity.SetDatabaseDefaults();
                _txtEntity.HorizontalMode = TextHorizontalMode.TextCenter;
                _txtEntity.VerticalMode = TextVerticalMode.TextVerticalMid;
                _txtEntity.TextString = _promptText;
                _txtEntity.ColorIndex = 1;
                CreateTransient();
            }
            else
            {
                _dwg.ViewChanged -= ViewChanged;
                ClearTransient();
                if (_txtEntity != null) _txtEntity.Dispose();
            }
        }
 
        public void SetPromptText(string prompt)
        {
            _promptText = prompt;
            if (_txtEntity!=null)
            {
                ClearTransient();
                CreateTransient();
            }
        }
        
        public void Dispose()
        {
            if (_txtEntity != null)
            {
                tsManager.EraseTransient(_txtEntity, new IntegerCollection());
                _txtEntity.Dispose();
            }
        }
 
        #region private methods
 
        private void ViewChanged(object sender, EventArgs e)
        {
            if (!_enabled) return;
            ClearTransient();
            CreateTransient();
        }
 
        private void DrawingTransient()
        {
            if (_txtEntity!=null)
            {
                tsManager.AddTransient(_txtEntity, TransientDrawingMode.DirectShortTerm, 128, new IntegerCollection());
            }
        }
        private void ClearTransient()
        {
            if (_txtEntity!=null)
            {
                tsManager.EraseTransient(_txtEntity, new IntegerCollection());
            }
        }
 
        private void GetViewSize(out double height, out double width, out Point3d center)
        {
            center = (Point3d)Application.GetSystemVariable("VIEWCTR");
            height = (double)Application.GetSystemVariable("VIEWSIZE");
            var screen = (Point2d)Application.GetSystemVariable("SCREENSIZE");
            width = screen.X / screen.Y * height;
        }
 
        private void GetTextLocation(out Point3d textPosition, out double textHeight)
        {
            GetViewSize(out double viewHeight, out double viewWidth, out Point3d viewCenter);
            textPosition = new Point3d(viewCenter.X - (viewWidth / 2.0) * .70, viewCenter.Y + (viewHeight / 2.0) * .70, 0.0);
            textHeight = viewHeight / 10.0;
        }
 
        private void CreateTransient()
        {
            GetTextLocation(out Point3d pos, out double h);
            _txtEntity.Height = h;
            _txtEntity.Position = pos;
            _txtEntity.TextString = _promptText;
 
            DrawingTransient();
        }
 
        #endregion 
    }
}

Then the CommandClass/Method where the TransientPrompt class is used:

using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(TransintTextPrompt.MyCommands))]
 
namespace TransintTextPrompt
{
    public class MyCommands 
    {
        [CommandMethod("TestTransient")]
        public static void RunCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
            int count = 0;
            using (var transient=new TransientPrompt(dwg, count.ToString()))
            {
                transient.Enable(true);
 
                while(true)
                {
                    var res = ed.GetEntity("\nSelect an ENTITY:");
                    if (res.Status== PromptStatus.OK)
                    {
                        count++;
                        transient.SetPromptText(count.ToString());
                    }
                    else
                    {
                        break;
                    }
                }
 
                transient.Enable(false);
            }
        }
    }
}

The code is fairly simple and self-descriptive. See the video clip showing the code execution effect:





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 set; get; }
 
        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 ed, out 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:



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.