Detecting and erasing zero-length lines in an AutoCAD drawing using .NET

A question came in by email, yesterday, and despite having a few mini-projects in progress that were spawned during last week's event in Prague, I couldn't resist putting some code together to address it.

The developer wanted to purge zero-length geometry: the simplest way to solve the problem is to run "_-PURGE _Z" at the command-line, but I saw this more as an opportunity to create a simple helper that would loop through all the entities in a drawing and erase any meeting a specified condition. In this case it would be curves with a length less than the global tolerance, but the same mechanism could certainly be used to erase geometry meeting other conditions, instead.

Erasing zero-length lines

Here's some C# code that does this:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System;

 

namespace ZeroLengthEntities

{

  public static class Extensions

  {

    /// <summary>

    /// Erases entities found in this database that fulfill a condition.

    /// </summary>

    /// <param name="f">Function indicating whether an enity needs erasing.</param>

    /// <param name="countOnly">Optional parameter to count but not erase.</param>

    /// <returns>The number of entities found (and - if !countOnly - erased).</returns>

 

    public static int ConditionalErase(

      this Database db, Func<Entity, bool> f, bool countOnly = false

    )

    {

      int count = 0;

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

      {

        var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);

        foreach (ObjectId btrId in bt)

        {

          var btr = (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);

          foreach (ObjectId entId in btr)

          {

            var ent = tr.GetObject(entId, OpenMode.ForRead) as Entity;

            if (ent != null && f(ent))

            {

              if (!countOnly)

              {

                ent.UpgradeOpen();

                ent.Erase();

                ent.DowngradeOpen();

              }

              count++;

            }

          }

        }

        tr.Commit();

      }

      return count;

    }

  }

 

  public class Commands

  {

    [CommandMethod("EZL")]


0;   public void EraseZeroLength()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var db = doc.Database;     

      var ed = doc.Editor;

 

      var count = db.ConditionalErase(

        e =>

        {

          var cur = e as Curve;

          return

            cur != null &&

            cur.GetDistanceAtParameter(cur.EndParam) <

              Tolerance.Global.EqualPoint;

        }

      );

 

      ed.WriteMessage("\nErased {0} entities.", count);

    }

  }

}

 

The code just loops through each of the blocks in the drawing's block table and checks its contents against a condition specified by a lambda function. The function takes one argument – an entity – and returns a Boolean indicating whether it deserves to be deleted or gets to live on.

After looking at the code for a few minutes, I realised it probably makes more sense to generalise the extension method even further, having it apply a generic operation to every entity in the drawing. This is the version we'll see in the next post, for comparison.

3 responses to “Detecting and erasing zero-length lines in an AutoCAD drawing using .NET”

  1. Great idea to pass a predicate into ConditionalErase.

    Mulling this over, I think IsZeroLength would actually be a great extension method to have directly on curve entities:

    public static bool IsZeroLength(this Curve curve)
    {
    return curve.GetDistanceAtParameter(curve.EndParam) <
    Tolerance.Global.EqualPoint;
    }

    So since this is an extension on Curve and not Entity, it moves the burden of type checking into the ConditionalErase method instead of directly in the lambda. This could be mitigated with a generic approach:

    public static int ConditionalErase<T>(this Database db, Func<T, bool> f, bool countOnly = false)
    where T : Entity
    {
    ...
    var ent = tr.GetObject(entId, OpenMode.ForRead) as Entity;
    if (ent != null && ent is T && f((T)ent))
    {
    ...
    }
    ...
    }

    So then your ConditionalErase call becomes a little clearer on the outside:
    var count = db.ConditionalErase<Curve>(c => c.IsZeroLength());

    I don't like the 'Is' followed by a cast to T in ConditionalErase, however I think a LINQ expression can be used to filter the EntityIds down to the type you want, which solves the double checking.

    1. Looks like our where constraint on T in this case as an Entity eliminates the possibility of T being a value type, and will allow the 'as' keyword. In the simple scratch code I typed up I didn't have this constraint. So this makes the insides of ConditionalErase (EraseIf also has a nice ring) a little cleaner:

      var ent = tr.GetObject(entId, OpenMode.ForRead) as T;
      if (ent != null && f(ent)
      {
      // erase...
      }

      1. Thanks for the input, Parrish!

        See today's post for the way I ended up going with this...

        Kean

Leave a Reply to Parrish Husband Cancel reply

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