Fitting AutoCAD text into a selected space using .NET

After the last post, where we saw how to get the centroid of a Region, today we're going to use that information to place some text inside a detected space.

To restate our process from last time:

  1. The user selects a point
  2. Call Editor.TraceBoundary() to determine the containing space
  3. Call Region.CreateFromCurves() with the resulting geometry
  4. Determine the centroid of the Region
  5. Check whether the centroid is actually inside the Region
  6. If it is, then generate some text to place (we could also have asked the user for this, of course)…
  7. … and then calculate the size of the text such that it fits entirely into the space

Here's an example of we're aiming for. We're placing the textual equivalent of consecutive integers in the space, for fun.

Labelled spaces

Here's the C# code that does this. The code defines a number of extension methods on various classes which I've left in a single static class contained in the same file as our command implementation.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.BoundaryRepresentation;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Generic;

 

namespace TextPlacement

{

  public static class Extensions

  {

    // Point3d extensions

 

    ///<summary>

    /// Projects the provided Point3d onto the specified coordinate system.

    ///</summary>

    ///<param name="ucs">The coordinate system to project onto.</param>

    ///<returns>A Point2d projection on the plane of the

    /// coordinate system.</returns>

 

    public static Point2d ProjectToUcs(this Point3d pt, CoordinateSystem3d ucs)

    {

      var pl = new Plane(ucs.Origin, ucs.Zaxis);

      return pl.ParameterOf(pt);

    }

 

    // DBText extensions

 

    ///<summary>

    /// Gets the bounds of a DBText object.

    ///</summary>

    ///<param name="fac">Optional multiplier to increase/reduce buffer.</param>

    ///<returns>A collection of points defining the text's extents.</returns>

 

    public static Point3dCollection ExtractBounds(

      this DBText txt, double fac = 1.0

    )

    {

      var pts = new Point3dCollection();

 

      if (txt.Bounds.HasValue && txt.Visible)

      {

        // Create a straight version of the text object

        // and copy across all the relevant properties

        // (stopped copying AlignmentPoint, as it would

        // sometimes cause an eNotApplicable error)

 

        // We'll create the text at the WCS origin

        // with no rotation, so it's easier to use its

        // extents

 

        var txt2 = new DBText();

        txt2.Normal = Vector3d.ZAxis;

        txt2.Position = Point3d.Origin;

 

        // Other properties are copied from the original

 

        txt2.TextString = txt.TextString;

        txt2.TextStyleId = txt.TextStyleId;

        txt2.LineWeight = txt.LineWeight;

        txt2.Thickness = txt2.Thickness;

        txt2.HorizontalMode = txt.HorizontalMode;

        txt2.VerticalMode = txt.VerticalMode;

        txt2.WidthFactor = txt.WidthFactor;

        txt2.Height = txt.Height;

        txt2.IsMirroredInX = txt2.IsMirroredInX;

        txt2.IsMirroredInY = txt2.IsMirroredInY;

        txt2.Oblique = txt.Oblique;

 

        // Get its bounds if it has them defined

        // (which it should, as the original did)

 

        if (txt2.Bounds.HasValue)

        {

          var maxPt = txt2.Bounds.Value.MaxPoint;

 

          // Only worry about this single case, for now

 

          Matrix3d mat = Matrix3d.Identity;

          if (txt.Justify == AttachmentPoint.MiddleCenter)

          {

            mat = Matrix3d.Displacement((Point3d.Origin - maxPt) * 0.5);

          }

 

          // Place all four corners of the bounding box

          // in an array

 

          double minX, minY, maxX, maxY;

          if (txt.Justify == AttachmentPoint.MiddleCenter)

          {

            minX = -maxPt.X * 0.5 * fac;

            maxX = maxPt.X * 0.5 * fac;

            minY = -maxPt.Y * 0.5 * fac;

            maxY = maxPt.Y * 0.5 * fac;

          }

          else

          {

            minX = 0;

            minY = 0;

            maxX = maxPt.X * fac;

            maxY = maxPt.Y * fac;

          }

          var bounds =

            new Point2d[] {

              new Point2d(minX, minY),

              new Point2d(minX, maxY),

              new Point2d(maxX, maxY),

              new Point2d(maxX, minY)

            };

 

          // We're going to get each point's WCS coordinates

          // using the plane the text is on

 

          var pl = new Plane(txt.Position, txt.Normal);

 

          // Rotate each point and add its WCS location to the

          // collection

 

          foreach (Point2d pt in bounds)

          {

            pts.Add(

              pl.EvaluatePoint(

                pt.RotateBy(txt.Rotation, Point2d.Origin)

              )

            );

          }

        }

      }

      return pts;

    }

 

    // Region extensions

 

    ///<summary>

    /// Returns whether a Region contains a Point3d.

    ///</summary>

    ///<param name="pt">A points to test against the Region.</param>

    ///<returns>A Boolean indicating whether the Region contains

    /// the point.</returns>

 

    public static bool ContainsPoint(this Region reg, Point3d pt)

    {

      using (var brep = new Brep(reg))

      {

        var pc = new PointContainment();

        using (var brepEnt = brep.GetPointContainment(pt, out pc))

        {

          return pc != PointContainment.Outside;

        }

      }

    }

 

    ///<summary>

    /// Returns whether a Region contains a set of Point3ds.

    ///</summary>

    ///<param name="pts">An array of points to test against the Region.</param>

    ///<returns>A Boolean indicating whether the Region contains

    /// all the points.</returns>

 

    public static bool ContainsPoints(this Region reg, Point3dCollection ptc)

    {

      var pts = new Point3d[ptc.Count];

      ptc.CopyTo(pts, 0);

      return reg.ContainsPoints(pts);

    }

 

    ///<summary>

    /// Returns whether a Region contains a set of Point3ds.

    ///</summary>

    ///<param name="pts">An array of points to test against the Region.</param>

    ///<returns>A Boolean indicating whether the Region contains

    /// all the points.</returns>

 

    public static bool ContainsPoints(this Region reg, Point3d[] pts)

    {

      using (var brep = new Brep(reg))

      {

        foreach (var pt in pts)

        {

          var pc = new PointContainment();

          using (var brepEnt = brep.GetPointContainment(pt, out pc))

          {

            if (pc == PointContainment.Outside)

              return false;

          }

        }

      }

      return true;

    }

 

    ///<summary>

    /// Get the centroid of a Region.

    ///</summary>

    ///<param name="cur">An optional curve used to define the region.</param>

    ///<returns>A nullable Point3d containing the centroid of the Region.</returns>

 

    public static Point3d? GetCentroid(this Region reg, Curve cur = null)

    {

      if (cur == null)

      {

        var idc = new DBObjectCollection();

        reg.Explode(idc);

        if (idc.Count == 0)

          return null;

 

        cur = idc[0] as Curve;

      }

 

      if (cur == null)

        return null;

 

      var cs = cur.GetPlane().GetCoordinateSystem();

      var o = cs.Origin;

      var x = cs.Xaxis;

      var y = cs.Yaxis;

 

      var a = reg.AreaProperties(ref o, ref x, ref y);

      var pl = new Plane(o, x, y);

      return pl.EvaluatePoint(a.Centroid);

    }

 

    // Database extensions

 

    ///<summary>

    /// Create a piece of text of a specified size at a specified location.

    ///</summary>

    ///<param name="norm">The normal to the text object.</param>

    ///<param name="pt">The position for the text.</param>

    ///<param name="conts">The contents of the text.</param>

    ///<param name="size">The size of the text.</param>

 

    public static void CreateText(

      this Database db, Vector3d norm, Point3d pt, string conts, double size

    )

    {

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var ms =

          tr.GetObject(

            SymbolUtilityServices.GetBlockModelSpaceId(db),

            OpenMode.ForWrite

          ) as BlockTableRecord;

 

        if (ms != null)

        {

          var txt = new DBText();

          txt.Normal = norm;

          txt.Position = pt;

          txt.Justify = AttachmentPoint.MiddleCenter;

          txt.AlignmentPoint = pt;

          txt.TextString = conts;

          txt.Height = size;

 

          var id = ms.AppendEntity(txt);

          tr.AddNewlyCreatedDBObject(txt, true);

 

          //tr.CreateBoundingBox(txt);

        }

 

        tr.Commit();

      }

    }

 

    // Transaction extensions

 

    ///<summary>

    /// Create a bounding rectangle around a piece of text (for debugging).

    ///</summary>

    ///<param name="txt">The text object around which to create a box.</param>

 

    public static void CreateBoundingBox(this Transaction tr, DBText txt)

    {

      var ms =

        tr.GetObject(

          SymbolUtilityServices.GetBlockModelSpaceId(txt.Database),

          OpenMode.ForWrite

        ) as BlockTableRecord;

 

      if (ms != null)

      {

        var corners = txt.ExtractBounds();

        if (corners.Count >= 4)

        {

          var doc = Application.DocumentManager.MdiActiveDocument;

          if (doc == null) return;

          var ed = doc.Editor;

          var ucs = ed.CurrentUserCoordinateSystem;

          var cs = ucs.CoordinateSystem3d;

 

          var pl = new Polyline(4);

          for (int i = 0; i < 4; i++)

          {

            pl.AddVertexAt(i, corners[i].ProjectToUcs(cs), 0, 0, 0);

          }

          pl.Closed = true;

          pl.TransformBy(ucs);

 

          ms.AppendEntity(pl);

          tr.AddNewlyCreatedDBObject(pl, true);

        }

      }

    }

 

    // Int extensions

 

    // Based on:

    // http://stackoverflow.com/questions/2729752/converting-numbers-in-to-words-c-sharp

 

    ///<summary>

    /// Return the description of an integer in string form.

    ///</summary>

    ///<returns>The words describing an integer

    /// e.g. "one hundred and twenty-eight."</returns>

 

    public static string ToWords(this int number)

    {

      if (number == 0)

        return "zero";

 

      if (number < 0)

        return "minus " + ToWords(Math.Abs(number));

 

      string words = "";

 

      if ((number / 1000000) > 0)

      {

        words += ToWords(number / 1000000) + " million ";

        number %= 1000000;

      }

 

      if ((number / 1000) > 0)

      {

        words += ToWords(number / 1000) + " thousand ";

        number %= 1000;

      }

 

      if ((number / 100) > 0)

      {

        words += ToWords(number / 100) + " hundred ";

        number %= 100;

      }

 

      if (number > 0)

      {

        if (words != "")

          words += "and ";

 

        var unitsMap =

          new[] {

            "zero", "one", "two", "three", "four", "five", "six", "seven",

            "eight", "nine", "ten", "eleven", "twelve", "thirteen",

            "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",

            "nineteen"

          };

        var tensMap =

          new[] {

            "zero", "ten", "twenty", "thirty", "forty", "fifty",

            "sixty", "seventy", "eighty", "ninety"

          };

 

        if (number < 20)

          words += unitsMap[number];

        else

        {

          words += tensMap[number / 10];

          if ((number % 10) > 0)

            words += "-" + unitsMap[number % 10];

        }

      }

 

      return words;

    }

  }

 

  public class Commands

  {

    private int _number = 1;

 

    [CommandMethod("LS")]

    public void LabelSpace()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null) return;

      var ed = doc.Editor;

 

      // Loop, creating labels in the selected space, until cancelled

 

      do

      {

        var ppr = ed.GetPoint("\nSelect point in boundary");

        if (ppr.Status != PromptStatus.OK)

          break;

 

        try

        {

          LabelSpace(doc, ppr.Value, _number);

          _number++;

        }

        catch (Autodesk.AutoCAD.Runtime.Exception)

        {

          ed.WriteMessage("\nUnable to label this space.");

        }

      }

      while (true);

    }

 

    // Our main operation: adding a label to a space

 

    private static void LabelSpace(Document doc, Point3d pt, int num)

    {

      var ed = doc.Editor;

      var db = doc.Database;

 

      // Start by tracing the boundary around the selected point

 

      var objs = ed.TraceBoundary(pt, false);

      if (objs == null || objs.Count == 0)

        return;

 

      // If we have some geometry, create a Region from it

 

      var regs = Region.CreateFromCurves(objs);

      if (regs == null || regs.Count == 0)

        return;

 

      // There should only be one Region, but you never know

 

      foreach (Region reg in regs)

      {

        // Get the Region's centroid: we'll use this for the text placement

 

        var cen = reg.GetCentroid(objs[0] as Curve);

 

        // If the centroid is contained in the Region (not always the case)

        // then we proceed

 

        if (cen.HasValue && reg.ContainsPoint(cen.Value))

        {

          // Get our number in words

 

          var txt = num.ToWords();

 

          // Establish the closest fit height for our text

 

          var size = FindTextSize(reg, cen.Value, txt);

 

          // If we didn't fail, create our text of this size

 

          if (size > 0)

            db.CreateText(reg.Normal, cen.Value, txt, size);

        }

        else

        {

          ed.WriteMessage("\nCenter of space is outside - skipping.");

        }

      }

    }

 

    // Helper function to find the size of text that fits into a particular

    // Region

 

    private static double FindTextSize(Region reg, Point3d pt, string text)

    {

      // Returning < 0 mean failure

 

      double lastGood = -1.0;

 

      // This factor must be > 1: if it's close to 1, we will iterate more

      // (but have a closer match)

 

      const double factor = 1.05;

 

      // We're using a temporary text object

 

      using (var txt = new DBText())

      {

        txt.Normal = reg.Normal;

        txt.Position = pt;

        txt.Justify = AttachmentPoint.MiddleCenter;

        txt.AlignmentPoint = pt;

        txt.TextString = text;

        txt.Height = 1.0;

 

        // Growing means we're working our way outwards

        // !Growing means we're working out way inwards

        // We'll only know which one we're doing when we pass/fail

        // the first time

 

        bool first = true;

        bool growing = true; // Setting a default to help the compiler

        do

        {

          // Add 10% so we have a bit of a buffer around the text

 

          if (reg.ContainsPoints(txt.ExtractBounds(1.1)))

          {

            // If we succeed first time, we grow the text

 

            if (first)

            {

              growing = true;

              first = false;

            }

 

            // When we're growing and we succeed, iterate by growing the text

 

            if (growing)

            {

              lastGood = txt.Height;

              txt.Height = txt.Height * factor;

            }

            else

            {

              // If we're not growing this will be the first time we succeed

 

              return txt.Height;

            }

          }

          else

          {

            // If we fail first time, we shrink the text

 

            if (first)

            {

              growing = false;

              first = false;

            }

 

            // When we're growing and we fail, return the previous good result

 

            if (growing)

            {

              return lastGood;

            }

            else

            {

              // If we're not growing, iterate by shrinking the text

              // (unless it gets too small, then we return failure)

 

              txt.Height = txt.Height / factor;

              if (txt.Height < Tolerance.Global.EqualPoint)

                return -1.0;

            }

          }

        }

        while (true);

      }

    }

  }

}

 

Here's the LS (LabelSpace) command in action:

Label space

Again, I see lots of potential applications for this one… in the next (related) post, I plan to extend the implementation to adjust the text size of text and block attributes, as per the original request from Alex. We'll see where it goes from there!

9 responses to “Fitting AutoCAD text into a selected space using .NET”

  1. Nice, especially the ContainsPoint method.

  2. hi,this is very nice. but I don't know why the text doesn't show until i press "ESC".

    thanks!

    1. Hi Matt,

      Which version of AutoCAD are you using it with? I was worried I'd posted an older version of the code by mistake, but just tried the posted code and see the behaviour in the video with AutoCAD 2016.

      Regards,

      Kean

  3. You are getting close to a program I did that makes a table of lot numbers and areas for a bunch of closed plines that may or may not have a lot number inside. Also, to make the closed plines, I did a program that takes any set of lines/arcs/plines and finds all the (non-nested) closed shapes formed. So you end up with an excess of closed shapes, and only the ones with lot numbers inside get put in the table. Saves hours and works in regular acad, no c3d parcels required. I bet you could do some slick things by looking for the closed shape around text, rather than requiring closed shapes to begin with. I did not know about Editor.TraceBoundary, holy cow.

  4. Hi Kean, i often read you blogs. but i'm not a programmer so i don't know how to use your codes in autocad. is the a posting how explain what i must do to use your codes

    best wishes

    Jörg

  5. Hi Jörg,

    I think I need to create another post showing this. Back in the day, I posted this:

    keanw.com/2006/07/getting_started.html

    But things have moved on, since: you can now use NuGet to install the AutoCAD .NET project references, for instance:

    keanw.com/2014/12/autocad-2015-apis-available-via-nuget.html

    keanw.com/2015/03/nuget-packages-now-available-for-autocad-2016.html

    I hope this gets you started, at least.

    Regards,

    Kean

  6. Hi Kean,
    thanks a lot for the links.
    I'm sure after reading the blog it will be no longer a miricle for me.

    by

  7. Hi Kean Walmsley,

    I have drawn Some lines in one layer. A layer name is called "SteelBoundary"

    All lines are connected. Now I want to find closest boundary only in this layer. How i can do. Some other lines also be there in other layers but I want to search only in "SteelBoundary" layers

    Thanks
    Nimesh

    1. Hi Nimesh,

      You will probably want to turn them off or freeze them (I assume this works - it should be asy for you to test).

      Regards,

      Kean

Leave a Reply to Parthiv Cancel reply

Your email address will not be published. Required fields are marked *