Monday, July 9, 2018

Change Width of DBText/AttributeReference - Part Three: Real Time Change

In 2 previous posts (here and here), the situation I tried to deal with is to fit a string of text into a limited space, such as a table's field with given width.

Obviously, besides shrinking the text string's width by reducing its WidthFactor, user can also rephrase the word/characters of the text string to reduce the width while keeping the meaning of the text. This way, the text would be presented visually the same (i.e. in the same WidthFactor).

With DBText, since it can be edited in place, so, user can change the wording of the text and see if the text would fit into the space while typing. But the the case of AttributeReference, the user can only either edit it in attribute editing dialog box, or in Properties window. For the former, when the attribute in edited in the dialog box, the real change occurs with the typing, which is good, because you can see if the text's width exceeds out of the allowed space at real time. For the latter, use can only see the change when the editing is completed (the focus leaves the edited field in Properties window).

After doing previous 2 posts, I thought why not doing one more article on changing text/attribute at real time by giving user chances of either changing text string's wording, or WidthFactor. The other reason of writing once more on this topic is that I have not seen sample code on the net about real time change of text entity while typing.

Here is the code of CommandClass RealTimeTextEditor:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(RealTimeTextEdit.RealTimeTextEditor))]
 
namespace RealTimeTextEdit
{
    public class RealTimeTextEditor
    {
        private Document _dwg;
        private string _defaultTarget = "Text";
        
        public RealTimeTextEditor()
        {
            SelectedId = ObjectId.Null;
            OriginalWidthFactor = 1.0;
            OriginalText = "";
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
        }
 
        #region properties
 
        internal ObjectId SelectedId { private setget; }
        internal string OriginalText { private setget; }
        internal double OriginalWidthFactor { private setget; }
 
        #endregion
 
        #region public methods
 
        [CommandMethod("MyTextEdit")]
        public void EditText()
        {
            while(true)
            {
                var target = SelectEditTarget(_defaultTarget);
                if (!string.IsNullOrEmpty(target))
                {
                    _defaultTarget = target;
 
                    ObjectId textId = ObjectId.Null;
                    if (_defaultTarget=="Text")
                    {
                        textId = SelectTextEntity();
                    }
                    else
                    {
                        textId = SelectAttributeEntity();
                    }
 
                    if (!textId.IsNull)
                    {
                        GetTextInformation(textId);
                        DoEditWork();
                    }
                }
                else
                {
                    break;
                }
            }
 
            _dwg.Editor.WriteMessage("\n");
        }
 
        #endregion
 
        #region private methods
 
        private string SelectEditTarget(string defaultTarget)
        {
            var target = "";
 
            var opt = new PromptKeywordOptions(
                "\nEdit Text or Attribute?");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Text");
            opt.Keywords.Add("Attribute");
            opt.Keywords.Add("eXit");
            opt.Keywords.Default = defaultTarget;
 
            var res = _dwg.Editor.GetKeywords(opt);
            if (res.Status== PromptStatus.OK)
            {
                if (res.StringResult!="eXit")
                {
                    return res.StringResult;
                }
            }
 
            return target;
        }
 
        private ObjectId SelectTextEntity()
        {
            ObjectId textId = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a TEXT entity:");
            opt.SetRejectMessage(
                "\nInvalid selection: not a TEXT entity!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = _dwg.Editor.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                textId = res.ObjectId;
            }
 
            return textId;
        }
 
        private ObjectId SelectAttributeEntity()
        {
            ObjectId attId = ObjectId.Null;
 
            while (true)
            {
                var opt = new PromptNestedEntityOptions(
                    "\nSelect an Attribute in block:");
                opt.AllowNone = false;
 
                var res = _dwg.Editor.GetNestedEntity(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.ObjectId.ObjectClass.DxfName.ToUpper() == "ATTRIB")
                    {
                        attId = res.ObjectId;
                    }
                    else
                    {
                        _dwg.Editor.WriteMessage(
                            "\nInvalid selection: not an attribue in block!");
                    }
                }
                else
                {
                    break;
                }
 
                if (!attId.IsNull) break;
            }
 
            return attId;
        }
 
        private void GetTextInformation(ObjectId textId)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(textId, OpenMode.ForRead);
                OriginalText = txt.TextString;
                OriginalWidthFactor = txt.WidthFactor;
                SelectedId = textId;
                tran.Commit();
            }
        }
 
        private void DoEditWork()
        {
            using (var dlg = new dlgTextBox(this))
            {
                dlg.TextStringChanged += (o, e) =>
                  {
                      UpdateTextEntity(e.TextString);
                  };
 
                dlg.TextWidthFactorChanged += (o, e) =>
                {
                    UpdateTextEntity(e.WidthFactor);
                };
 
                dlg.Left = 50;
                dlg.Top = 50;
 
                var res = CadApp.ShowModalDialog(CadApp.MainWindow.Handle, dlg, false);
 
                if (res== System.Windows.Forms.DialogResult.Cancel)
                {
                    //restore the originals
                    UpdateTextEntity(OriginalText);
                    UpdateTextEntity(OriginalWidthFactor);
                }
            }
        }
 
        private void UpdateTextEntity(string textString)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(SelectedId, OpenMode.ForWrite);
                txt.TextString = textString;
                tran.Commit();
            }
            _dwg.Editor.UpdateScreen();
        }
 
        private void UpdateTextEntity(double widthFactor)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(SelectedId, OpenMode.ForWrite);
                txt.WidthFactor = widthFactor;
                tran.Commit();
            }
            _dwg.Editor.UpdateScreen();
        }
 
        #endregion
    }
}

I need UI as a modal dialog box where user can re-typing text string, or change WidthFactor. It looks like this:


Here the dialog box' code behind:

using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.DatabaseServices;
 
namespace RealTimeTextEdit
{
    public partial class dlgTextBox : Form
    {
        private RealTimeTextEditor _tool = null;
        private ObjectId _textId = ObjectId.Null;
        private bool _setText = false;
 
        internal dlgTextBox()
        {
            InitializeComponent();
        }
 
        internal dlgTextBox(RealTimeTextEditor tool):this()
        {
            _tool = tool;
        }
 
        internal event TextStringChangedEventHandler TextStringChanged;
        internal event TextWidthFactorChangedEventHandler TextWidthFactorChanged;
 
        internal bool Undo { private setget; }
 
        #region private methods
 
        private void ShowSelected()
        {
            if (!_tool.SelectedId.IsNull)
            {
                _setText = true;
                _textId = _tool.SelectedId;
                txtOld.Text = _tool.OriginalText;
                txtNew.Text = _tool.OriginalText;
                txtNew.Enabled = true;
                txtFactor.Text = _tool.OriginalWidthFactor.ToString("#0.00");
                udFactor.Value = Convert.ToDecimal(_tool.OriginalWidthFactor);
                btnUndo.Enabled = false;
                _setText = false;
            }
        }
 
        #endregion
 
        private void txtNew_TextChanged(object sender, EventArgs e)
        {
            if (_setText) return;
 
            btnUndo.Enabled = true;
            TextStringChanged?.Invoke(
                this, 
                new TextStringChangedEventArgs(txtNew.Text));
        }
 
        private void udFactor_ValueChanged(object sender, EventArgs e)
        {
            if (_setText) return;
 
            btnUndo.Enabled = true;
            TextWidthFactorChanged?.Invoke(
                this, 
                new TextWidthFactorChangedEventArgs(Convert.ToDouble(udFactor.Value)));
        }
 
        private void btnUndo_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;
        }
 
        private void btnClose_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.OK;
        }
 
        private void dlgTextBox_Load(object sender, EventArgs e)
        {
            ShowSelected();
        }
    }
 
    internal class TextStringChangedEventArgs : EventArgs
    {
        internal string TextString { private setget; }
        internal TextStringChangedEventArgs(string textString)
        {
            TextString = textString;
        }
    }
 
    internal delegate void TextStringChangedEventHandler(
        object sender, TextStringChangedEventArgs e);
 
    internal class TextWidthFactorChangedEventArgs : EventArgs
    {
        internal double WidthFactor { private setget; }
        internal TextWidthFactorChangedEventArgs(double widthFactor)
        {
            WidthFactor = widthFactor;
        }
    }
 
    internal delegate void TextWidthFactorChangedEventHandler(
        object sender, TextWidthFactorChangedEventArgs e);
}

As the code shows, the real-time change to the target DBText/AttributeReference is triggered by the UI firing event TextStringChanged and TextWidthFactorChanged. The event handlers (highlighted in red) make the actual change.

See this video that shows the code in action.

It is worth pointing out: in the CommandClass RealTimeTextEditor I use non-static method as the CommandMethod, which means for each Document this command is used, a class instance is created by AutoCAD, so that the user's previous choice of editing target (Text, or Attribute), which is asked at beginning of the command execution, is the default choice (default keyword in the GetKeyword() call). The video clip shows this effect in its last portion.

Note:
Most of my posts come with video clips that show how the code run in AutoCAD. Until this post, I have always used Jing from TechSmith with free video host at screencast.com (thanks for the free stuff!). However, TechSmith recently announced their policy change that from now on, they would only keep video clips for one year for free Jing version user. So, starting from this post, I stopped using free Jing to generate screen capture video. Instead, I use OBS Studio to record screen as MP4 files and store and share them from my Google drive. I'll try to collect all the video clips used in my posts into my Google drive and update the links in the old posts as soon as possible before they are removed from screencast.com (hopefully I can find enough time before the deadline in September :-().

Monday, June 25, 2018

Change Width of DBText/AttributeReference by Its WidthFactor - Part Two: Drag-Fitting

In previous post, I showed the code to automatically adjust DBText's width to fit into a given space by reducing DBText's WidthFactor. Then I thought why not give user another handy way to adjust DBText's width: simply drag the DBText entity to desired width, much similar to AutoCAD's built-in dragging to scale an entity (but in this case, only width of DBText entity is "scaled").

Now that I am talking about dragging entities, I could derive my process from Jig class, or just build my own "Jig" style process with Transient graphics. I decided to go with the latter.

Here is the code:

using System;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace FitAttributeWidth
{
    public class TextWidthDragger
    {
        private Document _dwg;
        private Editor _ed;
        private ObjectId _txtId = ObjectId.Null;
        private double _factor = 1.0;
        private double _width = 0.0;
        private DBText _textDrawable = null;
        private Point3d _basePoint;
 
        private TransientManager _tsManager = TransientManager.CurrentTransientManager;
 
 
        public TextWidthDragger()
        {
            
        }
 
        public void DragTextEntity()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
            _ed = _dwg.Editor;
 
            var entId = SelectDragTarget();
            if (entId.IsNull)
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            DragDBText(_dwg, entId);
        }
 
        public void DragDBText(Document dwg, ObjectId txtId)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
            _txtId = txtId;
 
            DoTextDrag();
        }
 
        #region private methods: select DBText or AttributeReference
 
        private ObjectId SelectDragTarget()
        {
            var entId = ObjectId.Null;
 
            var opt = new PromptKeywordOptions(
                "\nSelect a Text or an Attribute in block:");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Text");
            opt.Keywords.Add("Attribute");
            opt.Keywords.Default = "Text";
 
            var res = _ed.GetKeywords(opt);
            if (res.Status== PromptStatus.OK)
            {
                if (res.StringResult == "Text")
                    entId = SelectDBText();
                else
                    entId = SelectAttribute();
            }
 
            return entId;
        }
 
        private ObjectId SelectDBText()
        {
            var txtId = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a Text entity:");
            opt.SetRejectMessage("\nInvalid selection: not a text entity!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = _ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                txtId = res.ObjectId;
            }
 
            return txtId;
        }
 
        private ObjectId SelectAttribute()
        {
            var attId = ObjectId.Null;
 
            while (true)
            {
                var opt = new PromptNestedEntityOptions(
                    "\nSelect an Attribute in block:");
                opt.AllowNone = false;
 
                var res = _ed.GetNestedEntity(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.ObjectId.ObjectClass.DxfName.ToUpper() == "ATTRIB")
                    {
                        attId = res.ObjectId;
                    }
                    else
                    {
                        _ed.WriteMessage(
                            "\nInvalid selection: not an attribue in block!");
                    }
                }
                else
                {
                    break;
                }
 
                if (!attId.IsNull) break;
            }
 
            return attId;
        }
 
        #endregion
 
        #region private methods
 
        private void DoTextDrag()
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var target = (DBText)tran.GetObject(_txtId, OpenMode.ForRead);
                _factor = target.WidthFactor;
                _width = GetTextWidth(target) / _factor;
                _basePoint = target.Position;
 
                bool dragOk = false;
                try
                {
                    _textDrawable = (DBText)target.Clone();
                    _textDrawable.ColorIndex = 2;
 
                    _ed.PointMonitor += Editor_PointMonitor;
 
                    var dist = GetDistance();
                    if (dist!=double.MinValue)
                    {
                        _factor = dist / _width;
                        target.UpgradeOpen();
                        target.WidthFactor = _factor;
                    }
                }
                finally
                {
                    ClearTransients();
                    _textDrawable.Dispose();
                    _ed.PointMonitor -= Editor_PointMonitor;
                }
 
                if (dragOk && _factor!=target.WidthFactor)
                {
                    target.UpgradeOpen();
                    target.WidthFactor = _factor;
                }
 
                tran.Commit();
            }
        }
 
        private double GetTextWidth(DBText txt)
        {
            var ext = txt.GeometricExtents;
            return Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
        }
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransients();
 
            var dist = e.Context.RawPoint.DistanceTo(_basePoint);
            _factor = dist / _width;
 
            e.AppendToolTipText($"Width scale factor: {_factor}");
 
            _textDrawable.WidthFactor = _factor;
 
            AddTransients();
        }
 
        private void AddTransients()
        {
            _tsManager.AddTransient(
                _textDrawable, 
                TransientDrawingMode.Highlight, 
                128, 
                new IntegerCollection());
        }
 
        private void ClearTransients()
        {
            _tsManager.EraseTransient(_textDrawable, new IntegerCollection());
        }
 
        private double GetDistance()
        {
            var opt = new PromptDistanceOptions(
                "\nSet text entity width scale factor:");
            opt.AllowZero = false;
            opt.AllowNegative = false;
            opt.AllowArbitraryInput = false;
            opt.AllowNone = false;
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.UseDashedLine = true;
 
            var res = _ed.GetDistance(opt);
            if (res.Status == PromptStatus.OK)
                return res.Value;
            else
                return double.MinValue;
        }
 
        #endregion
    }
}

Here is the command to run it:

[CommandMethod("DragTextWidth")]
public static void DragTextWidth()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    try
    {
        var dragger = new TextWidthDragger();
        dragger.DragTextEntity();
    }
    catch (System.Exception ex)
    {
        ed.WriteMessage("\nError:\n{0}.", ex.Message);
    }
    finally
    {
        Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
    }
}

Again, as mentioned in previous post, for simplicity, when calculate entity's width I assume the DBText/AttributeReference entity is placed horizontally. The code also tries to show the DBText's WidthFactor dynamically as tooltip when the DBText entity is dragged. However, since the tooltip thing is handled in PointMonitor event in the simplest way, the tooltip only shows when mouse cursor stays not moving. It might be possible to use the same or similar approach showed in this post to make a good tooltip to show changing WidthFactor while dragging. Again, for the simplicity of this post, I decide not spend time on this for now.

See this video clip showing how the code works.


Change Width of DBText/AttributeReference by Its WidthFactor - Part One: Auto-Fitting

When a text entity (DBText) is added int drawing, very often there is only limited space available to place the text entity (mainly the space would limit the width of the text entity). In the case when text entity is created by AutoCAD user, if the text entity's width exceeds the allowed space, user can either change the text entity's text string by using different words/characters without losing the text string's meaning, or change the text entity's WidthFactor (assuming the text entity's style and height remain) so that the characters of the text string are rendered narrower than normal.

Changing text entity's overall width with its WidthFactor from Properties window is an easy and straightforward "try-and-see" process. However, in the case of attribute in a block (AttributeReference, which is derived from DBText, hence its overall widht can also be changed with its WidthFactor), it would be not that easy/straightforward to set its WidthFactor via Properties window.

Furthermore, as AutoCAD programmers, we often programmatically set text string of DBText or AttributeReference entities. In this case, we usually do not have the freedom to change the words/characters of the text string to reduce the overall width of the entities. Therefore, it would be natural to use code to automatically set WidthFactor to a proper value so that that the width of an DBText entity would fit a given space. For example, many drawing title blocks/borders have fields or tables to be filled with either DBTexts, or AttributeReferences programmatically. It would be good the application is able to adjust the width of DBText/AttributeReference to fit the text string in given space (width of the field or table column).

Assume I am this situation: I need to use code to fill out a table with data from project database as text, or block's attribute. The text values could be too long to fit into the table's column width from time to time. So, as long as I know the width of the columns/fields, I want to shrink the text/attribute entity's width by change its WidthFactor automatically, so that user does not have to examine the table being updated by my application visually for the unfit texts.

The code to to the "auto-fit" work is really simple, shown below, as an extension method to ObjectId:

public static class TextWidthExtension
{
    public static bool FitTextWidth(
        this ObjectId entId, double width, double minWidthFactor=0.5)
    {
        var dfxName = entId.ObjectClass.DxfName.ToUpper();
        if (dfxName!="ATTRIB" && dfxName!="TEXT")
        {
            throw new ArgumentException(
                "Entity must be either DBText, or AttributeReference!");
        }
 
        bool fit = false;
 
        using (var tran = entId.Database.TransactionManager.StartTransaction())
        {
            var txtEnt = (DBText)tran.GetObject(entId, OpenMode.ForWrite);
            fit = FitToWidth(txtEnt, width, minWidthFactor);
            if (fit)
                tran.Commit();
            else
                tran.Abort();
        }
 
        return fit;
    }
 
    private static bool FitToWidth(DBText txt, double maxWidth, double minWidFactor)
    {
        var txtW = GetTextWidth(txt);
        bool fit = true;
        double factor = txt.WidthFactor;
 
        while (txtW > maxWidth && factor >= minWidFactor)
        {
            fit = false;
            factor -= 0.05;
            txt.WidthFactor = factor;
            txtW = GetTextWidth(txt);
        }
 
        fit = txtW < maxWidth;
 
        return fit;
    }
 
    private static double GetTextWidth(DBText txt)
    {
        var ext = txt.GeometricExtents;
        return Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
    }
}

This method takes an optional input to indicate the minimum value of WidthFactor of an DBText. In reality, it would make little sense to set WidthFactor value too small. Also, in the shown code, for simplicity, I calculate the width of a DBText entity with its GeometricExtents, assuming the DBText entity is horizontal (its Rotation=0.0). I could have also made the decrement of the WdithFactor in the "while{...}" loop a input parameter of the method instead of hard-coded as "0.05".

Here is the command to run the code:

using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FitAttributeWidth.MyCommands))]
 
namespace FitAttributeWidth
{
    public class MyCommands
    {
        [CommandMethod("FitAtts")]
        public static void FitAttributeToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var attIds = GetBlockAttributeIds(ed);
                if (attIds.Count()>0)
                {
                    foreach (var id in attIds)
                    {
                        id.FitTextWidth(60.0, 0.7);
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("FitText")]
        public static void FitTextToSpace()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                while(true)
                {
                    var txtId = SelectText(ed);
                    if (!txtId.IsNull)
                    {
                        txtId.FitTextWidth(60.0, 0.7);
                    }
                    else
                    {
                        break;
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError:\n{0}.", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        #region private methods
 
        private static IEnumerable<ObjectId> GetBlockAttributeIds(Editor ed)
        {
            var lst = new List<ObjectId>();
 
            var opt = new PromptEntityOptions(
                "\nSelect the block for fitting its attributes:");
            opt.SetRejectMessage("\nInvalid selection: not a block.");
            opt.AddAllowedClass(typeof(BlockReference), true);
            var res = ed.GetEntity(opt);
            if (res.Status==PromptStatus.OK)
            {
                using (var tran = res.ObjectId.Database.TransactionManager.StartTransaction())
                {
                    var bref = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                    if (bref.AttributeCollection.Count>0)
                    {
                        lst.AddRange(bref.AttributeCollection.Cast<ObjectId>());
                    }
                    tran.Commit();
                }
            }
 
            return lst;
        }
 
        private static ObjectId SelectText(Editor ed)
        {
            ObjectId id = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a Text entity in the table:");
            opt.SetRejectMessage("\nInvalid: not a DBText!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                id = res.ObjectId;
            }
 
            return id;
        }
 
        #endregion
    }
}

This video clip shows the result of code execution.



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.










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.