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.










Monday, January 8, 2018

Create Arc-Aligned Text with .NET API - 1 of ? (TBD)

AutoCAD's Express Tool comes with "ARCTEXT" command that creates text aligned along an arc. There are also a few LISP routines that create arc-aligned, or curve-aligned text (here is one of the best). The arc-text created by AutoCAD's Express Tool is a custom entity (non-standard AutoCAD entity), which means that if a drawing containing this type of arc-text is opened in an AutoCAD session without Express Tool loaded, these arc-texts would become proxy entity. This would not be a problem in quite later AutoCAD version, since Express Tool has long been part of AutoCAD installation (ctextapp.arx is the ARX tool that contains the command ARCTEXT).

I had a bit spare time during the past Christmas/New Year holiday, so I though it would be interesting to create arc-aligned text with .NET API. I did a quick search to see if someone has already done and it turned up empty. So, I spent some time on and off in past many days to give it a try (and took a few more days for me to write this article😓). I started with some pilot code to try out different ways of doing it, and finally refactored the code that seems work as expected and fairly smooth.

The process of doing it is like this:

1. obtain a text string to be drawn as arc-aligned text, usually from user input;
2. create a set non-database-residing DBText entity, each is a single character from the text string, including spaces;
3. obtain each DBText entity's size (width, height, ...);
4. obtain required arc information (center point, radius, start angle and end angle);
5. calculate each DBText object's location on the arc and move each DBText object to the calculated position;
6. add these DBText objects into database (current space) in a Transaction;
7. base on user option, either group there individual DBText objects into an anonymous group, or convert them into a named or anonymous block.

While I wrote the code, it appeared to me that there would be too much code to be covered in one post, thus the "1 of ?" in the title: I plan to follow this up with at least one more post to explore a "Jig" way to create arc-aligned text; and maybe another post for an Oeverrule.

Anyway, here is what I have now.

1. Data Classes

a. Class ArcText, which is used for holding a single DBText entity's information:

public class ArcText
{
    public DBText TextEntity { setget; }
    public double TextWidth { setget; }
    public double TextHeight { setget; }
    public Point3d BottomBasePoint { setget; }
    public Point3d MiddleBasePoint { setget; }
    public Point3d TopBasePoint { setget; }
}

In this class, the 3 "XxxxPoint" properties are actually no use in following code, so far. But I thought I may need them in later exploration.

b. Class ArcTextCollection, which is a list of ArcText objects based on a supplied text string:

public class ArcTextCollection : List<ArcText>, IDisposable 
{
    private double _spaceFactor = 1.1;
 
    public ArcTextCollection(
        string textString, double textHeight, double spaceFactor=1.1):base()
    {
        if (textString.Trim().Length==0)
        {
            throw new ArgumentException("Text string is empty");
        }
        _spaceFactor = spaceFactor;
        GenerateTextEntities(textString, textHeight);
    }
 
    #region public properties
 
    public double SpaceFactor
    {
        get { return _spaceFactor; }
    }
    public double OverAllTextHeight
    {
        get
        {
            if (Count == 0)
            {
                return 0.0;
            }
            else
            {
                double h = this[0].TextHeight;
                foreach (var txt in this)
                {
                    if (txt.TextHeight > h) h = txt.TextHeight;
                }
                return h;
            }
        }
    }
 
    public double OverAllTextWidth
    {
        get
        {
            if (Count == 0)
            {
                return 0.0;
            }
            else
            {
                double w = 0;
                foreach (var txt in this)
                {
                    w += txt.TextWidth * _spaceFactor;
                }
                return w;
            }
        }
    }
 
    #endregion
 
    #region IDisposable implemeting
 
    public void Dispose()
    {
        foreach (var txt in this)
        {
            if (txt.TextEntity == nullcontinue;
 
            if (!txt.TextEntity.IsDisposed)
            {
                txt.TextEntity.Dispose();
            }
        }
    }
 
    #endregion
 
    #region private method
 
    private void GenerateTextEntities(string textString, double textHeight)
    {
        int i = 0;
        while (i < textString.Length)
        {
            string txt = textString.Substring(i, 1);
 
            var textEnt = CreateDbText(txt, textHeight);
            ArcText singleText = GetSingleText(textEnt);
            Add(singleText);
 
            i++;
        }
            
        AdjustZeroWidthText();
 
        double txtH = OverAllTextHeight;
        foreach (var item in this)
        {
            var txt = item.TextEntity;
 
            Point3d bottomBase;
            Point3d topBase;
            double xOff;
            double yOff;
            GetBasePoints(
                txt, txtH, item.TextWidth, 
                out bottomBase, out topBase, out xOff, out yOff);
 
            item.BottomBasePoint = bottomBase;
            item.TopBasePoint = topBase;
            item.MiddleBasePoint = txt.AlignmentPoint;
        }
    }
 
    private DBText CreateDbText(string txt, double txtHeight)
    {
        var ent = new DBText();
        ent.TextString = txt;
        ent.SetDatabaseDefaults();
        ent.Height = txtHeight;
        ent.Position = Point3d.Origin;
        ent.Justify = AttachmentPoint.MiddleCenter;
        return ent;
    }
 
    private ArcText GetSingleText(DBText txtEnt)
    {
        Extents3d ext = txtEnt.GeometricExtents;
 
        double w = Math.Abs(ext.MaxPoint.X - ext.MinPoint.X);
        double h = Math.Abs(ext.MaxPoint.Y - ext.MinPoint.Y);
 
        return new ArcText
        {
            TextEntity = txtEnt,
            TextWidth = w * _spaceFactor,
            TextHeight = h
        };
    }
 
    private void AdjustZeroWidthText()
    {
        var maxW = 0.0;
        foreach (var txt in this)
        {
            if (txt.TextWidth > maxW) maxW = txt.TextWidth;
        }
 
        foreach (var txt in this)
        {
            if (txt.TextWidth == 0.0)
            {
                txt.TextWidth = maxW / 2.0;
            }
        }
    }
 
    private void GetBasePoints(
        DBText txt, double txtH, double txtW, 
        out Point3d bottomBase, out Point3d topBase, 
        out double xOffset, out double yOffset)
    {
        xOffset = txtW / 2.0;
        yOffset = txt.AlignmentPoint.Y - txt.Position.Y;
 
        bottomBase = new Point3d(
            txt.Position.X + txtW / 2.0, txt.Position.Y, txt.Position.Z);
        topBase = new Point3d(
            bottomBase.X, bottomBase.Y + txtH, bottomBase.Z);
    }

    #endregion
}

As the code shows, this class would generate a set of DBText entities against each characters in a supplied text string. The property "SpaceFactor" is used to keep each DBText from other with certain space with value 1.0 meaning no space between. Also, this class implements IDisposal interface to provide an easy way to make sure the DBText entities would be disposed, if they were not added to drawing database during arc-aligned text creating process (when user cancels the command, for example).

One may wonder what the method "AdjustZeroWidthText()" is for. Since I need to create DBText for each character in a text string, thus some of the DBText would have a "WhiteSpace" TextString. When a DBText's TextString is an empty string, or a white space, its GeometricExtents indicates its width is 0.0. So, in order to keep the required space between DBText objects, I use the 1/2 of the widest DBText object as the width of a "space" DBText object. thus the method "AdjustZeroWidthText()" method.

c. Class LayoutArc, which holds information of an arc the DBText objects would be aligned wih:

public class LayoutArc
{
    public Point3d Center { setget; }
    public double Radius { setget; }
    public double StartAngle { setget; }
    public double EndAngle { setget; }
}

2. A class of helper used for getting an arc to align text with,  CadHelper:

public static class CadHelper
{
    public static LayoutArc GetLayoutArc(this Editor editor)
    {
        LayoutArc arc = null;
 
        var opt = new PromptEntityOptions(
            "\nSelect an ARC entity:");
        opt.SetRejectMessage("\nInvalid: not an ARC.");
        opt.AddAllowedClass(typeof(Arc), true);
 
        var res = editor.GetEntity(opt);
        if (res.Status== PromptStatus.OK)
        {
            using (var tran = 
                res.ObjectId.Database.TransactionManager.StartTransaction())
            {
                var a = (Arc)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                arc = new LayoutArc
                {
                    Center = a.Center,
                    StartAngle = a.StartAngle,
                    EndAngle = a.EndAngle,
                    Radius = a.Radius
                };
 
                tran.Commit();
            }
 
            editor.WriteMessage("\n");
        }
        else
        {
            editor.WriteMessage("\n*Cancel*");
        }
 
        return arc;
    }
}

3. A Windows Form as dialog box UI to get user input:


The form's code:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace ArcTextV2
{
    public partial class dlgArcText : Form
    {
        public dlgArcText()
        {
            InitializeComponent();
        }
 
        public dlgArcText(IEnumerable<string> textStyles) : this()
        {
            foreach (var item in textStyles)
            {
                cboStyle.Items.Add(item);
            }
 
            if (cboStyle.Items.Count > 0) cboStyle.SelectedIndex = 0;
        }
 
        public string TextString
        {
            get { return txtString.Text.Trim(); }
            set { txtString.Text = value; }
        }
 
        public string TextStyle
        {
            get { return cboStyle.Text; }
            set
            {
                cboStyle.SelectedIndex = cboStyle.FindString(value);
            }
        }
 
        public double TextHeight
        {
            get { return double.Parse(txtHeight.Text); }
            set { txtHeight.Text = value.ToString(); }
        }
 
        public double SpaceFactor
        {
            get { return double.Parse(txtSpaceFactor.Text); }
            set { txtSpaceFactor.Text = value.ToString(); }
        }
 
        public bool ConvertToBlock
        {
            get { return rdoBlock.Checked; }
            set
            {
                rdoBlock.Checked = value;
                pnlBlock.Enabled = value;
            }
        }
 
        public bool UseAnonymousBlock
        {
            get { return rdoAnonymous.Checked; }
            set { rdoAnonymous.Checked = value; }
        }
 
        private void ValidateInputs()
        {
            bool valid = true;
            errInfo.Clear();
 
            if (txtString.Text.Trim().Length==0)
            {
                if (valid) valid = false;
                errInfo.SetError(txtString, "Text required!");
            }
 
            var good = ValidateDoubleInput(txtHeight);
            if (!good)
            {
                if (valid) valid = false;
            }
 
            good = ValidateDoubleInput(txtSpaceFactor);
            if (!good)
            {
                if (valid) valid = false;
            }
 
            btnOK.Enabled = valid;
        }
 
        private bool ValidateDoubleInput(TextBox txtBox)
        {
            if (txtBox.Text.Trim().Length==0)
            {
                errInfo.SetError(txtBox, "Input required!");
                return false;
            }
            else
            {
                try
                {
                    double d = double.Parse(txtBox.Text);
                }
                catch
                {
                    errInfo.SetError(txtBox, "Must be a numeric value!");
                    return false;
                }
            }
 
            return true;
        }
 
        private void txtString_TextChanged(object sender, EventArgs e)
        {
            ValidateInputs();
        }
 
        private void txtHeight_TextChanged(object sender, EventArgs e)
        {
            ValidateInputs();
        }
 
        private void txtSpaceFactor_TextChanged(object sender, EventArgs e)
        {
            ValidateInputs();
        }
 
        private void dlgArcText_Load(object sender, EventArgs e)
        {
            ValidateInputs();
        }
 
        private void rdoBlock_CheckedChanged(object sender, EventArgs e)
        {
            pnlBlock.Enabled = rdoBlock.Checked;
        }
    }
}

4. Class SimpleArcTextCreator that does the work of turning a user-inputted text string into arc-aligned text:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace ArcTextV2
{
    public class SimpleArcTextCreator
    {
        private const string BLOCK_NAME = "ArcText";
        private Document _dwg = null;
 
        private double _angleBase = 0.0;
        private bool _angleDirection = false;
 
        private ObjectId _styleId = ObjectId.Null;
        private LayoutArc _arc = null;
 
        private string _textString = "THIS IS ARC-TEXT";
        private double _textHeight = 1.0;
        private string _textStyle = "Sdandard";
        private double _spaceFactor = 1.1;
        private bool _asBlock = true;
        private bool _asAnonymousBlock = true;
 
        public SimpleArcTextCreator()
        {
            //To Do ...
        }
 
        public void CreateArcText(Document dwg)
        {
            _dwg = dwg;
 
            // Get text style list
            var styles = GetTextStyles(_dwg);
            _textStyle = "Standard";
 
            // get user inputs
            if (!GetUnserOptions(styles))
            {
                dwg.Editor.WriteMessage("\n*Cancel*");
                return;
            }
 
            _styleId = GetTextStyleId(_textStyle);
 
            // get arc information
            _arc = _dwg.Editor.GetLayoutArc();
            if (_arc == nullreturn;
 
            DrawArcTexts();
        }
 
        #region private methods: misc.
 
        private IEnumerable<string> GetTextStyles(Document dwg)
        {
            var lst = new List<string>();
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var tb = (TextStyleTable)tran.GetObject(dwg.Database.TextStyleTableId, OpenMode.ForRead);
                foreach (var id in tb)
                {
                    var style = (TextStyleTableRecord)tran.GetObject(id, OpenMode.ForRead);
                    lst.Add(style.Name);
                }
                tran.Commit();
            }
 
            return lst;
        }
 
        private bool GetUnserOptions(IEnumerable<string> styles)
        {
            bool oked = false;
            
            using (var dlg = new dlgArcText(styles))
            {
                dlg.TextString = _textString;
                dlg.TextHeight = _textHeight;
                dlg.TextStyle = _textStyle;
                dlg.SpaceFactor = _spaceFactor;
                dlg.ConvertToBlock = _asBlock;
                dlg.UseAnonymousBlock = _asAnonymousBlock;
 
                var res = CadApp.ShowModalDialog(CadApp.MainWindow.Handle, dlg, false);
                if (res == System.Windows.Forms.DialogResult.OK)
                {
                    _textString = dlg.TextString;
                    _textHeight = dlg.TextHeight;
                    _spaceFactor = dlg.SpaceFactor;
                    _textStyle = dlg.TextStyle;
                    _asBlock = dlg.ConvertToBlock;
                    _asAnonymousBlock = dlg.UseAnonymousBlock;
                    oked = true;
                }
            }
 
            return oked;
        }
 
        private ObjectId GetTextStyleId(string styleName)
        {
            var id = ObjectId.Null;
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var styleTable = (TextStyleTable)tran.GetObject(
                    _dwg.Database.TextStyleTableId, OpenMode.ForRead);
                if (styleTable.Has(styleName))
                {
                    id = styleTable[styleName];
                }
                tran.Commit();
            }
 
            if (id.IsNull)
            {
                id = _dwg.Database.Textstyle;
            }
 
            return id;
        }
 
        private void SaveAngleSysVariables()
        {
            _angleBase = _dwg.Database.Angbase;
            _dwg.Database.Angbase = 0.0;
 
            _angleDirection = _dwg.Database.Angdir;
            _dwg.Database.Angdir = false;
        }
 
        private void RestoreAngleSysVariable()
        {
            _dwg.Database.Angbase = _angleBase;
            _dwg.Database.Angdir = _angleDirection;
        }
 
        #endregion
 
        #region private methods: generate arc aligned texts
 
        private void DrawArcTexts()
        {
            try
            {
                SaveAngleSysVariables();
 
                var arcMidAngle = _arc.EndAngle > _arc.StartAngle ?
                    (_arc.StartAngle + _arc.EndAngle) / 2.0 :
                    (_arc.StartAngle + _arc.EndAngle + Math.PI * 2.0) / 2.0;
 
                List<ObjectId> textIds = new List<ObjectId>();
 
                using (var arcTexts = new ArcTextCollection(
                    _textString, _textHeight, _spaceFactor))
                {
                    var txtStartAngle = GetTextStartAngle(
                        _arc.Radius, arcTexts.OverAllTextWidth, arcMidAngle);
 
                    using (var tran = _dwg.TransactionManager.StartTransaction())
                    {
                        var space = (BlockTableRecord)tran.GetObject(
                            _dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                        for (int i = arcTexts.Count - 1; i >= 0; i--)
                        {
                            var txtEnt = arcTexts[i].TextEntity;
 
                            // Calculate DBText's position rotation on the arc
                            Point3d arcPoint;
                            double rotation;
                            double angle;
                            CalculateArcPoint(
                                arcTexts[i],
                                _arc.Center,
                                _arc.Radius,
                                txtStartAngle,
                                arcTexts.SpaceFactor,
                                out arcPoint,
                                out rotation,
                                out angle);
 
                            txtStartAngle += angle;
 
                            // Move onto the Arc
                            var mt = Matrix3d.Displacement(
                                arcTexts[i].MiddleBasePoint.GetVectorTo(arcPoint));
                            txtEnt.TransformBy(mt);
 
                            // Rotate text
                            txtEnt.Rotation = rotation;
 
                            txtEnt.TextStyleId = _styleId;
 
                            // Add the text entity into database
                            var id = space.AppendEntity(txtEnt);
                            tran.AddNewlyCreatedDBObject(txtEnt, true);
 
                            textIds.Add(id);
                        }
 
                        tran.Commit();
                    } 
                }
 
                if (textIds != null && textIds.Count() > 0)
                {
                    if (_asBlock)
                    {
                        ConvertToBlock(textIds, _arc.Center, _asAnonymousBlock);
                    }
                    else
                    {
                        AddToAnonymousGroup(textIds);
                    }
                } 
            }
            finally
            {
                RestoreAngleSysVariable();
            }    
        }
 
        private double GetTextStartAngle(
            double radius, double totalTextWidth, double arcMidAngle)
        {
            var totalTextAngle = totalTextWidth / radius;
            return arcMidAngle - totalTextAngle / 2.0;
        }
 
        private void CalculateArcPoint(
            ArcText arcText, Point3d arcCentre, 
            double radius, double startAngle, double spaceFactor,
            out Point3d arcPoint, out double textRotation, out double startAngleIncrement)
        {
            arcPoint = Point3d.Origin;
            textRotation = 0.0;
            startAngleIncrement = arcText.TextWidth * spaceFactor / radius;
 
            var angle = startAngle + startAngleIncrement / 2.0;
 
            var dx = radius * Math.Cos(angle);
            var dy = radius * Math.Sin(angle);
 
            arcPoint = new Point3d(arcCentre.X + dx, arcCentre.Y + dy, arcCentre.Z);
 
            if (angle < Math.PI / 2.0)
            {
                textRotation = Math.PI * 3.0 / 2.0 + angle;
            }
            else if (angle > Math.PI / 2.0)
            {
                textRotation = angle - Math.PI / 2.0;
            }
        }
 
        #endregion
 
        #region private methods: convert into a blockreference, or add to anonymous group
 
        private void ConvertToBlock(
            IEnumerable<ObjectId> textIds, Point3d arcCenter, bool anonymousBlock)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var blkTable = (BlockTable)tran.GetObject(
                    _dwg.Database.BlockTableId, OpenMode.ForWrite);
                
                string blkName = anonymousBlock ?
                    "*U" : GetBlockName(blkTable, tran);
 
                //Create block definition
                var br = new BlockTableRecord();
                br.Name = blkName;
 
                br.Origin = arcCenter;
 
                var blkId = blkTable.Add(br);
                tran.AddNewlyCreatedDBObject(br, true);
 
                var txtIds = new ObjectIdCollection(textIds.ToArray());
                br.AssumeOwnershipOf(txtIds);
 
                //Create block reference
                var bref = new BlockReference(_arc.Center, blkId);
                bref.SetDatabaseDefaults();
 
                var space = (BlockTableRecord)tran.GetObject(
                    _dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                space.AppendEntity(bref);
                tran.AddNewlyCreatedDBObject(bref, true);
 
                tran.Commit();
            }
        }
 
        private string GetBlockName(BlockTable br, Transaction tran)
        {
            RemoveUnreferencedArcTextBlock(br, tran);
 
            int i = 1;
            while (true)
            {
                string suffix;
                if (i < 1000)
                    suffix = i.ToString().PadLeft(4, '0');
                else
                    suffix = i.ToString().PadLeft(8, '0');
 
                var bName = BLOCK_NAME + suffix;
                if (!br.Has(bName)) return bName;
                i++;
            }
        }
 
        private void RemoveUnreferencedArcTextBlock(BlockTable bt, Transaction tran)
        {
            foreach (ObjectId bId in bt)
            {
                var blk = (BlockTableRecord)tran.GetObject(bId, OpenMode.ForWrite);
                if (blk.Name.StartsWith(BLOCK_NAME))
                {
                    var refIds = blk.GetBlockReferenceIds(truetrue);
                    if (refIds!=null && refIds.Count==0)
                    {
                        blk.Erase(true);
                    }
                }
            }
        }
 
        private void AddToAnonymousGroup(IEnumerable<ObjectId> textIds)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var groupDic = (DBDictionary)tran.GetObject(
                    _dwg.Database.GroupDictionaryId, OpenMode.ForWrite);
                var group = new Group("Anonymous group to hold arc-aligned texts"true);
 
                groupDic.SetAt("*", group);
 
                foreach(var id in textIds)
                {
                    group.Append(id);
                }
 
                tran.AddNewlyCreatedDBObject(group, true);
                tran.Commit();
            }
        }
 
        #endregion
    }
}

5. The CommandMethod to start the work:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ArcTextV2.Commands))]
 
namespace ArcTextV2
{
    public class Commands
    {
        private static SimpleArcTextCreator _arcTextTool = null;
        [CommandMethod("DoArcText")]
        public static void RunMyCommand()
        {
            var doc = CadApp.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;
 
            if (_arcTextTool == null) _arcTextTool = new SimpleArcTextCreator();
 
            try
            {
                _arcTextTool.CreateArcText(doc);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError\n{0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}

Well, that is a lot of code, but it is still only covers the very basic functionality to align a series of DBTexts along an arc.

See this video showing how the code works.

In order to make this arc-text tool useful, there are a lot extra options that are needed to provided to user, such as align text to the left/right end or center, text orientation (inward or outward against arc center)... I intend to enhance it to a "JIG" style tool, so that user can choose these options with ghost arc-text dynamically changing. It will covered in my next post. Stay tuned.

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.