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.










6 comments:

David A. Prontnicki said...

Hi Norman,

I am working with your code and ran into a few issues.

Within the CreateDrawables:

Point3d is a type but is used as a variable
Name center does not exist in current context
invalid expression term 'double'
Name radius does not exist in current context

Thank you!

Norman Yuan said...

Well, because I used new C# 7.0 language feature (.NET4.6.1), I bet the problem you have is due to the conversion to VB.NET.

You can simply make sure the sub "GetCircelCenterAndRadiu()'s signature is like:

Private Sub GetCircleCenterAndRadius(
ByVal cogo As CivilDb.CogoPoint, _
ByRef center As Point3d, _
ByRef radius As Double)

....
End Sub

And then in the method CreateDrawable(...), you do:

For Each ....
If cogo.IsLabelVisiable Then

Dim center As Point3d = Point3d.Origin
Dim radius As Double = 0.0

GetCircleCenterAndRadius(cogo, center, radius)

....

End if
Next

Tom Blades said...

Hi Norman,

I'm trying to load this piece of coding into Civil 3D 2016 - and i cant seem to get it to work.

My process has been as follows:
Copy code into notepad++
save as .dll file
type 'netload' into acad command bar
select 'CogoLabelMovingJig.dll'

I then get the following error:

Command: NETLOAD Cannot load assembly. Error details: System.BadImageFormatException: Could not load file or assembly 'file:///C:\Users\tom.blades\Documents\Lisp\CogoLabelMovingJig.dll' or one of its dependencies. The module was expected to contain an assembly manifest.
File name: 'file:///C:\Users\tom.blades\Documents\Lisp\CogoLabelMovingJig.dll'
at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, RuntimeAssembly reqAssembly, StackCrawlMark& stackMark, IntPtr pPrivHostBinder, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.InternalLoadFrom(String assemblyFile, Evidence securityEvidence, Byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm, Boolean forIntrospection, Boolean suppressSecurityChecks, StackCrawlMark& stackMark)
at System.Reflection.Assembly.LoadFrom(String assemblyFile)
at Autodesk.AutoCAD.Runtime.ExtensionLoader.Load(String fileName)
at loadmgd()
WRN: Assembly binding logging is turned OFF.
To enable assembly bind failure logging, set the registry value [HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD) to 1.
Note: There is some performance penalty associated with assembly bind failure logging.
To turn this feature off, remove the registry value [HKLM\Software\Microsoft\Fusion!EnableLog].

Hoping you can be of assistance,

Kind Regards,
Tom Blades

James Maeding said...

Hi Norman,
I've been using transients a lot lately, but also in paperspace viewports. My transients redraw when the user zooms, via viewchanged callback. The tricky part is knowing both modelspace and viewport extents on screen, so you can do things like keep the circles the same size on screen. My working project is here:
https://forums.autodesk.com/t5/net/working-test-project-showing-transient-drawing-during-command/td-p/8107495
I am improving it but was able to solve how to track extents of both paperspace, and the modelspace of a viewport, for a locked viewport.
The two solutions included use the same code for acad and bricscad, so its a great simple example of how to do that, and how to handle a couple quirks between the two platforms.
thx

James Maeding said...

Tom, You must compile the code into a dll, using visual studio. The free ones are fine, no need to buy.
You need to get a hello world program running, then expand it with Norman's code, while also adding civil3d references.
Actually, maybe try my program mentioned above, its set up for acad 2018. .net has two learning curves though - how to set up a program for acad that does almost nothing, and then doing something worth doing.
thx

Kumar Ranjan said...

NICE BLOG
AUTOCAD TRAINING IN GURGAON

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.