Connecting points to curves by the shortest distance in AutoCAD using .NET – Part 2

In the last post we saw a simple command that connects a block with a curve via a line that starts at the insertion point and meets the curve at its closest point. In this post we're going to see how we can search the modelspace for the nearest curve and connect each block to that.

There are a few interesting techniques used in this post's code:

  • We use the dynamic keyword to count the block references for a particular block without starting a transaction (as we're still in the user input phase, at that point).
  • We're going to use Linq to query all the curves from the modelspace block table record.
  • For each block we're going to use Linq again to sort the list of curves based on their distance from the insertion point.

Here's the C# code that connects all blocks of a certain type to the nearest curve. The code could easily be changed to only search for certain types or curve – or only curves tagged with a certain bit of XData – but I've left that as an exercise for the reader.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using System.Linq;

 

namespace ConnectBlocksToCurves

{

  public static class Extensions

  {

    public static double DistanceFrom(this Curve c, Point3d pt)

    {

      return pt.DistanceTo(c.GetClosestPointTo(pt, false));

    }

 

    public static void ConnectPointToCurve(

      this Transaction tr, BlockTableRecord btr,

      Point3d pt, Curve c

    )

    {

      tr.DrawLine(btr, pt, c.GetClosestPointTo(pt, false));

    }

 

    public static void ConnectPointsToCurves(

      this Transaction tr, BlockTableRecord btr,

      Point3dCollection pts, ObjectIdCollection ids

    )

    {

      // Open all the curves and store them in an array

 

      var objs = new Curve[ids.Count];

      for (int i = 0; i < ids.Count; i++)

      {

        objs[i] = tr.GetObject(ids[i], OpenMode.ForRead) as Curve;

      }

 

      // For each point...

 

      foreach (Point3d pt in pts)

      {

        // ... we're going to sort our curves based on their distance from

        // said point

 

        var sorted =

          objs.OrderBy(

            c => c == null ? double.PositiveInfinity : c.DistanceFrom(pt)

          );

 

        // Then we're going to connect the point to the first curve in the

        // sorted list

 

        ConnectPointToCurve(tr, btr, pt, sorted.First<Curve>());

      }

    }

 

    public static void DrawLine(

      this Transaction tr, BlockTableRecord btr, Point3d pt1, Point3d pt2

    )

    {

      var ln = new Line(pt1, pt2);

      btr.AppendEntity(ln);

      tr.AddNewlyCreatedDBObject(ln, true);

    }

  }

 

  public class Commands

  {

    [CommandMethod("CBC")]

    public static void ConnectBlocksToCurves()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Get the block to connect

 

      var peo = new PromptEntityOptions("\nSelect the block");

      peo.SetRejectMessage("\nMust be a block.");

      peo.AddAllowedClass(typeof(BlockReference), false);

 

      var per = ed.GetEntity(peo);

      if (per.Status != PromptStatus.OK)

        return;

 

      // Use a dynamic object to count the blocks, rather than creating

      // a transaction etc.

 

      dynamic brId = per.ObjectId;

      ObjectIdCollection refIds =

        brId.BlockTableRecord.GetBlockReferenceIds(true, true);

 

      ed.WriteMessage("\nFound {0} references. ", refIds.Count);

 

      var pko = new PromptKeywordOptions("Connect them all?");

      pko.AllowNone = true;

      pko.Keywords.Add("Yes");

      pko.Keywords.Add("No");

      pko.Keywords.Default = "Yes";

 

      var pkr = ed.GetKeywords(pko);

      if (pkr.Status != PromptStatus.OK)

        return;

 

      // Use the same code path for a single object, just reset the collection

 

      if (pkr.StringResult != "Yes")

      {

        refIds.Clear();

        refIds.Add(brId);

      }

 

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

      {

        var btr =

          (BlockTableRecord)tr.GetObject(

            SymbolUtilityServices.GetBlockModelSpaceId(db),

            OpenMode.ForRead

          );

 

        // Collect all the Curves from the modelspace

 

        var curveIds =

          from ObjectId id in btr

          where id.ObjectClass.IsDerivedFrom(RXClass.GetClass(typeof(Curve)))

          select id;

 

        // Convert our enumerable to a ObjectIdCollection

 

        var ids = new ObjectIdCollection(curveIds.ToArray<ObjectId>());

 

        // Could also do this dynamically using...

 

        // var ptEnum = from dynamic id in refIds select (Point3d)id.Position;

        // pts = new Point3dCollection(ptEnum.ToArray<Point3d>());

 

        var pts = new Point3dCollection();

        foreach (ObjectId id in refIds)

        {

          var br = tr.GetObject(id, OpenMode.ForRead) as BlockReference;

          pts.Add(br.Position);

        }

 

        // Now we have our points and curves, connect them

 

        btr.UpgradeOpen();

        tr.ConnectPointsToCurves(btr, pts, ids);

 

        tr.Commit();

      }

    }

 

    [CommandMethod("B2C")]

    public static void Block2Curve()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Get the block to connect

 

      var peo = new PromptEntityOptions("\nSelect the block");

      peo.SetRejectMessage("\nMust be a block.");

      peo.AddAllowedClass(typeof(BlockReference), false);

 

      var per = ed.GetEntity(peo);

      if (per.Status != PromptStatus.OK)

        return;

 

      var brId = per.ObjectId;

 

      // Get the curve to connect it to

 

      var peo2 = new PromptEntityOptions("\nSelect the curve");

      peo2.SetRejectMessage("\nMust be a curve.");

      peo2.AddAllowedClass(typeof(Curve), false);

 

      var per2 = ed.GetEntity(peo2);

      if (per2.Status != PromptStatus.OK)

        return;

 

      var cId = per2.ObjectId;

 

      // Use a transaction for the geometry access and connection creation

 

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

      {

        var br = tr.GetObject(brId, OpenMode.ForRead) as BlockReference;

        var c = tr.GetObject(cId, OpenMode.ForRead) as Curve;

 

        if (br != null && c != null)

        {

          // We'll be writing to the modelspace

 

          var btr =

            (BlockTableRecord)tr.GetObject(

              SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForWrite

            );

 

          // Connect the point to the closest point on the curve

 

          tr.ConnectPointToCurve(btr, br.Position, c);

        }

        tr.Commit();

      }

    }

  }

}

 

Here's the updated code in action:

Connecting points to curves

I'm now on my way to Montreal for the Autodesk Technical Summit 2016. I'll be presenting a session about web-based VR, which will be interesting in light of the announcements made at Google I/O last week. Another topic I expect to post about in the coming days…

8 responses to “Connecting points to curves by the shortest distance in AutoCAD using .NET – Part 2”

  1. Sorry for my bad English.

    It appears that the line drawn between the block and the reference line is not the shortest distance.

    foreach (ObjectId id in refIds)
    {
    var br = tr.GetObject(id, OpenMode.ForRead) as BlockReference;
    pts.Add(br.Position);
    }

    Should not consider the geometry of the block? and if the block is in 3D?

    1. Kean Walmsley Avatar

      It absolutely doesn't take into account the geometry of the block (that was not in the original request). It's the shortest distance between the block's insertion point and the curve: you can, of course, look at the block's geometry, if you wish (there are posts that look at that kind of approach, too - let me know if you have trouble finding them).

      Kean

  2. Matthew Vincent Avatar
    Matthew Vincent

    Hi Kean,
    I've been having some trouble in trying to use 3D polylines as opposed to lines which I've posted over in the original thread on AutoCAD forums.
    If you had a few moments, would you mind having a look?
    Matt

    1. Kean Walmsley Avatar

      Hi Matt,

      Done!

      Cheers,

      Kean

  3. Hi Kean thanks for the sample this can be time saving using this

    Have someone convert this to VB.NET and want to share

    finish to try and did not go so well 🙂

    1
    1

  4. is it possible to share examples dwg files

  5. Hi Kean,
    Thanks for the wonderful new approach on solving this.
    Is there any way to find distance between two entities based on user selection?

    Thank you,
    Antony

    1. Kean Walmsley Avatar

      Hi Antony,

      Of course - that should be very possible. I'm not working with AutoCAD, these days, but I'm sure someone on the AutoCAD .NET forum will be able to help you modify the code, if you need it.

      Best,

      Kean

Leave a Reply to Matthew Vincent Cancel reply

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