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:







1 comment:

  1. Hello, very good bloggers, but I have a question about preview before drawing, block preview control

    ReplyDelete