Sunday, January 23, 2011

Creating A "Move" Command With .NET API - Take 2

In a previous post Creating A "Move" Command With .NET API, I demonstrated how to use Editor.Drag() to build a custom "MOVE" command. It was pretty easy. However, as Jeroen Verdonschot pointed out in his comment, "Ortho" mode does not work within Editor.Drag(). This would be a serious drawback if you want the custom "MOVE" command to be really useful.

As matter of fact, in one of my earliest post here I demonstrated a technique of using Transient Graphics with a video clip. One can see it was almost a nearly finished "MOVE" command. So, I figured, I could just take on the "MOVE" command again and handle the "Ortho on/off" situation.

Here is the class "MyMoveCmd" that does the moving work:

using System;
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

namespace MoveWithTG
{
    public class MyMoveCmd
    {
        private Document _dwg;
        private Editor _editor;
        private ObjectIdCollection _selectedIds;
        private Point3d _basePoint;
        private Point3d _currentPoint;
        private Point3d _moveToPoint;
        private List  _tgDrawables;
        private bool _pointMonitored = false;

        public MyMoveCmd(Document dwg)
        {
            _dwg=dwg;
            _editor=_dwg.Editor;
        }

        public void DoMove()
        {
            //Select object and highlight selected
            ObjectIdCollection ids = new ObjectIdCollection();

            using (Transaction tran = _dwg.Database.
                TransactionManager.StartOpenCloseTransaction())
            {
                if (!GetSelectedEntities(tran, out ids))
                {
                    _editor.WriteMessage("\n*Cancel*");
                    ids = new ObjectIdCollection();
                }

            }

            if (ids.Count == 0) return;

            _selectedIds = ids;

            //Calculate base point (lower-left conner of 
            //bounding box that encloses all selected entities)
            _basePoint = GetDefaultBasePoint();

            //current dragging point
            _currentPoint = _basePoint;

            Transaction trans = _dwg.Database.
                TransactionManager.StartTransaction();

            CreateTransientGraphics(trans);

            try
            {
                _editor.PointMonitor +=
                    new PointMonitorEventHandler(Editor_PointMonitor);
                _pointMonitored = true;

                while (true)
                {
                    PromptPointOptions opt =
                        new PromptPointOptions("\nSelect point to move to: ");
                    opt.UseBasePoint = true;
                    opt.BasePoint = _basePoint;
                    opt.Keywords.Add("Base point");
                    opt.AppendKeywordsToMessage = true;
                    PromptPointResult res = _editor.GetPoint(opt);
                    if (res.Status == PromptStatus.OK)
                    {
                        //Get "Move To" point
                        if (IsOrthModeOn())
                        {
                            _moveToPoint = GetOrthoPoint(res.Value);
                        }
                        else
                        {
                            _moveToPoint = res.Value;
                        }

                        MoveEntities(trans);
                        _editor.WriteMessage("\n{0} moved", _selectedIds.Count);
                        break;
                    }
                    else if (res.Status == PromptStatus.Keyword)
                    {
                        //If user choose to pick BasePoint,
                        //Stop habdling PointMonitor
                        ClearTransientGraphics();

                        _editor.PointMonitor -=
                            new PointMonitorEventHandler(Editor_PointMonitor);
                        _pointMonitored = false;

                        Point3d p;
                        if (!PickBasePoint(out p))
                        {
                            _editor.WriteMessage("\n*Cancel*");
                            break;
                        }
                        else
                        {
                            //Reset base point and current dragging point
                            _basePoint = p;
                            _currentPoint = _basePoint;

                            //Re-create transient graphics
                            CreateTransientGraphics(trans);

                            _editor.PointMonitor +=
                                new PointMonitorEventHandler(Editor_PointMonitor);
                            _pointMonitored = true;
                        }
                    }
                    else
                    {
                        _editor.WriteMessage("\n*Cancel*");
                        break;
                    }
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearHighlight(trans);
                ClearTransientGraphics();

                if (_pointMonitored)
                {
                    _editor.PointMonitor -=
                        new PointMonitorEventHandler(Editor_PointMonitor);
                }

                trans.Commit();
                trans.Dispose();
            }

        }

        #region private metods

        private bool GetSelectedEntities(Transaction tran, out ObjectIdCollection ids)
        {
            ids = new ObjectIdCollection();

            PromptSelectionResult res = _editor.SelectImplied();
            if (res.Status == PromptStatus.OK)
            {
                foreach (ObjectId id in res.Value.GetObjectIds())
                {
                    ids.Add(id);
                    HighlightEntity(tran, id, true);
                }
            }
            else
            {
                while (true)
                {
                    string msg=ids.Count>0?ids.Count + " selected. Pick entity: ":"Pick entity: ";
                    PromptEntityOptions opt = new PromptEntityOptions("\n" + msg);
                    opt.AllowNone = true;
                    PromptEntityResult entRes = _editor.GetEntity(opt);
                    if (entRes.Status == PromptStatus.OK)
                    {
                        bool exists = false;
                        foreach (ObjectId id in ids)
                        {
                            if (id == entRes.ObjectId)
                            {
                                exists = true;
                                break;
                            }
                        }

                        if (!exists)
                        {
                            ids.Add(entRes.ObjectId);
                            HighlightEntity(tran, entRes.ObjectId,true);
                        }
                    }
                    else if (entRes.Status == PromptStatus.None)
                    {
                        break;
                    }
                    else
                    {
                        return false;
                    }
                }  
            }

            return true;
        }

        private void HighlightEntity(Transaction tran, ObjectId id, bool highlight)
        {
            Entity ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);
            if (highlight)
                ent.Highlight();
            else
                ent.Unhighlight();
        }

        private Point3d GetDefaultBasePoint()
        {
            Extents3d exts = new Extents3d(
                new Point3d(0.0, 0.0, 0.0), new Point3d(0.0, 0.0, 0.0));

            using (Transaction tran =
                _dwg.Database.TransactionManager.StartTransaction())
            {
                for (int i = 0; i < _selectedIds.Count; i++)
                {
                    ObjectId id = _selectedIds[i];
                    Entity ent = (Entity)tran.GetObject(id, OpenMode.ForRead);

                    if (i == 0)
                    {
                        exts = ent.GeometricExtents;
                    }
                    else
                    {
                        Extents3d ext = ent.GeometricExtents;
                        exts.AddExtents(ext);
                    }
                }

                tran.Commit();
            }

            return exts.MinPoint;
        }

        private bool PickBasePoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = new PromptPointOptions("Pick base point: ");
            PromptPointResult res = _editor.GetPoint(opt);
            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool IsOrthModeOn()
        {
            object orth = Autodesk.AutoCAD.ApplicationServices.
                Application.GetSystemVariable("ORTHOMODE");

            return Convert.ToInt32(orth) > 0;
        }

        private Point3d GetOrthoPoint(Point3d pt)
        {
            double x=pt.X;
            double y=pt.Y;

            Vector3d vec = _basePoint.GetVectorTo(pt);
            if (Math.Abs(vec.X)>=Math.Abs(vec.Y))
            {
                y = _basePoint.Y;
            }
            else
            {
                x = _basePoint.X;
            }

            return new Point3d(x, y, 0.0);
        }

        private void ClearHighlight(Transaction trans)
        {
            foreach (ObjectId id in _selectedIds)
            {
                HighlightEntity(trans, id, false);
            }
        }

        private void MoveEntities(Transaction tran)
        {
            Matrix3d mat = Matrix3d.Displacement(_basePoint.GetVectorTo(_moveToPoint));
            foreach (ObjectId id in _selectedIds)
            {
                Entity ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);
                ent.TransformBy(mat);
            }
        }

        #endregion

        #region private method: handling PointMonitor and transient graphics

        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            Point3d pt = e.Context.RawPoint;
            if (IsOrthModeOn())
            {
                pt = GetOrthoPoint(pt);
            }

            UpdateTransientGraphics(pt);

            _currentPoint = pt;
        }

        private void CreateTransientGraphics(Transaction tran)
        {
            _tgDrawables = new List();

            foreach (ObjectId id in _selectedIds)
            {
                Entity ent = (Entity)tran.GetObject(id, OpenMode.ForRead);

                Entity drawable = ent.Clone() as Entity;
                drawable.ColorIndex = 1;
                _tgDrawables.Add(drawable);
            }

            foreach (Drawable d in _tgDrawables)
            {
                TransientManager.CurrentTransientManager.
                    AddTransient(d, TransientDrawingMode.DirectShortTerm, 
                    128, new IntegerCollection());
            }
        }

        private void UpdateTransientGraphics(Point3d moveToPoint)
        {
            Matrix3d mat = Matrix3d.Displacement(_currentPoint.GetVectorTo(moveToPoint));
            foreach (Drawable d in _tgDrawables)
            {
                Entity e = d as Entity;
                e.TransformBy(mat);

                TransientManager.CurrentTransientManager.
                    UpdateTransient(d, new IntegerCollection());
            }
        }

        private void ClearTransientGraphics()
        {
            TransientManager.CurrentTransientManager.
                EraseTransients(TransientDrawingMode.DirectShortTerm, 
                128, new IntegerCollection());

            foreach (Drawable d in _tgDrawables)
            {
                d.Dispose();
            }

            _tgDrawables.Clear();
        }

        #endregion
    }
}

Then the simple command class:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(MoveWithTG.MyCommands))]

namespace MoveWithTG
{
    public class MyCommands
    {
        [CommandMethod("MyMove",CommandFlags.UsePickSet)]
        public static void DoCommand()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            MyMoveCmd move = new MyMoveCmd(dwg);
            move.DoMove();
        }
    }
}

The behaviour of the custom "MOVE" command can be seen here.

If you want the selected objects not only move horizontally or vertically when "Ortho" mode is on, but also can be moved along a specific angle, one of my other posts here would be of help.

Note: due to the blog's format, The Generic type cannot be shown in the post correctly. The "List" shown in red should be List(Of Drawable) in VB.NET. It looks like I have to post code in VB.NET if my code uses Generic. Too bad.

Note again: due to the blog's format, the code using Generic in C# cannot be shown here correctly for its angled bracket, I now post the VB.NET code here. In my future post, if there is Generic involved, I'll try post code in VB.NET.

Here is the class "MyMoveCmd" in VB.NET:

Imports System.Collections.Generic

Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.GraphicsInterface


Public Class MyMoveCmd

    Private _dwg As Document
    Private _editor As Editor
    Private _selectedIds As ObjectIdCollection
    Private _basePoint As Point3d
    Private _currentPoint As Point3d
    Private _moveToPoint As Point3d
    Private _tgDrawables As List(Of Drawable)
    Private _pointMonitored As Boolean = False

    Public Sub New(ByVal dwg As Document)
        _dwg = dwg
        _editor = _dwg.Editor
    End Sub

    Public Sub DoMove()
        'Select object and highlight selected
        Dim ids As New ObjectIdCollection()

        Using tran As Transaction = _
            _dwg.Database.TransactionManager.StartOpenCloseTransaction()

            If Not GetSelectedEntities(tran, ids) Then
                _editor.WriteMessage(vbLf & "*Cancel*")
                ids = New ObjectIdCollection()

            End If
        End Using

        If ids.Count = 0 Then
            Return
        End If

        _selectedIds = ids

        'Calculate base point (lower-left conner of 
        'bounding box that encloses all selected entities)
        _basePoint = GetDefaultBasePoint()

        'current dragging point
        _currentPoint = _basePoint

        Dim trans As Transaction = _
            _dwg.Database.TransactionManager.StartTransaction()

        CreateTransientGraphics(trans)

        Try

            AddHandler _editor.PointMonitor, AddressOf Editor_PointMonitor
            _pointMonitored = True

            While True
                Dim opt As New PromptPointOptions(vbLf & "Select point to move to: ")
                opt.UseBasePoint = True
                opt.BasePoint = _basePoint
                opt.Keywords.Add("Base point")
                opt.AppendKeywordsToMessage = True
                Dim res As PromptPointResult = _editor.GetPoint(opt)
                If res.Status = PromptStatus.OK Then
                    'Get "Move To" point
                    If IsOrthModeOn() Then
                        _moveToPoint = GetOrthoPoint(res.Value)
                    Else
                        _moveToPoint = res.Value
                    End If

                    MoveEntities(trans)
                    _editor.WriteMessage(vbLf & "{0} moved", _selectedIds.Count)
                    Exit While
                ElseIf res.Status = PromptStatus.Keyword Then
                    'If user choose to pick BasePoint,
                    'Stop habdling PointMonitor
                    ClearTransientGraphics()

                    RemoveHandler _editor.PointMonitor, AddressOf Editor_PointMonitor
                    _pointMonitored = False

                    Dim p As Point3d
                    If Not PickBasePoint(p) Then
                        _editor.WriteMessage(vbLf & "*Cancel*")
                        Exit While
                    Else
                        'Reset base point and current dragging point
                        _basePoint = p
                        _currentPoint = _basePoint

                        'Re-create transient graphics
                        CreateTransientGraphics(trans)

                        AddHandler _editor.PointMonitor, AddressOf Editor_PointMonitor
                        _pointMonitored = True
                    End If
                Else
                    _editor.WriteMessage(vbLf & "*Cancel*")
                    Exit While
                End If
            End While
        Catch
            Throw
        Finally
            ClearHighlight(trans)
            ClearTransientGraphics()

            If _pointMonitored Then
                RemoveHandler _editor.PointMonitor, AddressOf Editor_PointMonitor
            End If

            trans.Commit()
            trans.Dispose()
        End Try

    End Sub

#Region "private metods"

    Private Function GetSelectedEntities(ByVal tran As Transaction, _
                                         ByRef ids As ObjectIdCollection) As Boolean

        ids = New ObjectIdCollection()

        Dim res As PromptSelectionResult = _editor.SelectImplied()
        If res.Status = PromptStatus.OK Then
            For Each id As ObjectId In res.Value.GetObjectIds()
                ids.Add(id)
                HighlightEntity(tran, id, True)
            Next
        Else
            While True
                Dim msg As String = If(ids.Count > 0, _
                                       Convert.ToString(ids.Count) & " selected. Pick entity: ", _
                                       "Pick entity: ")
                Dim opt As New PromptEntityOptions(vbLf & msg)
                opt.AllowNone = True
                Dim entRes As PromptEntityResult = _editor.GetEntity(opt)
                If entRes.Status = PromptStatus.OK Then
                    Dim exists As Boolean = False
                    For Each id As ObjectId In ids
                        If id = entRes.ObjectId Then
                            exists = True
                            Exit For
                        End If
                    Next

                    If Not exists Then
                        ids.Add(entRes.ObjectId)
                        HighlightEntity(tran, entRes.ObjectId, True)
                    End If
                ElseIf entRes.Status = PromptStatus.None Then
                    Exit While
                Else
                    Return False
                End If
            End While
        End If

        Return True
    End Function

    Private Sub HighlightEntity(ByVal tran As Transaction, _
                                ByVal id As ObjectId, ByVal highlight As Boolean)

        Dim ent As Entity = DirectCast(tran.GetObject(id, OpenMode.ForWrite), Entity)
        If highlight Then
            ent.Highlight()
        Else
            ent.Unhighlight()
        End If

    End Sub

    Private Function GetDefaultBasePoint() As Point3d

        Dim exts As New Extents3d(New Point3d(0.0, 0.0, 0.0), New Point3d(0.0, 0.0, 0.0))

        Using tran As Transaction = _dwg.Database.TransactionManager.StartTransaction()
            For i As Integer = 0 To _selectedIds.Count - 1
                Dim id As ObjectId = _selectedIds(i)
                Dim ent As Entity = DirectCast(tran.GetObject(id, OpenMode.ForRead), Entity)

                If i = 0 Then
                    exts = ent.GeometricExtents
                Else
                    Dim ext As Extents3d = ent.GeometricExtents
                    exts.AddExtents(ext)
                End If
            Next

            tran.Commit()
        End Using

        Return exts.MinPoint

    End Function

    Private Function PickBasePoint(ByRef pt As Point3d) As Boolean

        pt = New Point3d()

        Dim opt As New PromptPointOptions("Pick base point: ")
        Dim res As PromptPointResult = _editor.GetPoint(opt)
        If res.Status = PromptStatus.OK Then
            pt = res.Value
            Return True
        Else
            Return False
        End If

    End Function

    Private Function IsOrthModeOn() As Boolean

        Dim orth As Object = Autodesk.AutoCAD.ApplicationServices. _
            Application.GetSystemVariable("ORTHOMODE")

        Return Convert.ToInt32(orth) > 0

    End Function

    Private Function GetOrthoPoint(ByVal pt As Point3d) As Point3d

        Dim x As Double = pt.X
        Dim y As Double = pt.Y

        Dim vec As Vector3d = _basePoint.GetVectorTo(pt)
        If Math.Abs(vec.X) >= Math.Abs(vec.Y) Then
            y = _basePoint.Y
        Else
            x = _basePoint.X
        End If

        Return New Point3d(x, y, 0.0)

    End Function

    Private Sub ClearHighlight(ByVal trans As Transaction)
        For Each id As ObjectId In _selectedIds
            HighlightEntity(trans, id, False)
        Next
    End Sub

    Private Sub MoveEntities(ByVal tran As Transaction)

        Dim mat As Matrix3d = Matrix3d.Displacement(_basePoint.GetVectorTo(_moveToPoint))
        For Each id As ObjectId In _selectedIds
            Dim ent As Entity = DirectCast(tran.GetObject(id, OpenMode.ForWrite), Entity)
            ent.TransformBy(mat)
        Next

    End Sub

#End Region

#Region "private method: handling PointMonitor and transient graphics"

    Private Sub Editor_PointMonitor(ByVal sender As Object, ByVal e As PointMonitorEventArgs)
        Dim pt As Point3d = e.Context.RawPoint
        If IsOrthModeOn() Then
            pt = GetOrthoPoint(pt)
        End If

        UpdateTransientGraphics(pt)

        _currentPoint = pt
    End Sub

    Private Sub CreateTransientGraphics(ByVal tran As Transaction)

        _tgDrawables = New List(Of Drawable)()

        For Each id As ObjectId In _selectedIds
            Dim ent As Entity = DirectCast(tran.GetObject(id, OpenMode.ForRead), Entity)

            Dim drawable As Entity = TryCast(ent.Clone(), Entity)
            drawable.ColorIndex = 1
            _tgDrawables.Add(drawable)
        Next

        For Each d As Drawable In _tgDrawables
            TransientManager.CurrentTransientManager.AddTransient( _
                d, TransientDrawingMode.DirectShortTerm, _
                128, New IntegerCollection())
        Next
    End Sub

    Private Sub UpdateTransientGraphics(ByVal moveToPoint As Point3d)

        Dim mat As Matrix3d = Matrix3d.Displacement( _
            _currentPoint.GetVectorTo(moveToPoint))

        For Each d As Drawable In _tgDrawables
            Dim e As Entity = TryCast(d, Entity)
            e.TransformBy(mat)

            TransientManager.CurrentTransientManager.UpdateTransient( _
                d, New IntegerCollection())
        Next

    End Sub

    Private Sub ClearTransientGraphics()

        TransientManager.CurrentTransientManager.EraseTransients( _
            TransientDrawingMode.DirectShortTerm, _
            128, New IntegerCollection())

        For Each d As Drawable In _tgDrawables
            d.Dispose()
        Next

        _tgDrawables.Clear()

    End Sub

#End Region
End Class

Here is the command class:

Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.DatabaseServices

 

Public Class MyCommands

     _
    Public Shared Sub DoCommand()
        Dim dwg As Document = Autodesk.AutoCAD.ApplicationServices. _
            Application.DocumentManager.MdiActiveDocument

        Dim move As New MyMoveCmd(dwg)
        move.DoMove()
    End Sub

End Class

Friday, January 21, 2011

Mimicking AutoCAD's "AREA" Command With .NET Code 3 - Using Overrule

After posting the second article on this topic, the approach I used kept bothering me: repeatedly adding/removing a polyline/hatch to/from working database. There must be some better way to do it. Something then suddenly rang a bell in my head: Overrule. And I remembered Kean Walmsley had a post may be of help.

So, I modified the code to use DrawableOverrule to do this job (obviously, the code only works with AutoCAD 2010 and later). I also modified the code to allow pick a background color before picking the first point.

First, I removed the code that creates Hatch, so the MyAreaCmd class was restored back to almost the same as the first version when there is not background rendered when user does the picking. Only a polygon is drawn dynamically.

Then, I created a custom DrawableOverrule, which filters out the polygon as the overrule's target and renders the background.

Finally, I added two 2 lines of code to MyAreaCmd to enable the overrule after user picked first 2 points and disable the overrule when the picking is done. Following is the whole set of code (I have renamed the class to "MyNewAraeCmd").

Class "MyNewAreaCmd":

using System;
using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.DatabaseServices;

namespace AreaCommand
{
    public class MyNewAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;

        private List _points;
        private bool _pickDone;

        private int _color = 1;

        public MyNewAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline = null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X, pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                //Enable custom Overrule
                MyPolylineOverrule.Instance.StartOverrule(_points, _color);

                //Handling mouse cursor moving during picking
                _editor.PointMonitor +=
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    Calculate();
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -=
                    new PointMonitorEventHandler(_editor_PointMonitor);

                //Disbale custom Overrule
                MyPolylineOverrule.Instance.EndOverrule();
            }

            return _pickDone;
        }

        #region private methods

        private void Calculate()
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline p =
                new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count);
            for (int i = 0; i < _points.Count; i++)
                p.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);

            p.Closed = true;

            _area = p.Area;
            _perimeter = p.Length;

            p.Dispose();
        }

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            while (true)
            {
                PromptPointOptions opt =
                    new PromptPointOptions("\nPick first corner: ");

                opt.Keywords.Add("Background");
                opt.AppendKeywordsToMessage = true;

                PromptPointResult res = _editor.GetPoint(opt);

                if (res.Status == PromptStatus.OK)
                {
                    pt = res.Value;
                    return true;
                }
                else if (res.Status == PromptStatus.Keyword)
                {
                    PromptIntegerOptions intOpt = new PromptIntegerOptions("\nEnter color number (1 to 7): ");
                    intOpt.AllowNegative = false;
                    intOpt.AllowZero = false;
                    intOpt.AllowArbitraryInput = false;
                    intOpt.UseDefaultValue = true;
                    intOpt.DefaultValue = 1;

                    PromptIntegerResult intRes = _editor.GetInteger(intOpt);

                    if (intRes.Status == PromptStatus.OK)
                    {
                        _color = intRes.Value;
                    }
                }
                else
                {
                    return false;
                }
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt =
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt =
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2)
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.Keywords.Default = "Total";
                opt.AppendKeywordsToMessage = true;
                opt.AllowArbitraryInput = false;
            }

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X, res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                        _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null )
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());

                _pline.Dispose();
                _pline = null;
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            //Get mouse cursor location
            Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

            _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

            for (int i = 0; i < _points.Count; i++)
            {
                _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
            }

            _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
            _pline.Closed = true;

            TransientManager.CurrentTransientManager.AddTransient(
                _pline, TransientDrawingMode.DirectTopmost,
                128, new IntegerCollection());
        }

        #endregion
    }
}
Class "MyPolylineOverrule":
using System;
using System.Collections.Generic;

using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;


namespace AreaCommand
{
    public class MyPolylineOverrule : DrawableOverrule
    {
        private static MyPolylineOverrule _instance = null;
        private bool _existingOverrulling;
        private int _color = 1;

        private List _points = null;

        public static MyPolylineOverrule Instance
        {
            get
            {
                if (_instance == null) _instance = new MyPolylineOverrule();
                return _instance;
            }
        }

        public int Color
        {
            set { _color = value; }
            get { return _color; }
        }

        public void StartOverrule(List points)
        {
            _points = points;

            _existingOverrulling = Overruling;

            //Add the custom overrule
            AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this, false);

            //Use custom filter, implemented in IsApplicable() method
            SetCustomFilter();

            //Make sure Overrule is enabled
            Overruling = true;
        }

        public void StartOverrule(List points, int color)
        {
            _color = color;

            _points = points;

            _existingOverrulling = Overruling;

            //Add the custom overrule
            AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this, false);

            //Use custom filter, implemented in IsApplicable() method
            SetCustomFilter();

            //Make sure Overrule is enabled
            Overruling = true;
        }

        public void EndOverrule()
        {
            //Remove this custom Overrule
            RemoveOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this);

            //restore to previous Overrule status (enabled or disabled)
            Overruling = _existingOverrulling;
        }

        public override bool WorldDraw(Drawable drawable, WorldDraw wd)
        {
            Point3dCollection pts = new Point3dCollection();
            for (int i = 0; i < _points.Count; i++)
            {
                pts.Add(new Point3d(_points[i].X, _points[i].Y, 0.0));
            }

            wd.SubEntityTraits.FillType = FillType.FillAlways;
            wd.SubEntityTraits.Color = Convert.ToInt16(_color);

            wd.Geometry.Polygon(pts);

            return base.WorldDraw(drawable, wd);
        }

        public override bool IsApplicable(
            Autodesk.AutoCAD.Runtime.RXObject overruledSubject)
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline pl = overruledSubject 
                as Autodesk.AutoCAD.DatabaseServices.Polyline;

            if (pl != null)
            {
                //Only apply this overrule to the polyline
                //that has not been added to working database
                //e.g. created for the Transient Graphics
                if (pl.Database == null)
                    return true;
                else
                    return false;
            }
            else
            {
                return false;
            }
        }
    }
}
Command class "MyCommands":
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;

[assembly: CommandClass(typeof(AreaCommand.MyCommands))]

namespace AreaCommand
{
    public class MyCommands 
    {
        [CommandMethod("MyNewArea")]
        public static void GetNewArea()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;

            MyNewAreaCmd cmd = new MyNewAreaCmd(dwg);

            if (cmd.GetArea())
            {
                ed.WriteMessage("\nArea = {0}", cmd.Area);
                ed.WriteMessage("\nPerimeter = {0}", cmd.Perimeter);
            }
            else
            {
                ed.WriteMessage("\n*Cancelled*");
            }
        }
    }
}

See this video clip for its action.

One will notice that the background showing is different from AutoCAD built-in "AREA" command: it only covers the picked points, not the point to be picked when user is moving the mouse. In my opinion, my approach is better: the background-covered area is the real area the command would produce if user hit Enter to complete the command, which should not include the area user has not picked.

Thursday, January 20, 2011

Mimicking AutoCAD's "AREA" Command With .NET Code 2 - Updated

(Note: latest bug fix update has been appended to the end of this article)

In the previous post leaves out a noticeable feature: showing a distinguishable background color of the selected area. It is due to difficulty for me to decide what entity that I can use as Transient Graphics that can show an region with solid color filled. It is natural to look at Hatch object.

However, due to the fact that when appending loop entities (closed Curves) the method Hatch.AppendLoop() takes a collection of ObjectId as input, the polyline used as the Hatch's loop must be added into curretn working database, so that in AppedLoop() method AutoCAD can find the closed curve as loop from the passed-in ObjectId. In the meantime, the Hatch object itself must also be added to the working database before loop can be appended and the hatch can be evaluated (Hatch.EvaluateHatch() being called after loops being appended).

Since the area need to be drawn dynamically and repeatedly in the PointMonitor event handler with every tiny mouse cursor move, I thought it would be bad thing to do to start atransaction, add new polyline to database, add hatch to database, draw them as Transient Graphics and then erased them whenever PointMonitor even fires. However, I looked hard into the AutoCAD .NET API classes and could not find anything that I can use to draw solid background color in the polygon, other than Hatch. So, I decided to give it a try. Here is the updated code MyAreaCmd class (I highlighted changes in red):

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.DatabaseServices;

namespace AreaCommand
{
    public class MyAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;
        private Autodesk.AutoCAD.DatabaseServices.Hatch _hatch = null;

        private List _points;
        private bool _pickDone;

        private Autodesk.AutoCAD.DatabaseServices.Transaction _tran = null;
        private BlockTableRecord _model = null;

        public MyAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline=null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X,pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                _tran = _dwg.Database.TransactionManager.StartTransaction();

                BlockTable bt = (BlockTable)_tran.GetObject(_dwg.Database.BlockTableId, OpenMode.ForRead);
                _model = (BlockTableRecord)_tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

                //Handling mouse cursor moving during picking
                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    _area = _pline.Area;
                    _perimeter = _pline.Length;
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                _tran.Abort();
                _tran.Dispose();

                _model = null;
            }

            return _pickDone;
        }

        #region private methods

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick first corner: ");
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2)
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.AppendKeywordsToMessage = true;
            }

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X,res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                        _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null || _hatch!=null)
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost, 
                    128, new IntegerCollection());

                if (_pline != null)
                {
                    if (!_pline.IsErased) _pline.Erase();
                    _pline.Dispose();
                    _pline = null;
                }

                if (_hatch != null)
                {
                    if (!_hatch.IsErased) _hatch.Erase();
                    _hatch.Dispose();
                    _hatch = null;
                }
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            //Draw polyline
            Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

            _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

            for (int i = 0; i < _points.Count; i++)
            {
                _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
            }

            _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
            _pline.Closed = true;

            _model.AppendEntity(_pline);
            _tran.AddNewlyCreatedDBObject(_pline, true);

            TransientManager.CurrentTransientManager.AddTransient(
                _pline, TransientDrawingMode.DirectTopmost, 
                128, new IntegerCollection());

            _hatch = new Hatch();
            _hatch.ColorIndex = 1; //set color to RED

            _model.AppendEntity(_hatch);
            _tran.AddNewlyCreatedDBObject(_hatch, true);

            ObjectIdCollection loops = new ObjectIdCollection();
            loops.Add(_pline.ObjectId);

            _hatch.SetHatchPattern(HatchPatternType.PreDefined, "Solid");
            _hatch.AppendLoop(HatchLoopTypes.Outermost, loops);
            _hatch.EvaluateHatch(true);

            TransientManager.CurrentTransientManager.AddTransient(
                _hatch, TransientDrawingMode.DirectTopmost,
                128, new IntegerCollection());
        }

        #endregion
    }
}
As one can see, I started a transaction after user picks second points, and the transaction always gets aborted and disposed in the "finally{...} block. Then the PointMonitor event handler, the polyline and hatch entities get re-created and used as Transient Graphics whenever PointMonitor event fires.

The visual effect looks roughly the same as the real "AREA" command in AutoCAD2010/2011, but flickering is likely noticeable, due to the fact that the 2 entities being added and erased from working database within the transaction, which are quite expensive operation without doubt. See video click here for its action. Nonetheless, it works closely like the AutoCAD built-in "AREA" command, and since picking points with this command is all AutoCAD does at the moment, being expensive process or not probably wouldn't concerns user to much, as long as the flickering is not too heavy.

Bug Fix Update

It turned out that repeatedly adding/erasing the polyline and the hatch to/from working database in the PointMonitor event handler is problematic, as I suspected: if user run this code to measure an existing entity's area, say a rectangle's area, user would definitely uses OSnap so that the picked point snaps to the intended location. The snapping causes the code in PointMonitor handler crash.

I cannot find technical resources to explain what effect OSnap would have to the PointMonitor event, so the solution I managed to find is to add a "try...catch{}" to ignore the deadly exception caused by OSnap in the PointMonitor event handler. Of course this may in turn lead other issue, such as the polyline/hatch drawn in the PointMonitor event handler not being added to working database, thus not being shown as Transient Graphics correctly. However, since the the polyline and hatch are redrawn with every tiny mouse move, merely used as visual hint to user, this wouldn't be a big issue, I figured.

I made a small improvement to the point picking prompt: making [Total] keyword as default keyword input after the third pick.

I have also fixed another bug: the area and perimeter are not calculated based on the actual points picked, not from the polygon drawn in the PointMonitor event handler.

Here is the updated code with updated line in blue:

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.DatabaseServices;

namespace AreaCommand
{
    public class MyAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;
        private Autodesk.AutoCAD.DatabaseServices.Hatch _hatch = null;

        private List _points;
        private bool _pickDone;

        private Autodesk.AutoCAD.DatabaseServices.Transaction _tran = null;
        private BlockTableRecord _model = null;

        public MyAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline=null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X,pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                _tran = _dwg.Database.TransactionManager.StartTransaction();

                BlockTable bt = (BlockTable)_tran.GetObject(_dwg.Database.BlockTableId, OpenMode.ForRead);
                _model = (BlockTableRecord)_tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

                //Handling mouse cursor moving during picking
                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    //_area = _pline.Area;
                    //_perimeter = _pline.Length;
                    Calculate();
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                _tran.Abort();
                _tran.Dispose();

                _model = null;
            }

            return _pickDone;
        }

        #region private methods

        private void Calculate()
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline p = 
                new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count);
            for (int i = 0; i < _points.Count; i++)
                p.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);

            p.Closed = true;

            _area = p.Area;
            _perimeter = p.Length;

            p.Dispose();
        }

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick first corner: ");
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2)
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.Keywords.Default = "Total";
                opt.AppendKeywordsToMessage = true;
                opt.AllowArbitraryInput = false;
            }

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X,res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                        _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null || _hatch!=null)
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost, 
                    128, new IntegerCollection());

                if (_pline != null)
                {
                    if (!_pline.IsErased)
                    {
                        if (_pline.Database!=null) _pline.Erase();
                    }
                    _pline.Dispose();
                    _pline = null;
                }

                if (_hatch != null)
                {
                    if (!_hatch.IsErased)
                    {
                        if (_hatch.Database != null) _hatch.Erase();
                    }
                    _hatch.Dispose();
                    _hatch = null;
                }
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            try
            {
                //Draw polyline
                Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

                _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

                for (int i = 0; i < _points.Count; i++)
                {
                    _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
                }

                _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
                _pline.Closed = true;

                _model.AppendEntity(_pline);
                _tran.AddNewlyCreatedDBObject(_pline, true);

                TransientManager.CurrentTransientManager.AddTransient(
                    _pline, TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());

                _hatch = new Hatch();
                _hatch.ColorIndex = 1;

                _model.AppendEntity(_hatch);
                _tran.AddNewlyCreatedDBObject(_hatch, true);

                ObjectIdCollection loops = new ObjectIdCollection();
                loops.Add(_pline.ObjectId);

                _hatch.SetHatchPattern(HatchPatternType.PreDefined, "Solid");
                _hatch.AppendLoop(HatchLoopTypes.Outermost, loops);
                _hatch.EvaluateHatch(true);

                TransientManager.CurrentTransientManager.AddTransient(
                    _hatch, TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());
            }
            catch 
            { 
                //Ignore the possible exception caused by OSnap
            }
        }

        #endregion
    }
}

Tuesday, January 18, 2011

Mimicking AutoCAD's "AREA" Command With .NET Code

In response to the question asked to me on how to do something similar to AutoCAD's "AREA" command, I spent most my lunch break time and got this simple code that mimicking the AREA" command. Well, just a simple version of "AREA" command.

First, a class does the work - MyAreaCmd:

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

namespace AreaCommand
{
    public class MyAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;
        private List _points;
        private bool _pickDone;

        public MyAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline=null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X,pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                //Handling mouse cursor moving during picking
                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    _area = _pline.Area;
                    _perimeter = _pline.Length;
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);
            }

            return _pickDone;
        }

        #region private methods

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick first corner: ");
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2) 
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.AppendKeywordsToMessage = true;
            }
            
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X,res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                       _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null)
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost, 
                    128, new IntegerCollection());

                _pline.Dispose();
                _pline = null;
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            //Draw polyline
            Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

            _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

            for (int i = 0; i < _points.Count; i++)
            {
                _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
            }

            _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
            _pline.Closed = true;

            TransientManager.CurrentTransientManager.AddTransient(
                _pline, TransientDrawingMode.DirectTopmost, 
                128, new IntegerCollection());
        }

        #endregion
    }
}
Then, I use MyAreaCmd class in a command method:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;

[assembly: CommandClass(typeof(AreaCommand.MyCommands))]

namespace AreaCommand
{
    public class MyCommands 
    {
        [CommandMethod("MyArea")]
        public static void GetArea()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;

            MyAreaCmd cmd = new MyAreaCmd(dwg);

            if (cmd.GetArea())
            {
                ed.WriteMessage("\nArea = {0}", cmd.Area);
                ed.WriteMessage("\nPerimeter = {0}", cmd.Perimeter);
            }
            else
            {
                ed.WriteMessage("\n*Cancelled*");
            }
        }
    }
}

See this video clip for the action.

Pretty simple, eh? Here are notes:

1. I did not try to give extra options during the picking as the real "Area" command does, such as [Arc/Length...]. if someone is interested in to expend the functionality, fell free to try.

2. The real "Area" command fills the picked polygon with color so that the area enclosed in the polygon is visually stand-out. I did not successfully mimic that. By "did not successfully", I mean I tried to create a hatch as Transient Graphics in the PointMonitor event handler. However, to my surprise, the code to append hatch's loop would fail unless the newly instantiated Hatch object and the loop object (closed Polyline) were appended into the drawing database in a transaction (hope someone would confirm this or confirm that I was wrong on this). Since the polygon has to be cleared and redraw (as Transient Graphics) repeatedly in the PointMonitor event handler, I felt repeatedly adding/erasing the Hatch in/from database within a transaction would be horrible thing to do. So I gave it up (for now).

Monday, January 17, 2011

Picking Points With Visual Aid

Again, this article was prompted with the discussion in the same thread from Autodesk's .NET discussion here.

As we all know, using PromptPointOptions with Editor.GetPoint(), one can use its properties UseBasePoint and BasePoint tp let AutoCAD show a rubber band line while picking points.

Some times, there are many points to pick, and user may want to have certain visual hint showing the points he/she has picked during the picking process. It may also be helpful to show a trace line follow the order of the points being picked.

Here is the code I do it, in a class called "PickPoints":

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

namespace PickPointWithVisualSpot
{
    public class PickPoints
    {
        private Document _dwg;
        private Editor _editor;
        private List _points = null;
        private List _circles = null;

        private bool _drawPline = false;
        private Polyline _pline = null;

        private int _color = 1;
        private double _radius = 0.1;

        public PickPoints(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public Point3d[] PickedPoints
        {
            get
            {
                if (_points == null)
                    return null;
                else
                    return _points.ToArray();
            }
        }

        public void DoPick(bool drawTraceLine)
        {
            try
            {
                //Pick first point
                Point3d pt;
                if (!PickFirstPoint(out pt)) return;

                _drawPline = drawTraceLine;

                _points = new List();
                _circles = new List();

                _points.Add(pt);

                //Crate visual at the point
                CreateCircle(pt);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }
            }
            catch { }
            finally
            {
                //Clear Transient graphics
                if (_circle != null)
                {
                    if (_circles.Count > 0)
                    {
                        foreach (Circle c in _circles)
                        {
                            TransientManager.CurrentTransientManager.
                                EraseTransient(c, new IntegerCollection());

                            c.Dispose();
                        }
                    }

                    _circles.Clear();
                    _circles = null;
                }

                ClearTraceLine();
            }
        }

        private bool PickFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick start point: ");
            PromptPointResult res = _editor.GetPoint(opt);
            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt=
                new PromptPointOptions("\nPick next point: ");
            opt.UseBasePoint=true;
            opt.BasePoint=_points[_points.Count-1];
            if (_points.Count>2)
            {
                opt.Keywords.Add("Close");
                opt.AppendKeywordsToMessage=true;
            }

            PromptPointResult res=_editor.GetPoint(opt);
            if (res.Status==PromptStatus.OK)
            {
                _points.Add(res.Value);

                CreateCircle(res.Value);

                if (_points.Count > 1 && _drawPline )
                {
                    DrawTraceLine();
                }

                return true;
            }
            else if (res.Status==PromptStatus.Keyword)
            {
                return false;
            }
            else
            {
                return false;
            }
        }

        private void CreateCircle(Point3d pt)
        {
            Circle c = new Circle();
            c.Center = pt;
            c.Radius = _radius;
            c.ColorIndex = _color;
            c.Highlight();

            TransientManager.CurrentTransientManager.AddTransient(c, 
                TransientDrawingMode.Highlight,128, new IntegerCollection());

            _circles.Add(c);
        }

        private void DrawTraceLine()
        {
            ClearTraceLine();

            _pline = new Polyline(_points.Count);

            for (int i = 0; i < _points.Count; i++)
            {
                Point2d p = new Point2d(_points[i].X, _points[i].Y);
                _pline.AddVertexAt(i, p, 0.0, 0.0, 0.0);
            }

            _pline.ColorIndex = _color;

            TransientManager.CurrentTransientManager.AddTransient(_pline,
                TransientDrawingMode.Highlight, 128, new IntegerCollection());
        }

        private void ClearTraceLine()
        {
            if (_pline != null)
            {
                TransientManager.CurrentTransientManager.
                        EraseTransient(_pline, new IntegerCollection());

                _pline.Dispose();
                _pline = null;
            }
        }
    }
}
Then the use of the class in custom command. There are 2 commands: one shows trace line, while the other does not:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

[assembly: CommandClass(typeof(PickPointWithVisualSpot.MyCommand))]

namespace PickPointWithVisualSpot
{
    public class MyCommand 
    {
        [CommandMethod("MyPick1", CommandFlags.Session)]
        public static void RunThisMethod1()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;

            PickPoints pick = new PickPoints(dwg);

            pick.DoPick(false);
            if (pick.PickedPoints != null)
            {
                dwg.Editor.WriteMessage("\nMyCommand executed: {0} points picked.", pick.PickedPoints.Length);
            }
            else
            {
                dwg.Editor.WriteMessage("\n*Cancelled*");
            }
        }

        [CommandMethod("MyPick2", CommandFlags.Session)]
        public static void RunThisMethod2()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;

            PickPoints pick = new PickPoints(dwg);

            pick.DoPick(true);
            if (pick.PickedPoints != null)
            {
                dwg.Editor.WriteMessage("\nMyCommand executed: {0} points picked.", pick.PickedPoints.Length);
            }
            else
            {
                dwg.Editor.WriteMessage("\n*Cancelled*");
            }
        }
    }
}

See this video clip for the code's behaviour.

Note: thanks to Irvin who prompted a bug in the "PickPoints" class' DoPick() method in the try...finally{...} block. I have corrected in red.

Sunday, January 16, 2011

Picking Entity Like Picking Point With Rubber Band Line

This article is inspired by this question posted by Irvin in Autodesk .NET discussion forum here.

In a occassion when picking a series of entities, the user may not only wants to have the picked entity highlighted, but also wants a rubber band line drawn from the previously picked point (or point derived from the previously picked entity) to the moving mouse cursor before picking next entity.

As we know, using PromptPointOption in conjunction with Editor.GetPoint(), AutoCAD draws a rubber band line, if PromptPointOptions.UseBasePoint is set true and a base point is supplied. However, to pick an entity, PromptEntityOptions and Editor.GetEntity() do not provide such option as "UseBasePoint".

Here is my way to solve this issue. After the first entity is picked, I handle Editor.PointMonitor event, so that when user moves mouse to pick next entity, a Transient Graphics (Line) is drawn in the event handler. The code is fairly simple, as following.

First, as I usually do, I created a class to encapsulate the wanted functionality: PickEntities.

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

namespace PickMultiplePoints
{
    public class PickEntities
    {
        private Document _dwg;
        private Editor _editor;

        private List _points = null;
        private List _entities = null;

        private Line _rubberLine = null;

        private Point3d _basePoint;
        private Point3d _currentPoint;

        public PickEntities(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        #region public properties 

        public List PickedPoints
        {
            get { return _points; }
        }

        public List PickedEntities
        {
            get { return _entities; }
        }

        #endregion

        public void DoPick(bool unhighlightAtEnd)
        {
            //Pick first entity
            ObjectId id;
            Point3d pt;
            if (!PickFirstEntity(out id, out pt)) return;

            _rubberLine = null;
            _points = new List();
            _entities = new List();

            _points.Add(pt);
            _entities.Add(id);

            SetHighlight(id, true);

            //Start drag rubber band line
            while (true)
            {
                if (!PickNextEntity()) break;
            }

            //Unhighlight all picked entities
            if (unhighlightAtEnd)
            {
                foreach (ObjectId entId in _entities)
                    SetHighlight(entId, false);
            }
        }

        #region private methods

        private bool PickFirstEntity(
            out ObjectId id, out Point3d pt)
        {
            pt = new Point3d();
            id = ObjectId.Null;

            PromptEntityOptions opt = 
                new PromptEntityOptions("\nPick an entity: ");
            PromptEntityResult res = _editor.GetEntity(opt);

            if (res.Status == PromptStatus.OK)
            {
                id = res.ObjectId;

                //if the entity is a BlockRefernece, get Insertion Point
                //otherwise get the picked point
                Point3d p;
                if (GetBlockInsPoint(res.ObjectId, out p))
                    pt = p;
                else
                    pt = res.PickedPoint;

                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetBlockInsPoint(ObjectId id, out Point3d insPt)
        {
            insPt=new Point3d();
            bool isBlk = false;

            using (Transaction tran = _dwg.Database.
                TransactionManager.StartTransaction())
            {
                BlockReference blk = tran.GetObject(
                    id, OpenMode.ForRead) as BlockReference;

                if (blk != null)
                {
                    insPt = blk.Position;
                    isBlk = true;
                }

                tran.Commit();
            }

            return isBlk;
        }

        private bool PickNextEntity()
        {
            _basePoint = _points[_points.Count - 1];

            string msg = 
                "\n" + _points.Count + " picked. Pick next entity: ";
            PromptEntityOptions opt = new PromptEntityOptions(msg);

            try
            {
                //Create rubber band line
                _rubberLine = new Line(_basePoint, _basePoint);

                //Set line properties, for example
                _rubberLine.SetDatabaseDefaults(_dwg.Database);

                //Create Transient graphics
                IntegerCollection intCol = new IntegerCollection();
                TransientManager.CurrentTransientManager.
                    AddTransient(_rubberLine,
                    TransientDrawingMode.DirectShortTerm, 128, intCol);

                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                PromptEntityResult res =_editor.GetEntity(opt);

                if (res.Status == PromptStatus.OK)
                {
                    bool exists = false;
                    foreach (ObjectId ent in _entities)
                    {
                        if (ent == res.ObjectId)
                        {
                            exists = true;
                            break;
                        }
                    }

                    if (!exists)
                    {
                        //if the entity is a BlockRefernece, 
                        //get Insertion Point.
                        //Otherwise get the picked point
                        Point3d p;
                        if (!GetBlockInsPoint(res.ObjectId, out p))
                        {
                            p = res.PickedPoint;
                        }

                        _points.Add(p);
                        _entities.Add(res.ObjectId);

                        SetHighlight(res.ObjectId, true);
                    }
                    else
                    {
                        _editor.WriteMessage(
                            "\nThe entity has already been picked!");
                    }
                        
                    return true;
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                if (_rubberLine != null)
                {
                    //Clear transient graphics
                    IntegerCollection intCol = new IntegerCollection();
                    TransientManager.CurrentTransientManager.
                        EraseTransient(_rubberLine, intCol);

                    _rubberLine.Dispose();
                    _rubberLine = null;
                }

                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);
            }
        }

        private void SetHighlight(ObjectId id, bool highlight)
        {
            using (Transaction tran = _dwg.Database.
                TransactionManager.StartTransaction())
            {
                Entity ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);

                if (highlight)
                    ent.Highlight();
                else
                    ent.Unhighlight();
            }
        }

        private void _editor_PointMonitor(
            object sender, PointMonitorEventArgs e)
        {
            //Get mouse cursor point
            _currentPoint = e.Context.RawPoint;

            //Update line
            _rubberLine.EndPoint = _currentPoint;

            //Update Transient graphics
            IntegerCollection intCol = new IntegerCollection();
            TransientManager.CurrentTransientManager.
                UpdateTransient(_rubberLine, intCol);
        }

        #endregion
    }
}

Then, I use "PickEntities" class in a command method of a command class:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;

[assembly: CommandClass(typeof(PickMultiplePoints.MyCommands))]

namespace PickMultiplePoints
{
    public class MyCommands
    {
        [CommandMethod("MyPick")]
        public static void DoCommand()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            Editor ed = dwg.Editor;

            //Do picking
            PickEntities picker = new PickEntities(dwg);
            picker.DoPick(true);

            ObjectId[] ids = picker.PickedEntities.ToArray();

            ed.WriteMessage(
                "\nMy command executed: {0} entities picked.", 
                ids.Length);
        }
    }
}

Here is the video clip of the code in action.

Use the technique presented in this article, one can also makes AutoCAD draw a polyline started from the first picked entity/point along the picked entities dynamically.

Thursday, January 13, 2011

Creating A "Move" Command With .NET API

In AutoCAD VBA era, although programmatic moving an entity with user interaction is fairly easy to do, but there is no way to make it behave like AutoCAD built-in "MOVE" command - the entity to be moved can be dragged with the cursor, which gives user a very good visual hint as where the entity is moving.

With AutoCAD .NET API being available, programming an custom command that allows user to drag entity or entities becomes a hot AutoCAD programming technique that every AutoCAD programmer would like to try out.

There are two ways to do entity dragging: creating a custom Jig class that derived from EntityJig or DrawJig class; or use a delegate (DragCallBack) in Editor.Drag() method. There are quite some discussions on the topic of Jig in various blogs/forums. In this article, I focus on the latter - Editor.Drag() with a delegate.

Kean Walmsly posted an article on this topic here. However, in dragging part of his code, there is no code to show a rubber band and show the entity/entities being dragged in highlight (of course this is because that was not what his post focused on).

The code shown below tries to imitate AutoCAD's "MOVE" command: the entity/entities can be selected before or after the custom command starts; the selected entity/entities being highlighted, a rubber band line being drawn from base point to the mouse cursor; the dragging method can ask user to pick base point; if no base point is required, the code finds the lower left corner of bounding box of all selected entities as base point... Enough explanations. Here are the code snippets following.

First, in the project, add a class "DragMove":

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

namespace InteractiveDrag
{
    public class DragMove
    {
        private Document _dwg;
        private SelectionSet _sset;

        private Point3d _basePoint = new Point3d(0.0, 0.0, 0.0);
        private bool _useBasePoint = false;
        private Line _rubberLine = null;

        public DragMove(Document dwg, SelectionSet ss)
        {
            _dwg = dwg;
            _sset = ss;
        }

        #region public properties

        public bool UseBasePoint
        {
            set { _useBasePoint = value; }
            get { return _useBasePoint; }
        }

        public Point3d BasePoint
        {
            set { _basePoint = value; }
            get { return _basePoint; }
        }

        #endregion

        #region public methods

        public void DoDrag()
        {
            if (_sset.Count == 0) return;

            _rubberLine = null;

            using (Transaction tran = 
                _dwg.Database.TransactionManager.StartTransaction())
            {
                //Highlight entities
                SetHighlight(true, tran);

                if (!_useBasePoint)
                {
                    _basePoint = GetDefaultBasePoint();
                }
                else
                {
                    Point3d pt;
                    if (!PickBasePoint(_dwg.Editor, out pt)) return;

                    _basePoint = pt;
                    _useBasePoint = true;
                }

                PromptPointResult ptRes = _dwg.Editor.Drag
                    (_sset, "\nPick point to move to: ",
                        delegate(Point3d pt, ref Matrix3d mat)
                        {
                            if (pt == _basePoint)
                            {
                                return SamplerStatus.NoChange;
                            }
                            else
                            {
                                if (_useBasePoint)
                                {
                                    if (_rubberLine == null)
                                    {
                                        _rubberLine = new Line(_basePoint, pt);
                                        _rubberLine.SetDatabaseDefaults(_dwg.Database);

                                        IntegerCollection intCol;

                                        //Create transient graphics: rubberband line
                                        intCol = new IntegerCollection();
                                        TransientManager.CurrentTransientManager.
                                            AddTransient(_rubberLine, 
                                            TransientDrawingMode.DirectShortTerm, 128, intCol);
                                    }
                                    else
                                    {
                                        _rubberLine.EndPoint = pt;

                                        //Update the transient graphics
                                        IntegerCollection intCol = new IntegerCollection();
                                        TransientManager.CurrentTransientManager.
                                            UpdateTransient(_rubberLine, intCol);
                                    }
                                }

                                mat = Matrix3d.Displacement(_basePoint.GetVectorTo(pt));
                                return SamplerStatus.OK;
                            }
                        }
                    );

                if (_rubberLine != null)
                {
                    //Clear transient graphics
                    IntegerCollection intCol = new IntegerCollection(); 
                    TransientManager.CurrentTransientManager.
                        EraseTransient(_rubberLine, intCol);

                    _rubberLine.Dispose();
                    _rubberLine = null;
                }

                if (ptRes.Status == PromptStatus.OK)
                {
                    MoveObjects(ptRes.Value, tran);
                }

                //Unhighlight entities
                SetHighlight(false, tran);

                tran.Commit();
            }
        }

        public void DoDrag(bool pickBasePt)
        {
            _useBasePoint = pickBasePt;

            DoDrag();
        }

        public void DoDrag(Point3d basePt)
        {
            _basePoint = basePt;
            _useBasePoint = true;

            DoDrag();
        }

        #endregion

        #region private methods

        private Point3d GetDefaultBasePoint()
        {
            Point3d pt = new Point3d();

            Extents3d exts = new Extents3d(
                new Point3d(0.0, 0.0, 0.0), new Point3d(0.0, 0.0, 0.0));

            using (Transaction tran = 
                _dwg.Database.TransactionManager.StartTransaction())
            {
                ObjectId[] ids=_sset.GetObjectIds();
                for (int i = 0; i < ids.Length; i++ )
                {
                    ObjectId id = ids[i];
                    Entity ent = (Entity)tran.GetObject(id, OpenMode.ForRead);

                    if (i == 0)
                    {
                        exts = ent.GeometricExtents;
                    }
                    else
                    {
                        Extents3d ext = ent.GeometricExtents;
                        exts.AddExtents(ext);
                    }
                }

                tran.Commit();
            }

            return exts.MinPoint;
        }

        private bool PickBasePoint(Editor ed, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = new PromptPointOptions("Pick base point: ");
            PromptPointResult res = ed.GetPoint(opt);
            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private void MoveObjects(Point3d pt, Transaction tran)
        {
            Matrix3d mat = Matrix3d.Displacement(_basePoint.GetVectorTo(pt));
            foreach (ObjectId id in _sset.GetObjectIds())
            {
                Entity ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);
                ent.TransformBy(mat);
            }
        }

        private void SetHighlight(bool highlight, Transaction tran)
        {
            foreach (ObjectId id in _sset.GetObjectIds())
            {
                Entity ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);

                if (highlight)
                    ent.Highlight();
                else
                    ent.Unhighlight();
            }
        }

        #endregion
    }
}

Here is the Command class:

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;

[assembly: CommandClass(typeof(InteractiveDrag.MyCommand))]

namespace InteractiveDrag
{
    public class MyCommand
    {
        [CommandMethod("DragMove", CommandFlags.UsePickSet)]
        public static void DoMove()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            Editor ed = dwg.Editor;

            //Get PickFirst SelectionSet
            PromptSelectionResult setRes = ed.SelectImplied();

            if (setRes.Status != PromptStatus.OK)
            {
                //if not PickFirst set, ask user to pick:
                ObjectId[] ids = GetUserPickedObjects(dwg);
                if (ids.Length == 0)
                {
                    ed.WriteMessage("\n*Cancelled*");
                    ed.WriteMessage("\n*Cancelled*");
                    return;
                }

                ed.SetImpliedSelection(ids);
                setRes = ed.SelectImplied();

                if (setRes.Status != PromptStatus.OK)
                {
                    ed.Regen();
                    return;
                }
            }
            
            //Do the dragging with the selectionSet
            DragMove drag = new DragMove(dwg, setRes.Value);
            drag.DoDrag(true);
        }

        private static ObjectId[] GetUserPickedObjects(Document dwg)
        {
            List ids = new List();
            using (Transaction tran = 
                dwg.Database.TransactionManager.StartTransaction())
            {
                bool go = true;
                while (go)
                {
                    go = false;

                    PromptEntityOptions opt = 
                        new PromptEntityOptions("\nPick an entity: ");
                    PromptEntityResult res = dwg.Editor.GetEntity(opt);

                    if (res.Status == PromptStatus.OK)
                    {
                        bool exists = false;
                        foreach (ObjectId id in ids)
                        {
                            if (id == res.ObjectId)
                            {
                                exists = true;
                                break;
                            }
                        }

                        if (!exists)
                        {
                            //Highlight
                            Entity ent = (Entity)tran.GetObject(
                                res.ObjectId, OpenMode.ForWrite);

                            ent.Highlight();

                            ids.Add(res.ObjectId);
                            go = true;
                        }
                    }
                }

                tran.Commit();
            }

            return ids.ToArray();
        }
    }
}

Build the project and "NETLOAD" it into AutoCAD. See this video clip for the behaviuor of the code shown above.

With some slight modification to the code one can easily do a drag-copying, drag-rotating...