Monday, April 30, 2018

Move Civil3D CogoPoint's Label in "Jig" Style

There is a discussion in Autodesk's Civil3D Customization Discussion forum on moving CogoPoint's label by dragging it. CogoPoint in Civil3D  is a custom entity that has its label well designed for user to manipulate its position/rotation/reset manually. However, the manual manipulation to the label can only be done one by one. There is no way to drag multiple labels with the same displacement vector at once, thus the topic of that discussion thread.

Civil3D uses labels heavily for annotation. However, CogoPoint's label is quite different from most labels used in Civil3D: most labels in Civil3D is a independent entity on its own and has a property FeatureId pointing to the entity it annotates; while CogoPoint is a single entity that includes the label. Therefore, if we want to move CogoPoint label without change the point's location in "Jig" style, we cannot transform the CogoPoint entity according to mouse movement, rather we have to change the CogoPoint's internal geometric relation between the point location and the label location in accordance with the mouse movement. If this is doable would depend on the CogoPoint's API.

Since CogoPoint has a read/write property LabelLocation, I did a quick try with code. Yes, we can use this property, in conjunction with ResetLabel()/ResetLabelLocation()/ResetLabelRotation() to move/relocate CogoPoint's label. However, setting CogoPoint's LabelLocation property continuously in Jig implementation seems not working properly.

So, I tried different approach: in the Jig, instead of moving/relocating the CogoPoint's label, I draw some temporary entities visually representing the CogoPoints' label and move these "proxy" entities around with Jig. and only after the Jig is OKed, the actual label relocating happens with committed transaction. With this approach, the Jig becomes very easy to be implemented, and only thing left for me to decide is how to create a temporary entity to be visually presented as the labels to be moved. For the sake of simplicity, I just used a circle in yellow. I could have easily made it as a small rectangle with a leader line, or even a text entity with same text string as the label's text content. But the focus of this study is about visual hint of moving labels without change point location, I left these goodies out.

Here is the code of custom DrawJig class:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CivilDb = Autodesk.Civil.DatabaseServices;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace CogoLabelMovingJig
{
    public class LabelMovingJig : DrawJig
    {
        private Document _dwg;
 
        private List<CadDb.Entity> _points = null;
 
        private Dictionary<ObjectIdDrawable> _circles = 
            new Dictionary<ObjectIdDrawable>();
 
        private Point3d _basePoint;
        private Point3d _prevPoint;
        private Point3d _currPoint;
 
        public LabelMovingJig(Document dwg):base()
        {
            _dwg = dwg;
        }
 
        public void JigCogoLabels()
        {
            var pointIds = SelectCogoPoints();
            if (pointIds==null)
            {
                _dwg.Editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            if (!PickBasePoint(out _basePoint))
            {
                _dwg.Editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            _currPoint = _basePoint;
            _prevPoint = _basePoint;
 
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                _points = (from id in pointIds
                           select GetCogoPointFromId(id, tran)).ToList();
 
                bool oked = false;
 
                CreateDrawables(_points);
                if (_circles.Count > 0)
                {
                    try
                    {
                        var res = _dwg.Editor.Drag(this);
 
                        if (res.Status == PromptStatus.OK)
                        {
                            oked = true;
                        }
 
                        foreach (var ent in _points)
                        {
                            ent.Unhighlight();
                        }
 
                        if (oked)
                        {
                            // Only relocate CogoPoints' label after Jig is OKed.
                            foreach (var point in _points)
                            {
                                if (_circles.ContainsKey(point.ObjectId))
                                {
                                    var location = ((Circle)_circles[point.ObjectId]).Center;
                                    var cogo = (CivilDb.CogoPoint)point;
                                    cogo.LabelLocation = location;
                                }
                            }
 
                            tran.Commit();
                        }
                        else
                        {
                            tran.Abort();
                        }
                    }
                    finally
                    {
                        if (_circles.Count > 0)
                        {
                            foreach (var c in _circles)
                            {
                                c.Value.Dispose();
                            }
                        }
                    }
 
                    if (oked) _dwg.Editor.Regen();
                }
                else
                {
                    _dwg.Editor.WriteMessage(
                        "\nNo selected CogoPoint has its label visible!");
                }    
            }
        }
 
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var opt = new JigPromptPointOptions(
                "\nMove to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.Cursor = CursorType.RubberBand;
 
            var res = prompts.AcquirePoint(opt);
            if (res.Status== PromptStatus.OK)
            {
                if (res.Value==_prevPoint)
                {
                    return SamplerStatus.NoChange;
                }
                else
                {
                    _currPoint = res.Value;
                    var mt = Matrix3d.Displacement(_prevPoint.GetVectorTo(_currPoint));
 
                    //==========================================================
                    // Tried this code in conjunction with code in WorldDraw()
                    // but did not work properly
                    //==========================================================
                    //foreach (var ent in _points)
                    //{
                    //    var cogo = (CivilDb.CogoPoint)ent;
                    //    if (cogo.IsLabelVisible)
                    //    {
                    //        var lblPt = new Point3d(
                    //            cogo.LabelLocation.X,
                    //            cogo.LabelLocation.Y,
                    //            cogo.LabelLocation.Z).TransformBy(mt);
 
                    //        cogo.LabelLocation = lblPt;
                    //    }
                    //}
 
                    foreach (var item in _circles)
                    {
                        ((Entity)item.Value).TransformBy(mt);
                    }
 
                    _prevPoint = _currPoint;
 
                    return SamplerStatus.OK;
                }
            }
            else
            {
                return SamplerStatus.Cancel;
            }
        }
 
        protected override bool WorldDraw(WorldDraw draw)
        {
            //==============================================
            // Tried this, in conjunction with the code 
            // commented out in Sampler() implementing
            //==============================================
            //foreach (var ent in _points)
            //{
            //    draw.Geometry.Draw(ent);
            //}
 
            foreach (var c in _circles)
            {
                draw.Geometry.Draw(c.Value);
            }
            
            return true;
        }
 
        #region private methods
 
        private IEnumerable<ObjectId> SelectCogoPoints()
        {
            var opt = new PromptSelectionOptions();
            opt.AllowDuplicates = false;
            opt.MessageForAdding = "Select CogoPoint:";
            opt.MessageForRemoval = "CogoPoint removed:";
           
            var filter = new SelectionFilter(
                new TypedValue[] 
                {
                    new TypedValue((int)DxfCode.Start, "AECC_COGO_POINT")
                });
 
            var res = _dwg.Editor.GetSelection(opt, filter);
            if (res.Status== PromptStatus.OK)
            {
                return res.Value.GetObjectIds();
            }
            else
            {
                return null;
            }
        }
 
        private CadDb.Entity GetCogoPointFromId(
            ObjectId pointId, Transaction tran, bool highlight=true)
        {
            var pt = (CadDb.Entity)tran.GetObject(pointId, OpenMode.ForWrite);
            if (highlight)
            {
                pt.Highlight();
            }
            return pt;
        }
 
        private bool PickBasePoint(out Point3d pt)
        {
            pt = Point3d.Origin;
 
            var res = _dwg.Editor.GetPoint(
                "\nSelect base point for move:");
            if (res.Status== PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private void CreateDrawables(List<CadDb.Entity> points)
        {
            foreach (var pt in points)
            {
                var cogo = (CivilDb.CogoPoint)pt;
                if (cogo.IsLabelVisible)
                {
                    GetCircleCenterAndRadius(
                        cogo, out Point3d center, out double radius);
 
                    var c = new Circle();
                    c.Center = center;
                    c.Radius = radius;
                    c.ColorIndex = 2;
 
                    _circles.Add(pt.ObjectId, c);
                }
            }
        }
 
        private void GetCircleCenterAndRadius(
            CivilDb.CogoPoint cogo, out Point3d center, out double radius)
        {
            center = new Point3d(
                cogo.LabelLocation.X, 
                cogo.LabelLocation.Y, 
                0.0);
 
            bool dragged = cogo.IsLabelDragged;
 
            if (dragged) cogo.ResetLabelLocation();
 
            var ext = cogo.GeometricExtents;
            var w = Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
            var h= Math.Abs(ext.MaxPoint.Y - ext.MinPoint.Y);
            radius = Math.Max(w, h) / 5.0;
 
            if (dragged) cogo.LabelLocation = center;
        }
 
        #endregion
    }
}

Then the CommandClass:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(CogoLabelMovingJig.MyCommands))]
 
namespace CogoLabelMovingJig
{
    public class MyCommands 
    {
        [CommandMethod("JigLabel")]
        public static void RunCommandA()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var jig = new LabelMovingJig(dwg);
                jig.JigCogoLabels();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}

See this video clip for the code in action.

Due to the very tight time available, the code is far from working properly, for example, if the labels have been moved once with this Jig and I move them again, the labels would not relocate properly, indicating there is a bug in the code. But I do not have time at the moment to dig it out. Hopefully, the idea of moving multiple CogoPoints' labels in a "Jig Style" is conveyed.

Obviously, similar visual effect can be achieved by using TransientGraphics in conjunction with Editor.PointMonitor event handler.