Saturday, August 14, 2021

An EntityJig For MLine Creation

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

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

This is the class MLineJig:

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

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

See this video clip for the code action:




Monday, March 15, 2021

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

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

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

Here is the class BlockNestedEntityHighlighter:

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

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

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

See this video clip for the effect:


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

Update

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

Class BlockNestedEntityHighlighter:

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

CommandClass MyCommands:

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

Thursday, March 11, 2021

Drag & Drop In AutoCAD (3) - Drag From PlugIn UI And Drop Onto AutoCAD

This is the third of this series on  Drag & Drop operation in AutoCAD. See previous ones here:

    Drag & Drop In AutoCAD (1) - The Scenarios

    Drag & Drop In AutoCAD (2) - Drag From External Application And Drop Onto AutoCAD

In this post I look into the scenario of dragging from an AutoCAD plugin UI and dropping onto AutoCAD's editor. Typically, we may have a form/window UI in our plugin application, where some data representing entities to be created in drawing. User can click a button on the UI to trigger the creation in the drawing with data in the UI; or we can let user to drag certain portion of the UI (i.e. dragging some data) and drop the data onto AutoCAD editor to let AutoCAD do something with the dropped data, usually, it is to create entity or entities with the data at the location of dropping.

If one drags something on the UI and drop it onto the same UI, the code logic of doing it is like:

1. Set the drop target control's AllowDrop property to True;

2. Handle MouseMove event of the dragging target control;

3. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

4. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call drop target control's DoDragDrop() method with the IDataObject passed in;

5. Handle drop target control's DragDrop event, where you can extract data transferred via IDataObject, and then do whatever with the data suitable to the drop target control.

Now that we want to change the drop target to AutoCAD application, not somewhere on the same UI as the drag target, the code logic for this kind of drag and drop process changes to:

1.Handle MouseMove event of drag target control on the UI;

2. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

3. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call Application.DoDragDrop(), where you not only pass IDataObject into it, you also need to implement the abstract class of Autodesk.AutoCAD.Windows.DropTarget and pass it into Application.DoDragDrop();

4. In your custom DropTarget class, you override the abstract method OnDrop(): that is where you unpackage data from the IDataObject and do whatever you need to do with the data, mostly, it is to create entity or entities at the dropping location.

I have to point out that dragging from plugin's UI and dropping onto AutoCAD only works with modeless plugin UI.

This is the plugin UI, which is a WinForm modeless dialog:


With the form floating on top of AutoCAD, user can drag one the 2 blue labels and drop onto AutoCAD to generate a line or circle entity; or user can drag a block name from the block list box to insert a block reference at dropping location. Here is the form's code behind:

using System;
using System.Linq;
using System.Windows.Forms;
 
namespace DragFromPluginUiToAcad
{
    public partial class DrawForm : Form
    {
        public DrawForm()
        {
            InitializeComponent();
        }
 
        private void BUttonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
 
        private void InitailizeControls()
        {
            try
            {
                Cursor.Current = Cursors.Default;
                
                var blocks = CadHelper.GetBlockList();
 
                if (blocks.Count() > 0)
                {
                    foreach (var blk in blocks)
                    {
                        BlockListBox.Items.Add(blk);
                    }
                }
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            ComboAngle.SelectedIndex = 0;
        }
 
        private void DrawForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void DrawForm_Load(object sender, EventArgs e)
        {
            InitailizeControls();
        }
 
        private void Linelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button== MouseButtons.Left)
            {
                EntityInfo entInfo = new LineInfo
                {
                    Length = Convert.ToDouble(TextBoxLength.Text),
                    Angle = Convert.ToDouble(ComboAngle.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Line, entInfo, CadHelper.CreateLine);
            }
        }
 
        private void Circlelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new CircleInfo
                {
                    Radius=Convert.ToDouble(TextBoxRadius.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Circle, entInfo, CadHelper.CreateCircle);
            }
        }
 
        private void BlockListBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (BlockListBox.SelectedIndex < 0) return;
 
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new BlockInfo
                {
                    BlockName=BlockListBox.Text
                };
                EntityDropper.DropEntity(
                    thisEntityType.BlockReference, entInfo, CadHelper.CreateBlockReference);
            }
        }
    }
}

As the code shows, the dragging begins by handling MouseMove event when the left mouse button is pressed, and the data used for creating entity is passed into a static method DropEntity() from a utility class EntityDropper, where Application.DoDragDrop() will be called to complete the drag & drop operation.

First, I needed to implement custom class that is derived from abstract class Autodesk.AutoCAD.Windows.DropTarget:

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class EntityDropTarget : DropTarget
    {
        private readonly Func<DocumentEntityInfoPoint3dObjectId> _createFunction;
        private Point3d _position = Point3d.Origin;
 
        public EntityDropTarget(Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            _createFunction = entityCreation;
        }
 
        public ObjectId EntityId { private setget; } = ObjectId.Null;
        public override void OnDrop(DragEventArgs e)
        {
            //Convert windows location of mouse into AutoCAD editor's
            //WCS coordinate (Point3d)
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
 
            System.Drawing.Point pt = new System.Drawing.Point(e.X, e.Y);
            _position = dwg.Editor.PointToWorld(pt);
 
            // Get data passed from Application.DoDragDrop()
            var data = e.Data.GetData(typeof(EntityInfo)) as EntityInfo;
 
            //Create the entity
            EntityId = _createFunction(dwg, data, _position);
 
            CadApp.MainWindow.Focus();
            dwg.Editor.WriteMessage($"\nEntity {EntityId} has been created @{_position}");
        }
    }
}

As the code shows in EntityDropTarget class that the drag & drop process is completed in OnDrop() method, where the data required for creating entity is extracted from IDataObject and then the actual entity generating work is done. In order to decouple the drag & drop process with how entity is to be created, I used an injected function (Func<Document, EntityInfo, Point3d, ObjectId>) to do the entity generating work. This allows me coding different way to generate entity without having to change the code for drag & drop. For example, for most user-friendly way in real production, it would be better to use a Jig to create entity, which would allow user to position the entity more accurately. But in this article, for simplicity, I just supply a simple entity creating process. Also, it is with OnDrop() method, the code can translate the mouse's drop location into AutoCAD's coordinate, so that the code would know where to create the entity. However, because it is quite difficult for user to drop (release let button) at an accurate location without usual AutoCAD drafting assistant (object snapping, coordinate entering), the drop location obtained from OnDrop() method would often be not good enough. That is why I just said, in real production, it would be ideal to use Jig for accurate entity creation.

Here is the utility class that has a static method to actually call Application.DoDragDrop():

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public enum EntityType
    {
        Line=0,
        Circle=1,
        BlockReference=2,
    }
 
    public class EntityDropper
    {
        public static ObjectId  DropEntity(
            System.Windows.Forms.Form userUi, 
            EntityType entType, 
            EntityInfo entityInfo,
            Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            var target = new EntityDropTarget(entityCreation);
            object data;
            IDataObject dataObject;
            if (entType== EntityType.Line)
            {
                data = (LineInfo)entityInfo;
            }
            else if (entType== EntityType.Circle)
            {
                data = (CircleInfo)entityInfo;
            }
            else
            {
                data = (BlockInfo)entityInfo;
            }
            dataObject = new DataObject();
            dataObject.SetData(typeof(EntityInfo), data);
            
            CadApp.DoDragDrop(userUi, dataObject, DragDropEffects.Copy, target);
 
            return target.EntityId;
        }
    } 
}

Obviously I also need a data class for transferring entity creating information between dragging and dropping via IDataObject:

namespace DragFromPluginUiToAcad
{
    public abstract class EntityInfo
    {
    }
 
    public class LineInfo : EntityInfo
    {
        public double Angle { setget; }
        public double Length { setget; }
    }
 
    public class CircleInfo : EntityInfo
    {
        public double Radius { setget; }
    }
 
    public class BlockInfo : EntityInfo
    {
        public string BlockName { setget; }
    }
}

In order to inject an entity creating function, all the data classes (LineInfo, CircleInfo and BlockInfo) are derived from an abstract class EntityInfo, which does not have its own properties/methods and only serves as a base class, so that the injected entity creating function can have a unified method signature.

Of course I need those injectable entity creating functions:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class CadHelper
    {
        public static IEnumerable<string> GetBlockList()
        {
            IEnumerable<string> names = null;
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var tbl = (BlockTable)tran.GetObject(dwg.Database.BlockTableId, OpenMode.ForRead);
                names = GetBlockNames(tbl, tran);
 
                tran.Commit();
            }
            return names;
        }
 
        public static ObjectId CreateLine(Document dwg, EntityInfo lineInfo, Point3d position)
        {
            var newId = ObjectId.Null;
 
            var len = ((LineInfo)lineInfo).Length;
            var ang = ((LineInfo)lineInfo).Angle * Math.PI / 180.0;
 
            var start = position;
            var x = position.X + len * Math.Cos(ang);
            var y = position.Y + len * Math.Sin(ang);
            var end = new Point3d(x, y, position.Z);
 
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var line = new Line(start, end);  
                    line.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(line);
                    tran.AddNewlyCreatedDBObject(line, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateCircle(Document dwg, EntityInfo circleInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var circle = new Circle();
                    circle.Center = position;
                    circle.Radius = ((CircleInfo)circleInfo).Radius;
                    circle.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(circle);
                    tran.AddNewlyCreatedDBObject(circle, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateBlockReference(Document dwg, EntityInfo blkInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var bt = (BlockTable)tran.GetObject(
                        dwg.Database.BlockTableId, OpenMode.ForRead);
                    if (bt.Has(((BlockInfo)blkInfo).BlockName))
                    {
                        var blk = new BlockReference(position, bt[((BlockInfo)blkInfo).BlockName]);
                        blk.SetDatabaseDefaults();
 
                        var space = (BlockTableRecord)tran.GetObject(
                            dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                        newId = space.AppendEntity(blk);
                        tran.AddNewlyCreatedDBObject(blk, true);
 
                    }
                    tran.Commit();
                }
            }
 
            return newId; ;
        }
 
        #region private methods
 
        private static IEnumerable<string> GetBlockNames(BlockTable tbl, Transaction tran)
        {
            var names = new List<string>();
            foreach (ObjectId id in tbl)
            {
                var blk = (BlockTableRecord)tran.GetObject(id, OpenMode.ForRead);
                if (!blk.IsLayout)
                {
                    names.Add(blk.Name);
                }
            }
 
            return from n in names orderby n ascending select n;
        }
 
        #endregion
    }
}

As I have already mentioned, in these injectable entity creating function, I just create entities (Line/Circle/BlockReference) at drop location, for the reason of simplicity. In real production, I would use EntityJig for better usability.

Finally, here is the CommandClass that put everything together to work, which simply show the plugin UI as singleton modeless dialog to allow user drag something from it and drop onto AutoCAD to create entities.

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(DragFromPluginUiToAcad.MyCommands))]
 
namespace DragFromPluginUiToAcad
{
    public class MyCommands 
    {
        private static DrawForm _floatForm = null;
 
        [CommandMethod("AddEntByDrag"CommandFlags.Session)]
        public static void AddEntityFromModelessForm()
        {
            if (_floatForm==null)
            {
                _floatForm = new DrawForm();
            }
 
            CadApp.ShowModelessDialog(CadApp.MainWindow.Handle, _floatForm);
        }
    }
}

See this video for the code in action:


Some Thoughts

While using drag & drop to create entities from custom plugin UI gives us programmers another "fancy way" to deliver CAD user experience, it is more common to trigger the process with proper control on the UI, typically, user would click button for this operation, which can be used with both modal and modeless UI (if the UI is modal, the code needs to call Editor.StartUserInteraction() to temporarily hide the modal UI). However, if the UI is modeless, the action (of creating entities) triggered from UI should be wrapped in a custom CommandMethod and UI's event handler (for the button click) should call SendStringToExecute() (see this article of mine on this topic); but when doing drag & and drop, as shown here, the action triggered from modeless UI (i.e. dragging, and then dropping) is not wrapped in a CommandMethod and no SendStringToExecute() involved.

Again, the dropping part of drag & drop operation does not provide an accurate dropping location as AutoCAD native drafting function does, it may not be as attractive as it looks like at first, considering that user has to hold left button while moving the mouse, in comparison to a single click of a button, or a double-click on something (such as a block icon on Tool Palette, or the latest block inserting palette). But, since drag & drop is a traditional GUI trick, some users may like it, if we can provide it.



Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.