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.

Friday, October 20, 2017

Validate Polyline (LighWeightPolyline) As Polygon - Updated

In our CAD designing/drafting practice we often Auto CAD Civil3D/AutoCAD Map to draw polygons with Polyine (LwPolyline) to present as an area on land. When exchange data with other business process (ofter GIS business), we often need to export the closed polylines as GIS polygons (export as shapes, for example). Thus, it is required that the closed polylines generated by our CAD users cannot have duplicate vertices, nor they can be self-intersecting.

AutoCAD Map comes with tool to check/fix polyline's duplicate vertices, self-intersecting, as polygon. However, if we develop our own CAD application that needs to do such check, we'll need our own code to do it. 

To determine if a polyline has duplicate vertices is fairly simple. I can loop through the polyline's vertices from  the first one and compare its position with the next vertex' position (for the last vertex, compare it with the first one, regardless the polyline is closed or not). If the distance of the 2 points is 0.0, or better yet, smaller than a given tolerance, then the polyline has duplicate vertices.

As for determine if the polyline is self-intersecting, originally I thought it would be tricky to code. Back to a few years ago, ADN's Philippe Leefsma posted an article on this topic. But it was one of the comment to that article, made by "Cincir" (very late thanks to you!) provided a much simpler solution. Here I simply materialize it in actual .NET code, for anyone to use, if interested in.

Here is the code:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace ValidatePolyline
{
    public enum PolygonValidateResult
    {
        OK = 0,
        DuplicateVertices = 1,
        SelfIntersection = 2,
        BothDuplicateAndIntersection = 3,
    }
 
    public static class PolylineValidation
    {
        private const double TOLERANCE = 0.001;
 
        public static PolygonValidateResult IsValidPolygon(this Polyline pline)
        {
            var result = PolygonValidateResult.OK;
 
            var t = new Tolerance(TOLERANCE, TOLERANCE);
            using (var curve1 = pline.GetGeCurve(t))
            {
                using (var curve2 = pline.GetGeCurve(t))
                {
                    using (var curveInter = new CurveCurveIntersector3d(
                        curve1, curve2, pline.Normal, t))
                    {
                        if (curveInter.NumberOfIntersectionPoints != pline.NumberOfVertices)
                        {
                            int overlaps = curveInter.OverlapCount();
 
                            if (curveInter.NumberOfIntersectionPoints < pline.NumberOfVertices)
                                result = PolygonValidateResult.DuplicateVertices;
                            else
                                result = overlaps == pline.NumberOfVertices ?
                                    PolygonValidateResult.SelfIntersection :
                                    PolygonValidateResult.BothDuplicateAndIntersection;
                        }
                    }
                }
            }
 
            return result;
        }
    }
}

As you can see, the code is very simple to use, as an extension method of Polyline class. Here is the code to use it:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ValidatePolyline.MyCommands))]
 
namespace ValidatePolyline
{
    public class MyCommands
    {
        [CommandMethod("TestPoly")]
        public static void RunDocCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var polyId = SelectPolyline(ed);
                if (!polyId.IsNull)
                {
                    TestPolyline(polyId, ed);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        private static ObjectId SelectPolyline(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nPick a polyline:");
            opt.SetRejectMessage("\nNot a polyline!");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void TestPolyline(ObjectId polyId, Editor ed)
        {
            var res = PolygonValidateResult.OK;
            using (var tran = polyId.Database.TransactionManager.StartTransaction())
            {
                var poly = (Polyline)tran.GetObject(polyId, OpenMode.ForRead);
 
                res = poly.IsValidPolygon();
 
                tran.Commit();
            }
 
            string msg = string.Format(
                "\nSelected polyline status: {0}", res.ToString());
 
            ed.WriteMessage(msg);
        }
    }
}

This video clip shows the result of running this code.

UPDATE

I probably posted prematurely without doing enough test run. Anyway, I update the code with a couple of change:

1. Since being "Closed" is the very basic requirement when treating a polyline as polygon, I modified the code to report back if the polyline is close.

2. This also leads to the change of PolygonValidationResult enum type, so that Bitwise result can come out from the test return value to show all the comination of invalid states of a polygon.

Here is the update:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace ValidatePolyline
{
    public enum PolygonValidateResult
    {
        OK = 0,
        NotClosed = 1,
        DuplicateVertices = 2,
        SelfIntersection = 4,
    }
 
    public static class PolylineValidationExtension
    {
        private const double TOLERANCE = 0.001;
 
        public static PolygonValidateResult IsValidPolygon(this Polyline pline)
        {
            var result = PolygonValidateResult.OK;
 
            if (!pline.Closed)
            {
                result += 1;
            }
 
            var t = new Tolerance(TOLERANCE, TOLERANCE);
            using (var curve1 = pline.GetGeCurve(t))
            {
                using (var curve2 = pline.GetGeCurve(t))
                {
                    using (var curveInter = new CurveCurveIntersector3d(
                        curve1, curve2, pline.Normal, t))
                    {
                        int interCount = curveInter.NumberOfIntersectionPoints;
                        int overlaps = curveInter.OverlapCount();
                        if (!pline.Closed) overlaps += 1;
 
                        if (overlaps < pline.NumberOfVertices)
                        {
                            result += 2;
                        }
 
                        if (interCount > overlaps)
                        {
                            result += 4;
                        }
                    }
                }
            }
 
            return result;
        }
 
        public static PolygonValidateResult IsValidPolygon(this ObjectId polyId)
        {
            var result = PolygonValidateResult.OK;
 
            if (polyId.ObjectClass.DxfName.ToUpper() != "LWPOLYLINE")
            {
                throw new ArgumentException("Not a Lightweight Polyline!");
            }
 
            using (var tran = polyId.Database.TransactionManager.StartTransaction())
            {
                var poly = (Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                result = poly.IsValidPolygon();
                tran.Commit();
            }
 
            return result;
        }
 
        public static string ToResultString(this PolygonValidateResult res)
        {
            string msg = "";
            if (res == PolygonValidateResult.OK)
            {
                msg = "valid polyline.";
            }
            else
            {
                if ((res & PolygonValidateResult.NotClosed) == PolygonValidateResult.NotClosed)
                {
                    msg = msg + "Polyline is not closed";
                }
 
                if ((res & PolygonValidateResult.DuplicateVertices) == PolygonValidateResult.DuplicateVertices)
                {
                    if (msg.Length > 0) msg = msg + "; ";
                    msg = msg + "Polyline has duplicate vertices";
                }
 
                if ((res & PolygonValidateResult.SelfIntersection) == PolygonValidateResult.SelfIntersection)
                {
                    if (msg.Length > 0) msg = msg + "; ";
                    msg = msg + "Polyline is self-intersecting";
                }
            }
 
            return msg;
        }
    }
}

and the command to run a test:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ValidatePolyline.MyCommands))]
 
namespace ValidatePolyline
{
    public class MyCommands
    {
        [CommandMethod("TestPoly")]
        public static void RunDocCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var polyId = SelectPolyline(ed);
                if (!polyId.IsNull)
                {
                    TestPolyline(polyId, ed);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        private static ObjectId SelectPolyline(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nPick a polyline:");
            opt.SetRejectMessage("\nNot a polyline!");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void TestPolyline(ObjectId polyId, Editor ed)
        {
            var res = polyId.IsValidPolygon();
            ed.WriteMessage("\nTest Result: " + res.ToResultString());
        }
    }
}


Tuesday, July 11, 2017

Using Rotation Parameter/Action in Conjunction with Point Parameter/Stretch Action of Dynamic Block

In my recent development work, I ran into an interesting issue: in a particular type of drawing, a dynamic block as label is used to annotate a long Polyline, which could the center line of pipe, road, ditch...This video clip shows how the label block behaves. As the video shows, the block's insertion point is at its end of leader. and its label portion can be dragged and rotated freely without affecting the label block's pointing location (its insertion point). The reason of its label portion being rotatable is because that the label blocks are placed in ModelSpace, but when drawing contents are presented in PaperSpace layout, the Viewports opened in layout could be twisted in different angles, thus the need to rotate the dynamic block's label portion so that the label appears horizontally in Viewports. See pictures below.

As CAD programmer, we obviously can help CAD users in this situation, so that they do not have to manually rotate the labels in all Viewports with different twisted angles; instead we can write some code to get it done easily.

Here is a short video clip that shows 2 dynamic blocks being used as label: one a block that can be dragged to rotate as whole; the other is a label with a leader (with leader pointer as the block's insertion point), so that the label portion can be dragged to different position and rotated with the leader pointer remains in original insertion point.

This picture shows how the label blocks are inserted into ModelSpace:


And this picture shows how the label blocks look like in a Viewport with twisted angle in paperSpace layout:


It would be ideal that the labels seen via the Viewport are all rotated so that its text information is presented horizontally. This is where the rotation parameter/action in the dynamic block comes into play. Of course, for the label that does not have a pointing leader, we can simply rotate the block reference itself instead of adding a rotation dynamic property. But here I just want to use it in comparison to the dynamic block that has its rotation part at the end of a pointing leader - there is something tricky to handle. Read on.

Firstly, I need to know which blocks in drawing are label blocks, and the dynamic property names I need to set. Here is a class to provide this information:
using Autodesk.AutoCAD.Geometry;
using System.Collections.Generic;
 
namespace LabelRotation
{
    public class LabelBlockConfiguration
    {
        public static Dictionary<stringPoint2d> GetLabelBlockConfigurations()
        {
            var dic = new Dictionary<stringPoint2d>();
 
            dic.Add("MYTAG"Point2d.Origin);
            dic.Add("MYLABEL"new Point2d(0.0, 10.0));
 
            return dic;
        }
 
        public const string LABEL_POSITION_PROP = "LabelPosition";
        public const string LABEL_ANGLE_PROP = "LabelAngle";
        public const string TAG_ANGLE_PROP = "TagAngle";
    }
}

The data in this class tells me:

1. There are 2 blocks ("MYTAG" and "MYLABEL") used as label;
2. Each block has a dynamic property named as "xxxxAngle", used to set the label's rotation;
3. Block "MYLABEL" also has a dynamic property names as "LabelPosition". Since the this dynamic property is a Point parameter/Stretch action, the actually property name should be "LabelPosition X" and LabelPosition Y" respectively.
4. Each block name is associated with a Point2d value, which is the original value of "LabelPosition" property (for the Point parameter/Stretch action). Why do we need to know this value? We'll see it later that how it is critical to rotate label correctly if the label's rotation part is stretch-able as previous video clip shows.

Here is the code with a command defined to ease CAD users from having to manually rotate the labels according to the twist angle of Viewport:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(LabelRotation.Commands))]
 
namespace LabelRotation
{
    public class Commands
    {
        private Dictionary<stringPoint2d> _labelBlockConfigs = 
            LabelBlockConfiguration.GetLabelBlockConfigurations();
 
        [CommandMethod("SetLabel"CommandFlags.NoTileMode)]
        public void SetLabelRotationByViewport()
        {
            var doc = CadApp.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;
 
            ed.SwitchToPaperSpace();
 
            try
            {
                var lblIds = GetLabelBlocksInModelSpace(doc);
                double twistAngle;
                if (GetTwistAngleOfViewportOnLayout(ed, out twistAngle))
                {
                    RotateLabels(doc, lblIds, twistAngle);
                }
                else
                {
                    ed.WriteMessage("\n*Cancel*");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError\n{0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        #region private methods
 
        private IEnumerable<ObjectId> GetLabelBlocksInModelSpace(Document dwg)
        {
            var lblIds = new List<ObjectId>();
 
            var filterVals = new TypedValue[]
            {
                new TypedValue((int)DxfCode.Start, "INSERT"),
                new TypedValue((int)DxfCode.LayoutName, "MODEL")
            };
 
            var res = dwg.Editor.SelectAll(new SelectionFilter(filterVals));
            if (res.Status == PromptStatus.OK)
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    foreach (var id in res.Value.GetObjectIds())
                    {
                        var blk = (BlockReference)tran.GetObject(
                            id, OpenMode.ForRead);
                        var blkName = blk.Name;
                        if (blk.IsDynamicBlock)
                        {
                            var br = (BlockTableRecord)tran.GetObject(
                                blk.DynamicBlockTableRecord, OpenMode.ForRead);
                            blkName = br.Name;
                        }
 
                        if (IsTargetLabelBlock(blkName))
                        {
                            lblIds.Add(id);
                        }
                    }
 
                    tran.Commit();
                }
            }
 
            return lblIds;
        }
 
        private bool IsTargetLabelBlock(string blkName)
        {
            foreach (var item in _labelBlockConfigs)
            {
                if (item.Key.ToUpper() == blkName.ToUpper()) return true;
            }
            return false;
        }
 
        private bool GetTwistAngleOfViewportOnLayout(
            Editor ed, out double twistAngle)
        {
            twistAngle = 0.0;
 
            ObjectId vportId = SelectViewport(ed);
 
            if (!vportId.IsNull)
            {
                using (var tran = 
                    vportId.Database.TransactionManager.StartTransaction())
                {
                    var vport = (Viewport)tran.GetObject(
                        vportId, OpenMode.ForRead);
                    twistAngle = vport.TwistAngle;
                    tran.Commit();
                }
 
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private ObjectId SelectViewport(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nSelect viewport:");
            opt.SetRejectMessage("\nInvalid: not a Viewport!");
            opt.AddAllowedClass(typeof(Viewport), true);
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private void RotateLabels(
            Document dwg, IEnumerable<ObjectId> lblIds, double twistAngle)
        {
            double angle = 0.0 - twistAngle;
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                foreach (var labelId in lblIds)
                {
                    var blk = (BlockReference)tran.GetObject(
                        labelId, OpenMode.ForWrite);
                    foreach (DynamicBlockReferenceProperty prop in
                        blk.DynamicBlockReferencePropertyCollection)
                    {
                        if (prop.PropertyName.ToUpper() ==
                            LabelBlockConfiguration.LABEL_ANGLE_PROP.ToUpper() ||
                            prop.PropertyName.ToUpper() ==
                            LabelBlockConfiguration.TAG_ANGLE_PROP.ToUpper())
                        {
                            prop.Value = angle;
                            break;
                        }
                    }
                }
                tran.Commit();
            }
        }
 
        #endregion
    }
}

Now, from this video clip we can see the result of the code execution, which is as expected: all the labels have been rotated into horizontal position according to the twist angle of the Viewport. Well... until this happens: if the stretch-able label portion of the dynamic block has been dragged before the code is executed. See this video clip. If you see the video clip closely, you should be able to see in the "Properties" window that even I do not rotate the label (the "LabelAngle" dynamic property value is not changed initially), simply dragging the label (thus the "LabelPosition X/Y" dynamic property value changes) makes the "LabelAngle" property change its value. That is why the code works only with the label blocks with its original Stretch action position, but not with the labels that were dragged prior to the code execution.

While I now know the Rotation parameter of the dynamic block somehow changes when being used in conjunction with Point parameter/Stretch action, figuring out what the Rotation property value is due to the stretch looks like difficult thing to do. But without knowing the initial rotation angle of a stretched label, my code simple does not work as expected. I was struggling to get my code work in this scenario for a while and a bell suddenly rang in my mind: I could try to save the current Stretch action's position (the property "LabelPosition X/Y", and then reset the property back to its original value (as the dynamic block definition defines); then I rotate the label (setting "LabelAngle" property according to Viewport's twist angle); after the rotation, I restore the label's Stretch action position. So, I modified the code in private method RotateLabels(), as following:
private void RotateLabels(
    Document dwg, IEnumerable<ObjectId> lblIds, double twistAngle)
{
    double angle = 0.0 - twistAngle;
 
    using (var tran = dwg.TransactionManager.StartTransaction())
    {
        foreach (var labelId in lblIds)
        {
            var blk = (BlockReference)tran.GetObject(
                labelId, OpenMode.ForWrite);
 
            var blkName = blk.Name;
            if (blk.IsDynamicBlock)
            {
                var br = (BlockTableRecord)tran.GetObject(
                    blk.DynamicBlockTableRecord, OpenMode.ForRead);
                blkName = br.Name;
            }
 
 
            Point2d originalPosition = Point2d.Origin;
            if (_labelBlockConfigs.ContainsKey(blkName.ToUpper()))
            {
                originalPosition = _labelBlockConfigs[blkName.ToUpper()];
 
                // Since dynamic property value from linear or point 
                // parameteris affected by block insertion scale, 
                // thus we need to multiply the scale.
                // Also, for the dynamic block, the block scale must 
                // be uniform scale
                double scale = blk.ScaleFactors.X;
                originalPosition = new Point2d(
                    originalPosition.X * scale, 
                    originalPosition.Y * scale);
            }
 
            //Save existing label position
            Point2d currentPosition = GetPositionProperty(blk);
 
            //Set label to its original position
            if (originalPosition!=currentPosition)
            {
                SetPositionProperty(blk, originalPosition);
            }
 
            //Set rotation
            SetRotationProperty(blk, angle);
 
            //Restore the label position
            if (originalPosition != currentPosition)
            {
                SetPositionProperty(blk, currentPosition);
            }
        }
        tran.Commit();
    }
}
 
private Point2d GetPositionProperty(BlockReference blk)
{
    double x = 0.0;
    double y = 0.0;
 
    foreach (DynamicBlockReferenceProperty prop in
                blk.DynamicBlockReferencePropertyCollection)
    {
        if (prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.LABEL_POSITION_PROP.ToUpper() + " X")
        {
            x = Convert.ToDouble(prop.Value);
        }
 
        if (prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.LABEL_POSITION_PROP.ToUpper() + " Y")
        {
            y = Convert.ToDouble(prop.Value);
        }
    }
 
    return new Point2d(x, y);
}
 
private void SetPositionProperty(BlockReference blk, Point2d position)
{
    foreach (DynamicBlockReferenceProperty prop in
                blk.DynamicBlockReferencePropertyCollection)
    {
        if (prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.LABEL_POSITION_PROP.ToUpper() + " X")
        {
            prop.Value = position.X;
        }
 
        if (prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.LABEL_POSITION_PROP.ToUpper() + " Y")
        {
            prop.Value = position.Y;
        }
    }
}
 
private void SetRotationProperty(BlockReference blk, double angle)
{
    foreach (DynamicBlockReferenceProperty prop in
                blk.DynamicBlockReferencePropertyCollection)
    {
        if (prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.LABEL_ANGLE_PROP.ToUpper() ||
            prop.PropertyName.ToUpper() ==
            LabelBlockConfiguration.TAG_ANGLE_PROP.ToUpper())
        {
            prop.Value = angle;
            break;
        }
    }
}

With this code change, this video clip shows the labels now are all rotated correctly whether they have been dragged from their original position or not prior to being rotated.

Some discussion:

1. Often there could be multiple Viewports to show different portion of ModelSpace content and the Viewports could have different twist angle. In this case, we obviously need to determine which labels are visible in which Viewport and then rotate them accordingly. In this article I omitted this scenario and simplified the case that all labels are seen in single twisted Viewport.

2. Since I somehow found the way to rotate the label correctly, I did not bother to figure out why/how the rotation property value changes due the dragging action. Because I need to rest the stretched position back to the original position (the position when the dynamic block is defined) before doing the rotation, that is why we need to associate a Point2d value with the block name as known label block information in class LabelBlockConfiguration.













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.