Saturday, March 14, 2020

Showing Helpful Information As Tool Tip During Jig Dargging - Updated

Recently there is a discussion thread in Autodesk's user discussion forum about showing information of an entity when mouse cursor is near, or hover, it during jig dragging. While the OP did indicate that my suggestion helped (in some way, but no details were provided), this discussion also somehow inspired extra interest for me to play it a bit. So, I decided to write something here on these topics:

1. How easy it is to create a custom "jig" with combination of Transient Graphics and Editor.PointMonitor event handling;

2. How easy to show custom tool tip because of handling PointMonitor;

3. How to obtain different tool tip content from an entity, over which the mouse cursor crosses, according to required business logic without having to modify PointMonitor event handling code when the need for different tool tip content arise while this custom job is used.

Any one who reads my blogs over the years would know that I had quite a few articles discussing Transient Graphics and handling PointMonitor. Using them to create a custom "jig" is really simple (even arguably simpler than deriving from built-in Entity/DrawJig class, in some case). Here I am going to create a custom moving jig for user to select an entity (say, a Circle); then, drag a ghost image of the entity to (or close to) another entity; during the dragging, when the mouse cursor is over, or near, the other entity, some information about the it will show as tool tip, so that user can decide if the first entity is dragged toward correct position. Obviously the tool tip content should be somewhat related to the second entity and the information is somehow not very obvious to the user (for example, the second entity may have XData attached, which may be critical for use to decide whether to drag the first entity to here or not).

Now that I have the process context clear enough, here is the code of the custom jig.

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace JigWithTooltip
{
    public class TooltipMovingJig
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private Entity _ghost = null;
        private Entity _entity = null;
        private Point3d _basePoint = Point3d.Origin;
        private Point3d _mousePoint = Point3d.Origin;
 
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
 
        public TooltipMovingJig(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;      
        }
 
        public void MoveEntity()
        {
            if (!SelectEntity(out ObjectId entIdout _basePoint)) return;
 
            _mousePoint = _basePoint;
 
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                _entity = (Entity)tran.GetObject(entIdOpenMode.ForRead);
                _entity.Highlight();
                try
                {
                    if (GetDestinationPoint(out Point3d destPoint))
                    {
                        var mt = Matrix3d.Displacement(
                            _basePoint.GetVectorTo(destPoint));
                        _entity.UpgradeOpen();
                        _entity.TransformBy(mt);
                    }
                }
                finally
                {
                    _entity.Unhighlight();
                }
 
                tran.Commit();
            }
        }
 
        #region private methods
 
        private bool SelectEntity(out ObjectId entIdout Point3d basePoint)
        {
            entId = ObjectId.Null;
            basePoint = Point3d.Origin;
 
            var res = _ed.GetEntity("\nSelect entity to move:");
            if (res.Status == PromptStatus.OK )
            {
                entId = res.ObjectId;
                basePoint = res.PickedPoint;
 
                var opt = new PromptPointOptions(
                    "\nSelect base point:");
 
                var pRes = _ed.GetPoint(opt);
                if (pRes.Status== PromptStatus.OK)
                {
                    basePoint = pRes.Value;
                }
 
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void CreateMovingGhost()
        {
            ClearMovingGhost();
 
            _ghost = _entity.Clone() as Entity;
            _ghost.ColorIndex = 2;
            var mt = Matrix3d.Displacement(_basePoint.GetVectorTo(_mousePoint));
            _ghost.TransformBy(mt);
 
            _tsManager.AddTransient(
                _ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearMovingGhost()
        {
            if (_ghost != null)
            {
                _tsManager.EraseTransient(_ghost, new IntegerCollection());
                _ghost.Dispose();
                _ghost = null;
            }
        }
 
        private bool GetDestinationPoint(out Point3d destPoint)
        {
            destPoint = Point3d.Origin;
            var picked = false;
 
            var opt = new PromptPointOptions(
                "Move to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.UseDashedLine = true;
 
            _ed.PointMonitor += Editor_PointMonitor;
 
            try
            {
                var res = _ed.GetPoint(opt);
                if (res.Status == PromptStatus.OK)
                {
                    destPoint = res.Value;
                    picked = true;
                }
            }
            finally
            {
                ClearMovingGhost();
                _ed.PointMonitor -= Editor_PointMonitor;
            }
            
            return picked;
        }
 
        private void Editor_PointMonitor(object senderPointMonitorEventArgs e)
        {
            _mousePoint = e.Context.RawPoint;
            CreateMovingGhost();
 
            ObjectId id = GetHoveredEntity(e.Context.RawPoint);
            if (!id.IsNull)
            {
                // Compose custom tool tip text
                var tip = GetDefaultTooltip(id);
                e.AppendToolTipText(tip);
            }
        }
 
        private ObjectId GetHoveredEntity(Point3d mousePoint)
        {
            var entId = ObjectId.Null;
 
            var res = _ed.SelectAtPickBox(mousePoint);
            if (res.Status== PromptStatus.OK)
            {
                entId = res.Value[0].ObjectId;
            }
 
            return entId;
        }
 
        private string GetDefaultTooltip(ObjectId entId)
        {
            return $"\nMove to/close to:{entId.ObjectClass.DxfName.ToUpper()}";
        }
 
        #endregion
    }
}

We can see code for this custom jig is incredibly simple: after selecting an entity to move (and its base point for moving), we only need to surround the moving point picking call (Editor.GetPoint()) with adding/removing PointMonitor event handler. The "jig effect" (showing a ghost image of selected entity that follows mouse cursor) is done by creating Transient Graphics in the event handler.

Since PointMonitorEventArgs object provides a method AppendToolTipText() method, we can update the tool tip with whatever text we want to. Thus, if we want to get information of an entity where the mouse cursor is crossing or is near, then we need to be able to know when the mouse cursor is crossing/is near to an entity. When AutoCAD Editor is not is selecting/picking mode, PointMonitorEventArgs.Context.GetPickedEmtities() would provide a way to find out if there is entity or entities at the mouse cursor. But unfortunately, in this custom jig case, because AutoCAD Editor is waiting for picking a point, the GetPickedEntities() method will not return any entity at mouse cursor at all. So, I need to somehow be able to programmatically "select" an entity that is at mouse cursor or very near to when mouse is moving/dragging.

In one of my old article, I used cursor pick box to call Editor.SelectCrossingPolygon[Window]() to select entities. I thought I could re-use that code. Of course, in this case, the mouse cursor is displayed as "cross", not "pick box", because AutoCAD Editor is waiting for a point picking. So, here the the extension method to select entity at mouse cursor with an invisible small window in size of the regular AutoCAD selecting box.

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
 
namespace JigWithTooltip
{
    public static class EditorSelectionExtension
    {
        public static PromptSelectionResult SelectAtPickBox(
           this Editor edPoint3d pickBoxCentre)
         {
            // Get pick box's size on screen
            System.Windows.Point screenPt = ed.PointToScreen(pickBoxCentre, 1);
     
            // Get pickbox's size. Note, the number obtained from
            // system variable "PICKBOX" is actually the half of
            // pickbox's width/height
            int pSize = Convert.ToInt32(Application.GetSystemVariable("PICKBOX"));
            if (pSize < 10) pSize = 10;
 
            // Define a Point3dCollection for CrossingWindow selecting
            Point3dCollection points = new Point3dCollection();
    
            System.Windows.Point p;
            Point3d pt;
    
            p = new System.Windows.Point(screenPt.X - pSizescreenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1).TransformBy(ed.CurrentUserCoordinateSystem.Inverse());
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSizescreenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1).TransformBy(ed.CurrentUserCoordinateSystem.Inverse());
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSizescreenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1).TransformBy(ed.CurrentUserCoordinateSystem.Inverse());
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X - pSizescreenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1).TransformBy(ed.CurrentUserCoordinateSystem.Inverse());
            points.Add(pt);
    
            return ed.SelectCrossingPolygon(points);
        }
    }
}

At this point, the moving jig with tool tip prompt is completed. Here is the code to use it in a command class:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(JigWithTooltip.MyCommands))]
 
namespace JigWithTooltip
{
    public class MyCommands
    {
        [CommandMethod("DoMove")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig(dwg);
                mover.MoveEntity();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
    }
}

This video clip shows the custom jig effect. One would notice that how custom tool tip appears when the mouse cursor crosses or is near to another entity.

Thus far the topic 1 and 2 have been addressed. Stay tuned for topic 3 in my next post.

Update

After I posted this, and involved another discussion thread, Alexander Rivilis suggested another solution to make PointMonitorEventArgs.Context.GetPickedEntities() work while Editor is waiting for user input, that is, use Editor.TurnForcedPickOn[Off]() method pair. I went ahead to give it a try. Indeed, this solves the issue with GetPickedEntities() in the PointMonitor event handler. So, I no longer need the Editor extension method Editor.SelectAtPickBox(), which make the code simpler. Here is the updated code (changed lines are in red).

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace JigWithTooltip
{
    public class TooltipMovingJig
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private Entity _ghost = null;
        private Entity _entity = null;
        private Point3d _basePoint = Point3d.Origin;
        private Point3d _mousePoint = Point3d.Origin;
 
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
 
        public TooltipMovingJig(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;      
        }
 
        public void MoveEntity()
        {
            if (!SelectEntity(out ObjectId entIdout _basePoint)) return;
 
            _mousePoint = _basePoint;
 
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                _entity = (Entity)tran.GetObject(entIdOpenMode.ForRead);
                _entity.Highlight();
                try
                {
                    if (GetDestinationPoint(out Point3d destPoint))
                    {
                        var mt = Matrix3d.Displacement(
                            _basePoint.GetVectorTo(destPoint));
                        _entity.UpgradeOpen();
                        _entity.TransformBy(mt);
                    }
                }
                finally
                {
                    _entity.Unhighlight();
                }
 
                tran.Commit();
            }
        }
 
        #region private methods
 
        private bool SelectEntity(out ObjectId entIdout Point3d basePoint)
        {
            entId = ObjectId.Null;
            basePoint = Point3d.Origin;
 
            var res = _ed.GetEntity("\nSelect entity to move:");
            if (res.Status == PromptStatus.OK )
            {
                entId = res.ObjectId;
                basePoint = res.PickedPoint;
 
                var opt = new PromptPointOptions(
                    "\nSelect base point:");
 
                var pRes = _ed.GetPoint(opt);
                if (pRes.Status== PromptStatus.OK)
                {
                    basePoint = pRes.Value;
                }
 
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void CreateMovingGhost()
        {
            ClearMovingGhost();
 
            _ghost = _entity.Clone() as Entity;
            _ghost.ColorIndex = 2;
            var mt = Matrix3d.Displacement(_basePoint.GetVectorTo(_mousePoint));
            _ghost.TransformBy(mt);
 
            _tsManager.AddTransient(
                _ghost, 
                TransientDrawingMode.DirectTopmost, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearMovingGhost()
        {
            if (_ghost != null)
            {
                _tsManager.EraseTransient(_ghost, new IntegerCollection());
                _ghost.Dispose();
                _ghost = null;
            }
        }
 
        private bool GetDestinationPoint(out Point3d destPoint)
        {
            destPoint = Point3d.Origin;
            var picked = false;
 
            var opt = new PromptPointOptions(
                "Move to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.UseDashedLine = true;
 
            // Set system variable "PICKBOX" to at least 10 (range 0 to 20)
            // so that mouse cursor would pick up entities easily 
            // when moveving close
            var pickBox = Convert.ToInt32(
                Application.GetSystemVariable("PICKBOX"));
            bool pickBoxChanged = false;
            if (pickBox < 10)
            {
                Application.SetSystemVariable("PICKBOX", 10);
                pickBoxChanged = true;
            }
 
            var forcedCount = _ed.TurnForcedPickOn();

            _ed.PointMonitor += Editor_PointMonitor;
 
            try
            {
                var res = _ed.GetPoint(opt);
                if (res.Status == PromptStatus.OK)
                {
                    destPoint = res.Value;
                    picked = true;
                }
            }
            finally
            {
                ClearMovingGhost();
                _ed.PointMonitor -= Editor_PointMonitor;
                var count = _ed.TurnForcedPickOff();
                while(count>forcedCount-1)
                {
                    count = _ed.TurnForcedPickOff();
                }
 
                // restore "PICKBOX" original value
                if (pickBoxChanged) 
                    Application.SetSystemVariable("PICKBOX", pickBox);
            }
            
            return picked;
        }
 
        private void Editor_PointMonitor(object senderPointMonitorEventArgs e)
        {
            _mousePoint = e.Context.RawPoint;
            CreateMovingGhost();
 
            var paths = e.Context.GetPickedEntities();
            if (paths == null || paths.Length == 0) return;
            var ids = paths[0].GetObjectIds();
            if (ids == null || ids.Length == 0) return;
 
            // Compose custom tool tip text
            var tip = GetDefaultTooltip(ids[0]);
            e.AppendToolTipText(tip);
 
            //ObjectId id = GetHoveredEntity(e.Context.RawPoint);
            //if (!id.IsNull)
            //{
            //    // Compose custom tool tip text
            //    var tip = GetDefaultTooltip(id);
            //    e.AppendToolTipText(tip);
            //}
        }
 
        private ObjectId GetHoveredEntity(Point3d mousePoint)
        {
            var entId = ObjectId.Null;
 
            var res = _ed.SelectAtPickBox(mousePoint);
            if (res.Status== PromptStatus.OK)
            {
                entId = res.Value[0].ObjectId;
            }
 
            return entId;
        }
 
        private string GetDefaultTooltip(ObjectId entId)
        {
            return $"\nMove to/close to:{entId.ObjectClass.DxfName.ToUpper()}";
        }
 
        #endregion
    }
}
















2 comments: