Join The Dots – How To Create VB.NET AutoCAD® Tools

by Will on February 20, 2011

Been struggling to find time to post on here recently due to exams, work and planning a wedding… with a bit of a lull in between everything, now’s a good time to get back in the swing of it!

Join The Dots – what on earth am I talking about?! Its the name I’ve given to a script I’ve been planning on writing for a while. It is a relatively simple idea, so I am going to take the opportunity to do a step-by-step walkthrough for writing a new AutoCAD® tool.

Preliminary Design

The first thing, is to have an idea. This sounds obvious, but having a clear idea is very important. Writing code will produce specific and predictable results, so you need to know exactly what you want to achieve.

Join The Dots is going to be a tool that will take a series of points, and will draw a polyline through every point, representing the shortest path (hopefully!).

The first step is to think about the idea. I usually ask myself the question “how would you achieve this manually?”

If you didn’t join up the dots in a specific way, you’d no doubt end up with situations like this:

Longest Path

Longest Path

This is in fact the longest path. After thinking about it a bit, I thought that you could start off by finding the shortest distance between any two points. This could form a starting segment for our polyline, and then we could append vertices to the line based on their proximity to the start and end points.

Basically, find the shortest line:

Shortest Path Step 1

Shortest Path Step 1

Then, look for the next closest point to one of the endpoints of the line:

Shortest Path Step 2

Shortest Path Step 2

Repeat:

Shortest Path Step 3

Shortest Path Step 3

And we’ve found the shortest path. So, we have a plan for how to create the tool – time to do some techie stuff!

Setting Up The Project

In my experience thus far, VBA was a lot simpler for interacting directly with AutoCAD®. With .NET, we need to interact with the document database, which is a tad more complex, and involves a bit more code to achieve the same thing.

Ok, firstly we need to set up our project. I’ve already created a template for AutoCAD® applications, so I’ve selected that. Using .NET you will need to set up your project correctly – if you want an easy way to set things up, read through this very short tutorial on setting up a VB.NET template that works with AutoCAD®.

Coding The Design

Step 1 – Create a selection

Once we’ve set up the project, the first step in our application is to make a selection that we can use later. We want the user to be able to select the points that are to be used in the JoinTheDots command. The code below goes in your main subroutine – if you’re not sure how things glue together, don’t worry – the full code will be given at the end.

'Setup
Dim ed As Editor = Application.DocumentManager.MdiActiveDocument.Editor

'Declare our filter entries this way
Dim values() As TypedValue = {
New TypedValue(DxfCode.Start, "POINT")
}

'Create the filter using our values
Dim sfilter As New SelectionFilter(values)

'Set the selection options
Dim SelOpts As New PromptSelectionOptions()
SelOpts.MessageForAdding = "Select points to find the shortest path through"
SelOpts.AllowDuplicates = True

'Make the selection
Dim res As PromptSelectionResult = ed.GetSelection(SelOpts, sfilter)

'If the user did something other than make a selection (like pressing ESC), then abort
If Not res.Status = PromptStatus.OK Then Return

'Create a selection set based on the user's selection
Dim ss As Autodesk.AutoCAD®.EditorInput.SelectionSet = res.Value

'Create an array of object IDs from the selection set, so that we can loop through them easily
Dim idarray As ObjectId() = ss.GetObjectIds()

So firstly we’re creating the variable ed, which refers to an object known as the Editor. This has many useful functions that we will use later.

Next, we’re going to create a filter that we will use to limit what the user can select. After all, we’re trying to “Join The Dots”, so we only want the user to be able to select AutoCAD® POINTs.

The next step is to ask the user to make a selection – however in order to do that we need to set up some things first. We need to set some PromptSelectionOptions, such as the message we want to display to the user. After these settings have been initialised, we can use the SelOpts variable which contains these settings, and the sfilter variable which contains our filter, to invoke the GetSelection method of our Editor object. This will give the user the opportunity to select any objects on screen, and the result of the user’s actions will be stored in the res variable, which contains a PromptSelectionResult object.

We need to be mindful to any possible action that the user might take. For example, the user might press the Escape key. If this happens, we need to know, and do something else. This is where capturing the result in the res variable is useful. We’re able to tell using res.Status, what type of input the user gave us. If res.Status is “OK”, then we know that the user made a selection as expected. If anything else happened (in other words, if the status is NOT “OK”), then the user did not make a selection as required, and we should exit the subroutine through the Return statement.

Finally, if the user made a selection, we want to convert that into a format that is more useful to us – a selection set. Then, from the selection set, we will retrieve the ObjectIds of the objects that were selected. That way, we are able to retrieve and use the individual entities that were selected at some point later on in our code.

Step 2 – Using Transactions

Developing for AutoCAD® using .NET requires that we wrap any modifications to the document in transactions. To make changes to anything in the drawing we need to start a transaction, make changes to the drawing, add these changes to the transaction, commit the transaction, and then dispose of the transaction. This is perhaps a bit long winded, but is actually inherently has some neat perks. One example is that the AutoCAD® UNDO command will undo the whole transaction rather than individual changes. Another example, is that if there is an error at some point in your code (heaven forbid!!) the whole transaction will be voided, and you’re not left with a drawing that has been half messed with, and half the same as before.

But anyway, you need to use a transaction – and here’s how it’s done:

'Create a link to the active document's database
Dim db As Database = Application.DocumentManager.MdiActiveDocument.Database
'Create a transaction manager object
Dim tm As Autodesk.AutoCAD®.DatabaseServices.TransactionManager = db.TransactionManager
'Start a transaction
Dim myT As Transaction = tm.StartTransaction()

'DO STUFF HERE!

'Commit the transaction
myT.Commit()
'Dispose of our transaction
myT.Dispose()

So the first step is to create the variable db which refers to the database of the active document. Next, we create the variable tm, which refers to db.TransactionManager. Then, its just a case of invoking tm.StartTransaction, and storing the resultant Transaction object in the variable myT. Easy huh?

After “DOING STUFF!”, we simply Commit the transaction, then Dispose of it. Changes should then be visible in your drawing (assuming you did make changes!!).

Step 3 – Make Changes To The Drawing

'Create a coord array, the same size as the idarray
Dim points(idarray.GetUpperBound(0)) As DBPoint
'Populate the coord array with point2d objects representing the location of the DBPoint objects
Dim n As Long
For n = 0 To idarray.GetUpperBound(0)
points(n) = tm.GetObject(idarray(n), OpenMode.ForRead, True)
Next

'Find the shortest line formed by two coordinates, create a polyline representing this line, and
'remove the two coordinates from the coords array
Dim pl As Polyline = GetShortestLine(points)

Dim TimeToExit As Boolean = False
Do
TimeToExit = AppendPoint(pl, points)
Loop Until TimeToExit

'Add the polyline to modelspace
AddToModelSpace(db, myT, pl)

If you were thinking it were as simple as the code above – I’m sorry to disappoint you… This code calls a few other functions, so we’ll be looking at those seperately, but the abstract idea for “making changes to the drawing” is as follows.

  • Firstly, get a list of all the DBPoints. To do this, we’ll loop through all the ids in our idarray, and add each DBPoint object to a new array called points().
  • Now, find the two points that are closest together, and we will use those two points to create the first segment of our polyline. This is what the GetShortestLine function does.
  • Now we want to add the remaining points to the polyline, using the point that is closest to one of the ends of the polyline. We want to repeat this step until there are no points remaining.
  • Finally, we need to add the polyline (in the variable pl) to the transaction.

And that’s the high level process. Specific tasks like getting the shortest line, and appending a point the the polyline, are useful to separate from the main subroutine. This makes it much more readable – we can see the high level process, without getting confused with the detail.

Step 4 – Get the shortest line

This code isn’t as complicated as it looks – honest!

Private Function GetShortestLine(ByRef points() As DBPoint) As Polyline

Dim n As Long
Dim m As Long

'Info that needs to be captured
Dim basePoint As DBPoint
Dim endPoint As DBPoint
Dim shortestDist As Double = 1.0E+300 'A very very big number!

'Loop through every combination of point pairs, and find the pair with the shortest distance
For n = 0 To points.GetUpperBound(0)
For m = n + 1 To points.GetUpperBound(0)

Dim tmpDist As Double
tmpDist = points(n).Position.DistanceTo(points(m).Position)

'If this is the shortest distance so far, update
If tmpDist < shortestDist Then
shortestDist = tmpDist
basePoint = points(n)
endPoint = points(m)
End If

Next
Next

'Add the points forming the shortest distance to a new polyline
Dim pl As Polyline = New Polyline
pl.AddVertexAt(0, basePoint.Position.Convert2d(New Plane), 0, 0, 0)
pl.AddVertexAt(1, endPoint.Position.Convert2d(New Plane), 0, 0, 0)

'remove basePoint and endPoint from the array of points, so that they are not reused
RemovePoint(points, basePoint)
RemovePoint(points, endPoint)

Return pl
End Function

Basically, what this function does is iterates through every point, and measures the distance from that point to every other point. The shortest distance found will be remembered, along with the two points that form the shortest distance. The main mechanism at work here is a combination of two For Next loops. The first For Next loop iterates the value of n between 0 and the upper bound of points array. So if there are 10 points, n will loop between 0 and 9. The second For Next loop iterates the value of m between the value of n + 1, and the upper bound of the points array. So, in the case of n=0, m would loop from 1 to 9. This is so that we only make new comparisons – for example, we wouldn’t want to compare points(0) with points(0), as its the same point. So we start the inner loop at n+1. We want the combination of n and m to always be unique, and cover every combination. This is what the pair of For Next loops achieves.

After the For Next loops have completed, we have found the points that create the shortest distance. What we want to do now, is create a polyline based on these two points. What we do, is create a new polyline object, and simply add the vertices to the polyline, at the correct positions, i.e., at 0 for the basePoint, and 1 for the endPoint. This creates a polyline joining up the two points.

Finally, so that in future we do not use these points any more as they are already within our polyline, we want to remove them from the points() array. Here we are using another function to do that.

Step 5 – Remove Item From An Array in VB.NET

There is no native function for removing an item from an array in VB.NET, so we will have to create a function to do this ourselves. It’s a fairly simple idea:

Remove Item From Array

Remove Item From Array

So, we loop from 0 to the upper bound of the array, and when we get to the item number we want to remove, we simply overwrite it with the next value in the array, and keep overwriting each subsequent item from this point.

Here’s the code:

Private Function RemovePoint(ByRef points() As DBPoint, ByRef remPoint As DBPoint) As Boolean
Dim n As Long
Dim newUpperBound As Long = points.GetUpperBound(0) - 1
Dim pointFound As Boolean
'Iterate through the points array until the removePoint is found, then nudge points down the array
For n = 0 To newUpperBound
If Not pointFound Then
If points(n) Is remPoint Then
pointFound = True
End If
End If
If pointFound Then
points(n) = points(n + 1)
End If
Next
'Set the new size of the array, clipping off the last item of the array
ReDim Preserve points(newUpperBound)

If newUpperBound = -1 Then
Return True
Else
Return False
End If

End Function

The final stage in this code is to simply re-dimension the size of the array, to a length 1 shorter than before, thus clipping out the final value, which is no longer needed as it has been copied to the previous item in the array.

Finally, when the array upper bound has reached -1, there are no more items left in the array. In this situation we will Return a value of TRUE, which we will later use as a trigger to stop looking for more points.

Step 6 – Append points to polyline

The final stage of our changes to the drawing is to append points to our polyline in order of how close they are to the ends of our existing polyline. This works on a similar principle to the other subroutine – looping through the points looking for the nearest point, then adding the point to the polyline. This sub will add a single point to the polyline, and remove that point from the points() array.

Private Function AppendPoint(ByRef pl As Polyline, ByRef points() As DBPoint) As Boolean
Dim startPoint As Point2d = pl.StartPoint.Convert2d(New Plane)
Dim endPoint As Point2d = pl.EndPoint.Convert2d(New Plane)
Dim nearestPoint As String = ""
Dim shortestDist As Double = 1.0E+300 'A very very big number!
Dim remPoint As DBPoint

Dim n As Long
For n = 0 To points.GetUpperBound(0)

Dim targetPoint As Point2d
targetPoint = points(n).Position.Convert2d(New Plane)

Dim tmpDist As Double
tmpDist = startPoint.GetDistanceTo(targetPoint)
If tmpDist < shortestDist Then
shortestDist = tmpDist
nearestPoint = "startpoint"
remPoint = points(n)
End If

tmpDist = endPoint.GetDistanceTo(targetPoint)
If tmpDist < shortestDist Then
shortestDist = tmpDist
nearestPoint = "endpoint"
remPoint = points(n)
End If

Next

Select Case nearestPoint
Case "startpoint"
pl.AddVertexAt(0, remPoint.Position.Convert2d(New Plane), 0, 0, 0)
Case ("endpoint")
pl.AddVertexAt(pl.NumberOfVertices, remPoint.Position.Convert2d(New Plane), 0, 0, 0)
Case Else
Err.Raise(0, , "nearestPoint not set!")
End Select

Return RemovePoint(points, remPoint)

End Function

As noted earlier, RemovePoints will Return a TRUE value when there are no more points left in the array. This TRUE or FALSE signal is again passed back through this function, so that we are able to know if this was the last point added. See the Do – While loop, and the TimeToExit variable, in the code earlier on in this document.

Step 7 – Add the polyline to ModelSpace

The final subroutine is to simply add entities to ModelSpace, ensuring that we correctly add it to the transaction too:

Private Sub AddToModelSpace(ByVal db As Database, ByVal myT As Transaction, ByVal ent As Entity)

'Open the ModelSpace Block Table Record
Dim acBT As BlockTable = db.BlockTableId.GetObject(OpenMode.ForRead)
Dim BTR As BlockTableRecord
BTR = acBT(BlockTableRecord.ModelSpace).GetObject(OpenMode.ForWrite)

'Add the entity to the ModelSpace Block Table Record
BTR.AppendEntity(ent)

'Add the entity to the transaction
myT.AddNewlyCreatedDBObject(ent, True)

End Sub

Compiling The Project

If all has been coded correctly, you should now be able to save, and compile your project to a dll file! You can then use the NETLOAD command to load this dll into AutoCAD®, and invoke the command using whatever command name you specified before your main subroutine.

Join The Dots

Join The Dots

Source Files

I’ve included below a zip file containing all the source files for this project – just in case things went awry, you can hopefully find the problem. The command name I’ve used is JOINTHEDOTS.

jointhedots

Source Code

Also, below I’ve pasted ALL the source code for the project:

Imports Autodesk.AutoCAD®.DatabaseServices
Imports Autodesk.AutoCAD®.Runtime
Imports Autodesk.AutoCAD®.Geometry
Imports Autodesk.AutoCAD®.ApplicationServices
Imports Autodesk.AutoCAD®.EditorInput
Imports Autodesk.AutoCAD®.Colors

Public Class Class1

<CommandMethod("JoinTheDots")> _
Public Sub JoinTheDots()
'Setup
Dim ed As Editor = Application.DocumentManager.MdiActiveDocument.Editor

'Declare our filter entries this way
Dim values() As TypedValue = {
New TypedValue(DxfCode.Start, "POINT")
}

'Create the filter using our values
Dim sfilter As New SelectionFilter(values)

'Set the selection options
Dim SelOpts As New PromptSelectionOptions()
SelOpts.MessageForAdding = "Select points to find the shortest path through"
SelOpts.AllowDuplicates = True

'Make the selection
Dim res As PromptSelectionResult = ed.GetSelection(SelOpts, sfilter)

'If the user did something other than make a selection (like pressing ESC), then abort
If Not res.Status = PromptStatus.OK Then Return

'Create a selection set based on the user's selection
Dim ss As Autodesk.AutoCAD®.EditorInput.SelectionSet = res.Value

'Create an array of object IDs from the selection set, so that we can loop through them easily
Dim idarray As ObjectId() = ss.GetObjectIds()

'Create a link to the active document's database
Dim db As Database = Application.DocumentManager.MdiActiveDocument.Database
'Create a transaction manager object
Dim tm As Autodesk.AutoCAD®.DatabaseServices.TransactionManager = db.TransactionManager
'Start a transaction
Dim myT As Transaction = tm.StartTransaction()

'Create a coord array, the same size as the idarray
Dim points(idarray.GetUpperBound(0)) As DBPoint
'Populate the coord array with point2d objects representing the location of the DBPoint objects
Dim n As Long
For n = 0 To idarray.GetUpperBound(0)
points(n) = tm.GetObject(idarray(n), OpenMode.ForRead, True)
Next

'Find the shortest line formed by two coordinates, create a polyline representing this line, and
'remove the two coordinates from the coords array
Dim pl As Polyline = GetShortestLine(points)

Dim TimeToExit As Boolean = False
Do
TimeToExit = AppendPoint(pl, points)
Loop Until TimeToExit

'Add the polyline to modelspace
AddToModelSpace(db, myT, pl)

'Commit the transaction
myT.Commit()
'Dispose of our transaction
myT.Dispose()

End Sub

Private Function AppendPoint(ByRef pl As Polyline, ByRef points() As DBPoint) As Boolean
Dim startPoint As Point2d = pl.StartPoint.Convert2d(New Plane)
Dim endPoint As Point2d = pl.EndPoint.Convert2d(New Plane)
Dim nearestPoint As String = ""
Dim shortestDist As Double = 1.0E+300 'A very very big number!
Dim remPoint As DBPoint

Dim n As Long
For n = 0 To points.GetUpperBound(0)

Dim targetPoint As Point2d
targetPoint = points(n).Position.Convert2d(New Plane)

Dim tmpDist As Double
tmpDist = startPoint.GetDistanceTo(targetPoint)
If tmpDist < shortestDist Then
shortestDist = tmpDist
nearestPoint = "startpoint"
remPoint = points(n)
End If

tmpDist = endPoint.GetDistanceTo(targetPoint)
If tmpDist < shortestDist Then
shortestDist = tmpDist
nearestPoint = "endpoint"
remPoint = points(n)
End If

Next

Select Case nearestPoint
Case "startpoint"
pl.AddVertexAt(0, remPoint.Position.Convert2d(New Plane), 0, 0, 0)
Case ("endpoint")
pl.AddVertexAt(pl.NumberOfVertices, remPoint.Position.Convert2d(New Plane), 0, 0, 0)
Case Else
Err.Raise(0, , "nearestPoint not set!")
End Select

Return RemovePoint(points, remPoint)

End Function

Private Function GetShortestLine(ByRef points() As DBPoint) As Polyline

Dim n As Long
Dim m As Long

'Info that needs to be captured
Dim basePoint As DBPoint
Dim endPoint As DBPoint
Dim shortestDist As Double = 1.0E+300 'A very very big number!

'Loop through every combination of point pairs, and find the pair with the shortest distance
For n = 0 To points.GetUpperBound(0)
For m = n + 1 To points.GetUpperBound(0)

Dim tmpDist As Double
tmpDist = points(n).Position.DistanceTo(points(m).Position)

'If this is the shortest distance so far, update
If tmpDist < shortestDist Then
shortestDist = tmpDist
basePoint = points(n)
endPoint = points(m)
End If

Next
Next

'Add the points forming the shortest distance to a new polyline
Dim pl As Polyline = New Polyline
pl.AddVertexAt(0, basePoint.Position.Convert2d(New Plane), 0, 0, 0)
pl.AddVertexAt(1, endPoint.Position.Convert2d(New Plane), 0, 0, 0)

'remove basePoint and endPoint from the array of points, so that they are not reused
RemovePoint(points, basePoint)
RemovePoint(points, endPoint)

Return pl
End Function

Private Function RemovePoint(ByRef points() As DBPoint, ByRef remPoint As DBPoint) As Boolean
Dim n As Long
Dim newUpperBound As Long = points.GetUpperBound(0) - 1
Dim pointFound As Boolean
'Iterate through the points array until the removePoint is found, then nudge points down the array
For n = 0 To newUpperBound
If Not pointFound Then
If points(n) Is remPoint Then
pointFound = True
End If
End If
If pointFound Then
points(n) = points(n + 1)
End If
Next
'Set the new size of the array, clipping off the last item of the array
ReDim Preserve points(newUpperBound)

If newUpperBound = -1 Then
Return True
Else
Return False
End If

End Function

Private Sub AddToModelSpace(ByVal db As Database, ByVal myT As Transaction, ByVal ent As Entity)

'Open the ModelSpace Block Table Record
Dim acBT As BlockTable = db.BlockTableId.GetObject(OpenMode.ForRead)
Dim BTR As BlockTableRecord
BTR = acBT(BlockTableRecord.ModelSpace).GetObject(OpenMode.ForWrite)

'Add the entity to the ModelSpace Block Table Record
BTR.AppendEntity(ent)

'Add the entity to the transaction
myT.AddNewlyCreatedDBObject(ent, True)

End Sub

End Class

I hope this tutorial helped you get started with VB.NET in AutoCAD® – you’ll be developing your own tools in no time.

Also, don’t be disheartened if you find it difficult to get things going – its a learning curve, and you will get there. If you think I wrote all this code perfect first time, THINK AGAIN!!! It’s a process of trying things out, experimenting, and persisting.

Please subscribe to my blog if you found this helpful – my primary concern is giving great tips, tricks and tutorials. There is a lot more to come!

Regards,

Will

P.s. Seriously, subscribe!

Enter Your Mail Address

Be Sociable, Share!