Thursday, February 24, 2022

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

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

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

Anyway, here is the code:

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

The CommandClass/Method to run it:

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


The video below shows the code in action:







Tuesday, February 15, 2022

Using Transient Graphics to Prompt User on Screen

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

First a class TransientPrompt:

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

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

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

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





Sunday, November 21, 2021

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

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

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

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

This picture shows the ModelSpace contents to be shown:


The coding process would be:

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

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

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

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

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

Option Explicit

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

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

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

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

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

Public Sub GetViewportData(blk As AcadBlockReference)

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

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

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


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


Option Explicit

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

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

Private Sub CreatePVort(border As BorderBlock)

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

See following video clip for the action of the code:





Friday, September 10, 2021

Using EntityJig with Keyword Options for Copying Entity

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

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

Some thoughts here:

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

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

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

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

See the video clip below showing how the code works:



Saturday, August 14, 2021

An EntityJig For MLine Creation

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

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

This is the class MLineJig:

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

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

See this video clip for the code action:




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.