Showing posts with label Transient graphics. Show all posts
Showing posts with label Transient graphics. Show all posts

Monday, April 20, 2020

Show Temporary Entity/Shape During User Selecting

This post is one of the solutions I offered to an discussion topic in Autodesk's discussion forum, where I proposed 2 possible solutions - using Transient Graphics in Editor.SelectionAdded[Removed] event handing to show temporary entity as visual hint to help user to decide which temporary entity would be eventually changed to permanent on in the drawing database. I may later try to code the other proposed solution of using DrawableOverrule, if I can manage a bit time later.

The idea of the process is something like:

1. User want to create a new entity, based on existing entities in the drawing; but the new entity could be different (entity type, entity geometry) when different existing entity is chosen.
2. So, user would select a few existing entities; for each selected entity, a possible new entity associated to it would be shown as temporary entity. Thus, looking at these temporary entities, user would be easily decide which temporary entity is the one he/she needed.
3. Once user made the choice, chosen temporary entity would be added to database and other temporary entities would be gone.

The solution design:

1. Creating a class that handles Editor's SelectionAdded/Removed event, so that during Editor.GetSelection() calling, Transient Graphics can be drawn as temporary entity images.
2. Since the temporary entity is only dependent to an existing entity, the actual temporary entity generating process is coded outside the said class and only to be "injected" in to the said class' selecting process. This way, the code of generating temporary entity can be easily expended to create different type of entity or entity with different geometric data without affect the process of selecting with temporary entity image.

Here is the code of class MySelector:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace TempEntityInSelection
{
    public class MySelector : IDisposable 
    {
        private readonly Document _dwg;
        private readonly Database _db;
        private readonly Editor _ed;
        private Dictionary<ObjectIdEntity> _tempEntities = 
            new Dictionary<ObjectIdEntity>();
        private Func<ObjectIdEntity> _createTempEntityFunction = null;
        private List<string> _layerFilters = new List<string>();
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
 
        public MySelector(IEnumerable<stringlayerFilter = null)
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            _db = _dwg.Database;
            _ed = _dwg.Editor;
            if (layerFilter!=null &&
                layerFilter.Count()>0)
            {
                _layerFilters.AddRange(layerFilter);
            }
        }
 
        #region publuc methods
 
        public void Dispose()
        {
            ClearTransients();
 
            if (_tempEntities.Count>0)
            {
                foreach (var item in _tempEntities)
                {
                    if (item.Value == nullcontinue;
 
                    if (item.Value.ObjectId.IsNull &&
                        !item.Value.IsDisposed) item.Value.Dispose();
                }
 
                _tempEntities.Clear();
            }
        }
 
        public ObjectId SelectTempEntity(
            Func<ObjectIdEntitycreateTempGraphicAction)
        {
            ObjectId selectedEnt = ObjectId.Null;
 
            _createTempEntityFunction = createTempGraphicAction;
 
            ObjectId[] selectedEntities = null;
            try
            {
                _ed.SelectionAdded += Editor_SelectionAdded;
                _ed.SelectionRemoved += Editor_SelectionRemoved;
 
                PromptSelectionResult res;
                if (_layerFilters.Count==0)
                {
                    res = _ed.GetSelection();
                }
                else
                {
                    var vals = new TypedValue[]
                    {
                        new TypedValue(
                            (int)DxfCode.LayerName, 
                            string.Join(",", _layerFilters.ToArray()))
                    };
                    res = _ed.GetSelection(new SelectionFilter(vals));
                }
 
                if (res.Status== PromptStatus.OK)
                {
                    selectedEntities = res.Value.GetObjectIds();
                    HighlightEntities(selectedEntitiestrue);
 
                    selectedEnt = SelectTargetEntity(selectedEntities);
                }
 
                ClearTransients();
            }
            finally
            {
                _ed.SelectionAdded -= Editor_SelectionAdded;
                _ed.SelectionRemoved -= Editor_SelectionRemoved;
 
                if (selectedEntities!=null)
                {
                    HighlightEntities(selectedEntitiesfalse);
                }
            }
 
            return selectedEnt;
        }
 
        #endregion
 
        #region private methods
 
        private void Editor_SelectionAdded(
            object senderSelectionAddedEventArgs e)
        {
            var added = e.AddedObjects;
            if (added.Count == 0) return;
 
            if (_createTempEntityFunction == nullreturn;
 
            ClearTransients();
 
            foreach (SelectedObject s in added)
            {
                if (!_tempEntities.ContainsKey(s.ObjectId))
                {
                    Entity tempEnt = _createTempEntityFunction(s.ObjectId);
                    _tempEntities.Add(s.ObjectId, tempEnt);
                }
                else
                {
                    Entity tempEnt = _tempEntities[s.ObjectId];
                    if (tempEnt != nulltempEnt.Dispose();
 
                    tempEnt = _createTempEntityFunction(s.ObjectId);
                    _tempEntities[s.ObjectId] = tempEnt;
                }
            }
 
 
            if (_tempEntities.Count>0)
            {
                DrawTransients();
            }
        }
 
        private void Editor_SelectionRemoved(
            object senderSelectionRemovedEventArgs e)
        {
            var removed = e.RemovedObjects;
            if (removed.Count == 0) return;
 
            ClearTransients();
 
            foreach (SelectedObject s in removed)
            {
                if (_tempEntities.ContainsKey(s.ObjectId))
                {
                    _tempEntities[s.ObjectId].Dispose();
                    _tempEntities.Remove(s.ObjectId);
                }
            }
 
            if (_tempEntities.Count > 0)
            {
                DrawTransients();
            }
        }
 
        private void ClearTransients()
        {
            _tsManager.EraseTransients(
                TransientDrawingMode.DirectTopmost, 
                128, 
                new IntegerCollection());
        }
 
        private void DrawTransients()
        {
            foreach (var item in _tempEntities)
            {
                _tsManager.AddTransient(
                    item.Value, 
                    TransientDrawingMode.DirectTopmost, 
                    128, 
                    new IntegerCollection());
            }
        }
 
        private void HighlightEntities(
            IEnumerable<ObjectIdentIdsbool highlight)
        {
            foreach (var id in entIds)
                HighlightEntity(idhighlight);
        }
 
        private void HighlightEntity(ObjectId entIdbool highlight)
        {
            using (var tran = entId.Database.TransactionManager.
                StartOpenCloseTransaction())
            {
                var ent = (Entity)tran.GetObject(entIdOpenMode.ForRead);
                if (highlight)
                    ent.Highlight();
                else
                    ent.Unhighlight();
            }
        }
 
        private ObjectId SelectTargetEntity(IEnumerable<ObjectIdentIds)
        {
            while(true)
            {
                var res = _ed.GetEntity(
                    "\nSelect one of the highlighted entity to draw new circle:");
                if (res.Status== PromptStatus.OK)
                {
                    if (entIds.Contains(res.ObjectId))
                    {
                        return res.ObjectId;
                    }
                    else
                    {
                        _ed.WriteMessage(
                            "\nInvalid: selected wrong entity!");
                    }
                }
                else
                {
                    break;
                }
            }
 
            return ObjectId.Null; ;
        }
 
        #endregion
    }
}

Because MySelector class is responsible to create Transient Graphics with non-database-residing entities (temporary entities), it should also be responsible to dispose these temporary entities when the process is done. To make the code of using MySelector simpler, I have it implement IDisposable interface, so that the instance of MySelector can be wrapped with using (...){...} block and its Dispose() method is called automatically.

Also, the only public method SelectTempEntity() accepts a Func<ObjectId, Entity> as argument. This function is responsible to create a new entity based on an existing entity (i.e. input an entity's ObjectId, a non-database-residing entity will be returned). This allows the programmer of using MySelector class to freely implement the Func<ObjectId, Entity> to produce whatever entity he/she wants without affecting how MySelector works.

Following is a set of static methods that implement Func<ObjectId, Entity>:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace TempEntityInSelection
{
    public class TempEntityGenerator
    {
        /// <summary>
        /// This method create a new circle at
        /// 1. If the source entity is a Circle: @its center
        /// 2. If the source is line, @its middle poiny
        /// 3. If it is neither circle, nor line, no new entity is created
        /// </summary>
        /// <param name="entId"></param>
        /// <returns></returns>
        public static Entity Create(ObjectId entId)
        {
            Circle circle = null;
 
            using (var tran = entId.Database.
                TransactionManager.StartOpenCloseTransaction())
            {
                var ent = (Entity)tran.GetObject(entIdOpenMode.ForRead);
 
                if (ent is Line || ent is Circle)
                {
                    circle = new Circle();
                    circle.ColorIndex = 1;
                    circle.Radius = 200;
 
                    if (ent is Circle)
                    {
                        circle.Center = ((Circle)ent).Center;
                    }
                    else if (ent is Line)
                    {
                        var line = (Line)ent;
                        var x = (line.EndPoint.X + line.StartPoint.X) / 2.0;
                        var y = (line.EndPoint.Y + line.StartPoint.Y) / 2.0;
                        circle.Center = new Point3d(xy, 0.0);
                    }
                }
 
                tran.Commit();
            }
 
            return circle;
        }
 
        public static Entity CreateDifferentEntity(ObjectId entId)
        {
            throw new NotImplementedException();
        }
    }
}

Now, here is the CommandClass that uses MySelector to select an target entity and actual create a new entity based selected target entity.

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(TempEntityInSelection.MyCommands))]
 
namespace TempEntityInSelection
{
    public class MyCommands
    {
        [CommandMethod("TestDraw")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            ObjectId selectedEntId = ObjectId.Null;
            try
            {
                using (var selector = new MySelector())
                {
                    selectedEntId = selector.SelectTempEntity(
                        TempEntityGenerator.Create);
                }
 
                if (!selectedEntId.IsNull)
                {
                    AddNewEntity(selectedEntId);
                }
                else
                {
                    ed.WriteMessage("\n*Cancel*");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
                ed.WriteMessage("\n*Cancel*");
            }
 
            ed.WriteMessage("\n");
        }
 
        private static void AddNewEntity(ObjectId sourceEntId)
        {
            Database db = sourceEntId.Database;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var ent = TempEntityGenerator.Create(sourceEntId);
                if (ent != null)
                {
                    ent.SetDatabaseDefaults(db);
 
                    var space = (BlockTableRecord)tran.GetObject(
                        db.CurrentSpaceId, OpenMode.ForWrite);
 
                    space.AppendEntity(ent);
                    tran.AddNewlyCreatedDBObject(enttrue);
                }
 
                tran.Commit();
            }
        }
    }
}


Following video shows the effect of running the code:







Wednesday, March 18, 2020

Showing Helpful Information As Tool Tip During Jig Dragging - 2

In the first part of this topic, I have built a quite simple moving jig by handling Editor.PointMonitor, where Transient Graphics is used to show a ghost image as the dragging effect. Because of PointMonitorEventArgs, it is really easy to show custom tool tip to provide useful information that would help user to decide where/how to drag an entity.

At this point, I have a good working moving jig that could prompt some information during entity dragging. However a new issue comes: how to make the jig to show different information as tool tip, based on business workflow, without having to modify the jig's code? When mouse cursor is at an entity during dragging, we now can get the entity's ObjectId, then we could obtain different information according to business requirements and show the information as tool tip, if necessary. Obviously, we do not want to modify the code in the PointMonitor event handler whenever there is different business requirement.

The approach to solve this is to inject a predefined tool tip generating interface functions, and the interface functions are implemented/coded outside the jig class. Following is the updated jig class code (red lines are the changes).

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 TooltipMovingJig2
    {
        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;
 
        private Func<ObjectId, Point3d, string> _tipExtractFunction = null;
        private Func<ObjectId, bool> _isTooltipTargetFunc = null;
 
        public TooltipMovingJig2(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
        }
 
        public void MoveEntity(
            Func<ObjectId, Point3d, string> tipExtractFunc = null,
            Func<ObjectId, bool> isTooltipTargetFunc = null)
        {
            if (!SelectEntity(out ObjectId entIdout _basePoint)) return;
 
            _tipExtractFunction = tipExtractFunc;
            _isTooltipTargetFunc = isTooltipTargetFunc;
 
            _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;
 
            var id = ids[0];
 
            if (_isTooltipTargetFunc != null)
            {
                if (!_isTooltipTargetFunc(id))
                {
                    id = ObjectId.Null;
                }
            }
            
            if (!id.IsNull)
            {
                var tip = GetDefaultTooltip(id);
                if (_tipExtractFunction != null)
                {
                    tip = _tipExtractFunction(id, _mousePoint);
                }
 
                e.AppendToolTipText(tip);
            }
        }
 
        private string GetDefaultTooltip(ObjectId entId)
        {
            return $"\nMove to/close to:{entId.ObjectClass.DxfName.ToUpper()}";
        }
 
        #endregion
    }
}

As the code shows, the jig class now has 2 Functions as its member, which can be injected from the jig's calling procedure. One function is to determine if an entity is the target entity that I want to show custom tool tip; the other is to generate actual tool tip content. Both function take ObjectId as input parameter; and the tool til generating function also takes a Point3d input as parameter, which is where the mouse cursor is and may be needed for specific tool tip content.

The jig class only defines the 2 function's signature (interface). The actual implementations of the 2 functions are done outside the jig class. They are injected into the jig class when the jig's public method MoveEntity() is called. Thus I am free to write different functions to determine whether an entity is the tool tip showing target and what tool tip content to be generated against the entity.

Following are 2 pairs of these functions: one pair is to test if the entity is closed polyline, if yes, get its area information as tool tip; the other pair - to test if the entity is curve, and show the distance of mouse cursor point to the curve's start point as tool tip information.

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
 
namespace JigWithTooltip
{
    public class EntityToolTipHelper
    {
        public static bool IsAreaToolTipTarget(ObjectId entId)
        {
            return entId.ObjectClass == RXClass.GetClass(typeof(Polyline));
        }
 
        public static string ExtractAreaToolTip(ObjectId entIdPoint3d mouseLocation)
        {
            if (entId.ObjectClass != RXClass.GetClass(typeof(Polyline))) return "";
 
            var tip = "";
 
            using (var tran = new OpenCloseTransaction())
            {
                var poly = (Polyline)tran.GetObject(entIdOpenMode.ForRead);
                if (poly.Closed)
                {
                    tip =$"AREA: {poly.Area.ToString("##########0.00")}";
                }
            }
 
            return tip;
        }
 
        public static bool IsDistanceToolTipTarget(ObjectId entId)
        {
            return entId.ObjectClass.IsDerivedFrom(RXClass.GetClass(typeof(Curve)));
        }
 
        public static string ExtractDistanceToolTip(ObjectId entIdPoint3d mousePoint)
        {
            if (!entId.ObjectClass.IsDerivedFrom(RXClass.GetClass(typeof(Curve)))) return "";
 
            var tip = "";
 
            using (var tran = new OpenCloseTransaction())
            {
                var curve = (Curve)tran.GetObject(entIdOpenMode.ForRead);
                var pt = curve.GetClosestPointTo(mousePointfalse);
                var dist = curve.GetDistAtPoint(pt);
 
                tip = $"DISTANCE FROM START POINT: {dist.ToString("########0.00")}";
            }
 
            return tip;
        }
    }
}

Now I can have a command to run the moving jig with area being prompted and another command to run the moving jig with distance being prompted:

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 TooltipMovingJig2(dwg);
                mover.MoveEntity();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod("MoveToArea")]
        public static void MoveWithAreaPrompt()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig2(dwg);
                mover.MoveEntity(
                    EntityToolTipHelper.ExtractAreaToolTip,
                    EntityToolTipHelper.IsAreaToolTipTarget);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod("MoveToDistance")]
        public static void MoveWithDistancePrompt()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mover = new TooltipMovingJig2(dwg);
                mover.MoveEntity(
                    EntityToolTipHelper.ExtractDistanceToolTip,
                    EntityToolTipHelper.IsDistanceToolTipTarget);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
    }
}

As the code shows, it is very easy to implement a pair of functions outside the jig class code to make the jig smart enough to decide whether custom tool tip is wanted, and what tool tip content is to appear, Here the a video clip showing the visual effect of running the commands:


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.