Strings as Symbols

If we can interpret strings as symbols, we can store parameter data in spreadsheets.

The basic technique uses VBA statements Replace and Evaluate.

Sub test()
Dim A As Double, B As Double, C As Double, D As Double, R As Double

Dim str As String

str = "A + B * C + D"

A = 5
B = 3
C = 2
D = 3

str = Replace(str, "A", A)
str = Replace(str, "B", B)
str = Replace(str, "C", C)
str = Replace(str, "D", D)

R = Evaluate(str)

MsgBox R

End Sub

Lets use a simple polyline box to illustrate the all-in-code method versus the data-in-spreadsheet method.

If the box is A wide (in the x direction) and B tall (in the y direction), and the lower left corner is at 0,0, the points are (0,0) (A,0) (A,B) (0,B)

AddLightWeightPolyline will draw the box if the 8 data entries are given in an array in sequence. The CLOSED property of the polyline object is set to true to draw the line from the last point back to the first point.

In code
X0=0
X1=A
Y0=0
Y1=B

Dim pts as variant
Pts = Array (x0, y0, x1, y0, x1, y1, x0, y1)

The array named pts is then handed off to a sub that will convert it to a format AddLightWeightPolyline can handle.

For any complex shape a labeled drawing is necessary. Creating the array is a two-step process. x and y values are found starting from the origin in relation to the parameters A and B (and any others necessary). Then the x and y values are listed as needed in the array in the order that the polyline vertices require. At runtime the x and y values have their formulas evaluated and the results are stored in the array given to the polyline sub to be drawn.

To duplicate this with the data in a spreadsheet, the process has to be collapsed to a single step. We need to store the formulas in the order the polyline requires them, not evaluate them to x and y symbols first.

At first the added complexity of that was tedious. But eventually I learned to enter the formulas per the sketch just doing all the x values, in the order they are to be drawn, then do all the y values, similar to the code process. The benefits of putting the data into a spreadsheet are that we can have a whole sheet of parts, and we can manipulate the data and the finished part easily.

In standard database design, every table has a unique name with one column that has unique values called a primary key. The record (row) is found by finding the unique value in the key column. If we call the spreadsheet table SHAPE then the unique column will be called SHAPE_ID. We find the row data by the name of the shape we are looking for. For instance, the key column is column B, the shape we are looking for is “OSCF”, then

Set rng = ws1.Range(”B1:B30”).Find(”OSCF”)

Find has arguments. If the lookat:=xlWhole is not set, Find will search and find the substring, so that searching for OSCF will stop when OSCF1 is found. Also it is written that VBA saves its last search parameters, and uses them when there are no arguments to the contrary, which could lead to some pretty confounding bugs.

Set rng = ws1.Range(”B1:B30”).Find(”OSCF” , LookIn:=xlValues, lookat:=xlWhole)

in practice variables will be used for the range and search string

Set rng = ws1.Range(shape_key).Find(shape_ID, LookIn:=xlValues, lookat:=xlWhole)

If found, this will set the excel range object to the single cell that contains the found string.

If our data is extended out to the right, and there is a blank column after the last cell, then

First we move the range one cell to the right to pick up the first data cell

Set rng = rng.Offset(0, 1)

Then we extend the range all the way to the right, a variable number of columns depending on the individual record.

Set rng = ws1.Range(rng.Address, rng.End(xlToRight).Address)

Ws1 is a sheet reference.

Now we have the data, whatever it is, in a range object. In the cells are our formulas. Here are the values for OSCF,

A, B, and W are defined by the user at run time. But the strings A, B and W in the spreadsheet are not the symbols A, B and W in VBA.

The range is saved. We count the number of items in it and re-dimension the array to accept it.

icount = rng.Count
ReDim ar(1 To icount)

Now we can loop through the range. Each time through the loop we get the cell value in a string. Then we use VBA Replace to change the string values A, B, W to their symbols which at runtime have actual numerical values. Finally we use VBA Evaluate to do the math and load the result into the array. The finished array is an array of doubles and suitable for the AddLightWeightPollyLine method.


Sub shape_2(shape_ID As String, A As Double, B As Double, C As Double, D As Double, W As Double)
 Dim i As Integer, icount As Integer
 Dim str As String
 Dim rng As Range
 Dim plineobj As AcadLWPolyline
  
 Dim ar() As Double
 Dim shape_key As String
 
 Set ws1 = ThisWorkbook.Worksheets("Shape_Param")
 shape_key = "B1:B30"
 Set rng = ws1.Range(shape_key).Find(shape_ID, LookIn:=xlValues, lookat:=xlWhole)

 
 If rng Is Nothing Then
   MsgBox "no find shape_ID in column shape_key"
    Exit Sub
    End If
   
    'rng has one cell,  offset it one cell right to begin data
    Set rng = rng.Offset(0, 1)
    'extend the range to right to the end of the record
     Set rng = ws1.Range(rng.Address, rng.End(xlToRight).Address)
     'this also works but it seems like it shouldnt
     'because excel seems to infer the range default as address
     'Set rng = Range(rng, rng.End(xlToRight))
        icount = rng.Count
        ReDim ar(1 To icount)
    
    For i = 1 To icount
        str = rng.Cells(1, i)
        str = Replace(str, "A", A)
        str = Replace(str, "B", B)
        str = Replace(str, "C", C)
        str = Replace(str, "D", D)
        str = Replace(str, "W", W)
        
        ar(i) = Evaluate(str)
    Next i

  Set plineobj = acadDoc.ModelSpace.AddLightWeightPolyline(ar)
      plineobj.Closed = True
      plineobj.Update
      Set g_pline = plineobj

End Sub

The created polyline can be a global variable and the calling program can draw several shapes and move, rotate, mirror them into assemblies. For drawing sheet metal end sections, either the part can be drawn with a thickness, a closed polyline, or a single not-closed polyline. Thats the origin of my having two subs Shape_1 and Shape_2. They differ only by whether they close the polyline at the end. I preload 4 variables A,B,C,D, and W which I use for thickness, but if they are not needed, zeros can be passed.

Since this is a spreadsheet, not an actual database, our records can have a variable number of columns, we can skip lines, we can have sections of closed and not closed parts. The only requirement is that the key column contains the name of the part.

Different parts drawn with shape_ID and parameters.

Sub test2_sheet()
    Call init_draw
    Dim A As Double, B As Double, C As Double, D As Double, W As Double
    Dim shape_ID As String
    
    A = 2
    B = 4.25
    C = 0
    D = 0
    W = 0.0625
    
    shape_ID = "OSCF"
    
    ' shape_ID = "ISCF"
    ' shape_ID = "CAP"
    ' shape_ID = "FLOOR"
    'shape_ID = "FLAT"
    'shape_ID = "JCHAN"
    'shape_ID = "HANG"
     
    Call shape_2(shape_ID, A, B, C, D, W)
    
    pt1 = pt(4, 4, 0)
    g_pline.Move pt0, pt1
    
End Sub

Assemblies drawn using global polyline methods move, rotate, mirror


Sub test1_sheet()
    Call init_draw
 
    Call shape_2("box", 34, 0.0625, 0, 0, 0)
    
    Call shape_2("box", 34, 0.0625, 0, 0, 0)
    g_pline.Move pt0, pt(0, 3.9375, 0)
    
     Call shape_2("box", 1, 3.875, 0, 0, 0)
     g_pline.Move pt0, pt(0, 0.0625, 0)
    
    Call shape_2("box", 1, 3.875, 0, 0, 0)
     g_pline.Move pt0, pt(33, 0.0625, 0)
     
  acadApp.Update
End Sub

More complex assemblies can use a selection set to block the result using parameters in the name. So its pretty easy to develop a lot of similar parts with different dimensions.

Sub part_end_view_2(W As Double, Thk As Double)
 
    Dim strblk As String
    strblk = "Panel " & Thk & " x " & W - 1 & " GL"
     
    Call init_draw
    Dim sset As AcadSelectionSet
    addss "SSBLOCK"
    Set sset = acadDoc.SelectionSets.Item("SSBLOCK")
 
    Call shape_1("FM_SINGLE", W, 0, 0, 0, 0)
    sset.Select acSelectionSetLast

    g_pline.Mirror pt(0, Thk / 2, 0), pt(5, Thk / 2, 0)
    sset.Select acSelectionSetLast
  
    Call shape_1("CHAN_SINGLE", 1, Thk - 0.125, 0, 0, 0)
    g_pline.Move pt0, pt(0, -(Thk - 0.125) / 2, 0)
    g_pline.Rotate pt0, PI
    g_pline.Move pt0, pt(1.25, Thk / 2, 0)
    sset.Select acSelectionSetLast
  
    Call shape_1("CHAN_SINGLE", 1, Thk - 0.5, 0, 0, 0)
    g_pline.Move pt0, pt(0, (-(Thk - 0.5) / 2), 0)
    g_pline.Move pt0, pt(W - 1.25, Thk / 2, 0)
    sset.Select acSelectionSetLast
     
    make_blk sset, strblk
    sset.Erase
    sset.Clear
    sset.Delete
    acadDoc.ModelSpace.InsertBlock pt0, strblk, 1, 1, 1, 0
  
    acadApp.Update
End Sub

When rotating and moving, sometimes its conceptually easier to go backwards. What point is easiest to reference in the final position? Move the part so that point is at 0,0 then rotate about the origin and move from there. Or you can draw the object in the rotation and position you wish by experimenting with the point values in the spreadsheet.

I have found the Evaluate function in VBA is pretty robust and it doesnt matter whether all the spreadsheet entries have the proper spaces, A+W or A + W both are evaluated correctly.

EDIT:- a bug,

the code ran many times before this error popped up. When it does error, it seems to always be that Evaluate is trying to convert a string representing an integer to a double. Other integers work fine, 6 instead of a 4, input from a form, exact same code, CDBL does not seem to help. Variant does not help. something here i am not getting.

When errors make no sense, i tell people, re-boot.

the downside to sheet driven parts – besides sometimes it does not run – its conceptually simpler to see it in code. parameters in a sheet are more fragile, its much easier to delete data in a sheet than code. protect the sheet if you give it out.

on the other hand, I quickly made several assembly drawing programs using sheet driven parts.

this seems to get past the error, though it needs further looking at.

Dim dbl As Double

For i = 1 To icount
str = rng.Cells(1, i)
str = Replace(str, “A”, A)
str = Replace(str, “B”, B)
str = Replace(str, “C”, C)
str = Replace(str, “D”, D)
str = Replace(str, “W”, W)

On Error Resume Next
dbl = CDbl(Evaluate(str))

If Err Then
ar(i) = CDbl(str)
Err.Clear
Else
ar(i) = Evaluate(str)
End If

On Error GoTo 0

Next i

AddLine Absolute and Relative Variations

AddLine absolute and relative variations

Relative coordinates are conceptually the same thing as a vector. Given a start point, a line drawn to relative coordinates will be the start point plus the new vector added to it. Relative data looks the same and has the same structure as the absolute point, but the programming context differs. Our standard addline wrapper using absolute coordinates we will call Line1. Our addline wrapper using relative coordinates we will call Line2. It looks the same as Line1, but the first point is absolute, the second point is relative. The line is drawn from the first point to their sum. To get the full benefit of relative coordinates, the endpoint needs to be saved to a lastpoint type variable, I will call g_pt. This is similar to the way turtle geometry remembers its current position. We use sub Line2(abs pt, rel pt) where abs pt can be but does not have to be the current point. Both Line1 and Line2 save their endpoint as g_pt. With this system we can walk down a line of features drawing an item in sequence. To draw a line from any point to a point 5 units to the east, Line2 pt1, pt(5,0,0). To draw the next line 2 units up, Line2 g_pt, pt(0,2,0). pt(x,y,z) is a wrapper i have previously detailed.

Autocad has four basic ways of addressing the coordinate system, two for 2D and 2 for 3d. Each of these has an absolute and a relative method. The absolute mode references the origin, 0,0,0. The relative mode references the last point.

If you do not type the at sign, you are entering an absolute coordinate relative to the origin.
If you type an at sign, you are entering a coordinate relative to the starting point.

Start the line command, pick a random start point, the following options can be typed to draw different lines – All options have absolute and relative modes.

Cartesian coordinates
3,1
Draws a line from the startpoint to the absolute coordinate address defined by 0,0

@3,1
Draws a line to the right 3 and up 1 relative from the start point.

Polar coordinates
3<45
Draws a line from the startpoint to a point 3 units from the origin and at angle 45 from the zero angle line. The zero angle line is positive x, but of course it can be changed. angle zero is to the right, the east, and angles increment counter clockwise (< 90 is to top).

@3<45
Draws a line 3 units long at 45 degrees from the startpoint

These are the main methods for 2D drafting. The relative address can also be typed for the first prompt. Autocad uses the LastPoint variable, adds the relative address and uses that for the first point.

for 3D there are two more methods.

Cylindrical coordinates
3<45,4
The first part of the address is the same as polar coordinates, the second part raises the line to a point with Z = 4

@3<45,4
Draws a line with cylindrical coordinates relative from the startpoint at distance and angle, then to z dimension.

Spherical coordinates
3<45<30
Distance and angle same as polar coordinates, then an angle from the xy plane.
For instance, if a sphere the size of earth had its origin at its center, and if England were on the positive X axis, with the z-axis through the poles, the address for Moscow would be 3985<37<55, approximately, in miles. 3985 is the earth radius in miles. Latitude and Longitude use this system, though they use North South East West rather than plus minus.

@3<45<30 would use the starting point as the relative origin.

For completeness, a user should know direct distance entry, which accepts a number for distance and uses the cursor for direction.

Now lets look at autocad VBA addline. I wont do any 3D today. Addline uses absolute points. It has no built-in relative addressing, but its easy to add. Addline has only two arguments, two points.

Dim lineobj As AcadLine
Set lineobj = acadDoc.ModelSpace.AddLine (pt1, pt2)

If these are dynamic arrays, we have all kinds of options.

Rather than embed addline in our main code, the first thing we should do is make a convenient wrapper for it. We can forget the details if we do that. Lets call the basic addline wrapper with absolute point addresses Line1

 Sub line1(pt1() As Double, pt2() As Double, Optional strlayer As Variant)
   ' line wrapper absolute pt args with optional layer
    
        Dim lineobj As AcadLine
        Set lineobj = acadDoc.ModelSpace.AddLine(pt1, pt2)
            
        If Not IsMissing(strlayer) Then
           lineobj.Layer = strlayer
        End If
        
        g_pt = pt2
        Set g_line = lineobj
 End Sub

3 things are added above the minimum. An optional layer name in the arguments list and two global variables. G_pt is a dynamic array. It becomes the VBA version of LastPoint. G_line is a global acadline. All the properties and methods of the line just created are available in the calling program (including move). These dont have to be used but they are there.

Here is our current drawing project. We want to draw a notched plate for any length of plate and any center dimension on the half-slots. We have previously derived N and R, now we want to apply those numbers.

The easiest way to approach this is with a lastpoint variable that knows the endpoint of the last line is the beginning of the next line. To take advantage of that we also need relative coordinates. Line2 expects an absolute first point and a relative second point. It adds them to find the absolute lastpoint. At the end it saves the result to g_pt.

Sub line2(pt1() As Double, pt2() As Double)
  'vector method, line from pt1 startpoint at pt2 displacement
        Dim lineobj As AcadLine
        Dim pt3() As Double
        
        pt3 = pt(pt1(0) + pt2(0), pt1(1) + pt2(1), pt1(2) + pt2(2))
        Set lineobj = acadDoc.ModelSpace.AddLine(pt1, pt3)
 
         g_pt = pt3
         Set g_line = lineobj
 End Sub

In our example, the left end of the line is at 0,0. There are two horizontal lines, LineR and LineC. We need a loop. If we iterate the loop N times, there is one extra slot. So the pseudocode is

LineR
Half-slot
Loop n times
LineC
Half-slot
End loop
LineR

Line1 is sufficient for LineR and initializes g_pt as the starting point of the slot (pt function is explained in detail previously)

line1 pt0, pt(R – D / 2, 0, 0)

The sub half-slot uses g_pt as its starting point and the variables for the size of the slot. Line2 is used so that the line is drawn

from g_pt @ 0, gage

line2 g_pt, pt(0, gage, 0)

Now the arc has to be made. The VBA addarc takes four arguments, the center, radius, startangle and endangle. The arc is always drawn counterclockwise. I dont have anything fancy for it. It does not use endpoints. We have to calculate the center. The startangle is zero. The endangle is 180. We can use a degree to radian converter in the wrapper so we can feed it degrees. The startpoint actually is the point we want to save as our g_pt, so we need to calculate that. The original g_pt, the ctr and the ending g_pt are all in a line with the same y value. We just need to increment the x value. V_add is a function to add two points and return their sum. It stands for vector_add. The concept of vector addition to calculate points simplifies the code.

After we have the arc, we add the last line, the same as the first line except in the other direction. Everytime we draw anything, g_pt is updated so we can use it for our next starting point. Here is the code for the half_slot.

Sub half_slot(g_pt() As Double, gage As Double, dia As Double)
    Dim ptc() As Double
     
     'first vertical line with vector method
        line2 g_pt, pt(0, gage, 0)
    
    'calculate arc center with by adding vector to current point
    ptc = v_add(g_pt, pt(dia / 2, 0, 0))
    arc1 ptc, dia / 2, 0, 180
    
    'since arc doesnt work with points
    'the starting point for the next line has to be calculated
     g_pt = pt(g_pt(0) + dia, gage, 0)
    
    'second vertical line with vector method
    ' a variable cannot be negated with a negative sign
    ' have to subtract from zero
    line2 g_pt, pt(0, 0 - gage, 0)
End Sub

Here is the sub that draws the notched edge, given L, N, C and R

 Sub edge_slot(L As Double, N As Integer, C As Double, R As Double)
    'G = slot Gage - distance of center from edge
    'D = Dia of slot
       Dim G As Double, D As Double
       Dim i As Integer
       'read in globals
       G = hole_gage
       D = hole_dia

    'line R1
       line1 pt0, pt(R - D / 2, 0, 0)
    'half_slot prior to loop
       half_slot g_pt, G, D
    
    'loop draws horiz lineC and the half-slot
         'absolute coordinates in the loop are messy
         'line1 g_pt, pt(R + i * C - D / 2, 0, 0)
         'easier to figure the vector from the startpoint
      For i = 1 To N
          line2 g_pt, pt(C - D, 0, 0)
          half_slot g_pt, G, D
      Next i
    'line R2
        line1 g_pt, pt(L, 0, 0)
End Sub

Parametric Dimensioning

Sub-routines and functions should be single purpose, they should list inputs and outputs in comments at the top of the file, and they should print on one page. Thats what I learned at school. The sub that calculates parametric points though tends to turn into a long list of tasks because the point values are so useful. They are used to hang dimensions, notes, blocks, associate with other parts, move into position on the page, and on and on. Another thing sometimes making it difficult to break up the laundry list is that functions can only return one value.

Small pieces are easier to manage than one big. Irrelevant but necessary details are hidden. Compromises then are these. The calling sub we will call main (not literally). The polyline draw routine is a sub of main. Anything that does not require point values developed in the draw sub will sub from main. Anything that must use the point values in polyline draw can tack on to polyline draw or sub from it. Functions (actually subs) that need to set up more than one value can use global variables. Global variables are a useful tool.

Dimensions hang from the points of the object. It’s hard to remove the dimension sub from the geometry sub entirely, but they can be reduced to the essential. One of the complications of dimensioning parts that can change size is locating the dimensions. They have to move with the part. A big part of this post is showing how to do that. The array of geometry points can be searched in a loop to get a bounding box, then standard distances off the bounding box can be calculated for locating dimension lines.

To illustrate how dimensions work – the part is outside corner, abbrev OS_CRNR

The xy data are calculated from the sketch. The array is made. The array is passed off to be drawn.

Dim pts As Variant

x0 = 0
x1 = W
x2 = W + B – 0.375
x3 = W + B
x4 = x2
x5 = 0.125
x6 = 0.125 + W

y0 = 0
y1 = W
y2 = W + A – 0.375
y3 = W + A
y4 = y2
y5 = 0.125
y6 = 0.125 + W

pts = Array(x0, y0, x2, y0, x3, y5, x3, y6, x4, y1, x1, y1, x1, y4, x6, y3, x5, y3, x0, y2)
Call draw_array(pts)

bound_box pts
dimloc LL, UR, sc

The array can then be traversed to find its minimum and maximum values, and a bounding box, defined by lower left point and upper right point, can be calculated. Since there are two values to be returned, global variables are used.

Public LL() As Double
Public UR() As Double


Sub bound_box(pts)
'input is variant array of pts created for 2D polyline
'output is global LL and UR points of bounding box
     Dim i As Integer
     Dim lower As Integer, upper As Integer
     Dim xmin As Double, xmax As Double
     Dim ymin As Double, ymax As Double
     
     lower = LBound(pts)
     upper = UBound(pts)
     
     xmin = pts(0)
     xmax = pts(0)
          
     For i = 0 To upper - 1 Step 2
     If xmin > pts(i) Then xmin = pts(i)
     If xmax < pts(i) Then xmax = pts(i)
     Next i
 
     ymin = pts(1)
     ymax = pts(1)
     
     For i = 1 To upper Step 2
     If ymin > pts(i) Then ymin = pts(i)
     If ymax < pts(i) Then ymax = pts(i)
     Next i
     
     'global pt variables set
     LL = pt(xmin, ymin, 0)
     UR = pt(xmax, ymax, 0)

End Sub

Now that the corners of a virtual box are obtained, it can be used to situate dimension lines. These have to be calculated in relation to whatever scale is used. Thats a separate determination in the program. Here I am using global variable sc which is set to 2. The distance between dimension lines is one half inch. Those two numbers are multiplied.

Linear Dimensions in autocad vba need three inputs – two endpoints and a location for the dimension line. We are making standard locations on all 4 sides for the dimension lines. They are global variables. They are our common dynamic point array. They are calculated a set distance from the part and almost never have to be moved after the fact.

Bound_box returns the extents of the part. The sub dimloc uses the extents for input, LL and UR, and the scale sc and sets the location variables for this particular part.


Sub dimloc(LL() As Double, UR() As Double, sc As Integer)
'input is lower and upper corner points of geometry
'output are global midpoint locations of dimension lines
' VT1, VT2, VT3, VT4, HZ1, HZ2, HZ3, HZ4
     
    Dim x As Double, y As Double
    Dim A As Double
    A = 0.5 * sc
     
    x = (LL(0) + UR(0)) / 2
    y = LL(1) - A * 2
    hz1 = pt(x, y, 0)
     
    y = LL(1) - A
    hz2 = pt(x, y, 0)
    
    y = UR(1) + A
    hz3 = pt(x, y, 0)
    
    y = UR(1) + 2 * A
    hz4 = pt(x, y, 0)
    
    
    x = LL(0) - A * 2
    y = (LL(1) + UR(1)) / 2
    vt1 = pt(x, y, 0)
    
    x = LL(0) - A
    vt2 = pt(x, y, 0)
    
    x = UR(0) + A
    vt3 = pt(x, y, 0)
    
    x = UR(0) + 2 * A
    vt4 = pt(x, y, 0)
      
      
     LL2 = pt(vt1(0), hz1(1), 0)
     UR2 = pt(vt4(0), hz4(1), 0)

End Sub

And then finally we use those variables to create dimensions. We do that in the sub where the points are using the xy data available. The call is simple, the details are hidden away. Wrapper subs for the dimension are used. I use one for each vertical and horizontal dimensions with the rotations built-in.

Here is the calling sub OS_CRNR (main), OS_CRNR_draw, and the dimensioning wrappers.


Sub OS_CRNR(A As Double, B As Double, W As Double)
 
Call OS_CRNR_draw(A, B, W)
           do_os_crnr_note
           flat_dim = A + B
           Do_bend_det
           Do_border
           Do_title

           Etc
           Etc
  End Sub


 Sub OS_CRNR_draw(A As Double, B As Double, W As Double)
    Dim x0 As Double, y0 As Double
    Dim x1 As Double, x2 As Double, x3 As Double, x4 As Double, x5 As Double, x6 As Double
    Dim y1 As Double, y2 As Double, y3 As Double, y4 As Double, y5 As Double, y6 As Double
    Dim pts As Variant

    x0 = 0
    x1 = W
    x2 = W + B - 0.375
    x3 = W + B
    x4 = x2
    x5 = 0.125
    x6 = 0.125 + W
    
    y0 = 0
    y1 = W
    y2 = W + A - 0.375
    y3 = W + A
    y4 = y2
    y5 = 0.125
    y6 = 0.125 + W

    pts = Array(x0, y0, x2, y0, x3, y5, x3, y6, x4, y1, x1, y1, x1, y4, x6, y3, x5, y3, x0, y2)
    Call draw_array(pts)
    global_pline.Layer = "0"
    
    bound_box pts
    dimloc LL, UR, sc
    
    pt1 = pt(x1, y1, 0)
    pt2 = pt(x5, y3, 0)
    pt3 = pt(x3, y5, 0)
    pt4 = pt(x0, y3 - 0.625, 0)
    pt5 = pt(x3 - 0.625, y0, 0)
    
    dimv pt1, pt2, vt1
    dimh pt1, pt3, hz1
    dimv pt4, pt2, vt2
    dimh pt5, pt3, hz2

    do_screw pt4, 0
    do_screw pt5, PI / 2

    acadApp.Update
End Sub


Sub dimv(pt1() As Double, pt2() As Double, dimlocpt() As Double)
 Dim dimObj As AcadDimRotated
 Set dimObj = acadDoc.ModelSpace.AddDimRotated(pt1, pt2, dimlocpt, PI / 2)
 dimObj.Layer = "DIM"
End Sub


Sub dimh(pt1() As Double, pt2() As Double, dimlocpt() As Double)
 Dim dimObj As AcadDimRotated
 Set dimObj = acadDoc.ModelSpace.AddDimRotated(pt1, pt2, dimlocpt, 0)
 dimObj.Layer = "DIM"
End Sub

see the previous post for the essential PT(x,y,z) function that creates point arrays.

global variables that dimloc sets look like this (below). LL2 and UR2 are bounding box of the part and its dimensions, for the purpose of moving it into position on a 8×10 border. I have made peace with the idea that there can be too many global variables. They help solve the problem of passing values between smaller programs.

‘vert dim line locations left to right
Public vt1() As Double
Public vt2() As Double
Public vt3() As Double
Public vt4() As Double

‘horiz dim line locations bottom to top
Public hz1() As Double
Public hz2() As Double
Public hz3() As Double
Public hz4() As Double

Public LL() As Double
Public UR() As Double

Public LL2() As Double
Public UR2() As Double

Direction Cosines

Inclination – angle in radians or degrees a 2D line makes with the positive x-axis, measuring counter-clockwise. A line extends indefinitely in both directions.

In radians, 0 <= theta < pi
In degrees, 0 <= theta < 180

Slope – tangent of the inclination, y/x, rise over run.

Slope m = Tan alpha = y/x

Lines are perpendicular if their slopes are m1 = -1/m2

Tan alpha = – tan (180 – alpha)

A 3D line, a 3D vector, is determined by any two points. If one of these points is the origin, the coordinates of every point on the line form a set of direction numbers. Every line has an infinite set of direction numbers. The numbers always have the same proportions. You can get the coordinates of any point by multiplying the coordinates of some other point by some constant.

The direction angles of a 3D line are defined to be the angle the line makes directly with each of the positive axes. Knowing any two the third can be calculated. The angles are limited to 0 to 180, similar to the 2D inclination, but not limited to being counter-clockwise.

For an undirected line that extends indefinitely in both directions, there are two sets of direction angles,
theta and (180 – theta)

The basic calculation of 3D line direction is the division of the direction numbers by the length between the two points that generated that set. This gives the direction cosines, the x,y and z values for a line of length one. Multiplying the direction cosines by a constant gives a point on the same line at distance of the constant. The 3 direction angles of the line can be found with the Cos(-1) function.

For instance a line from the origin to (3,4,5) has a length of 7.07. The direction cosines are 3/7.07, 4/7.07, 5/7.07), the angle of the line to each of the axes, xyz, by calculator cos(-1), is approx 65, 55, and 45 degrees. Lets check that out with autocad and VBA.


Sub test24()
Call Connect_Acad

Dim x As Double, y As Double, z As Double
Dim pt0() As Double
pt0 = pt(0, 0, 0)

Dim ptX() As Double 'pt on x axis to make alpha ucs
Dim ptY() As Double 'pt on y axis to make beta ucs
Dim ptZ() As Double 'pt on z axis to make gamma ucs

'i could re-use these to only have one
Dim ptx2() As Double 'pt to define new x axis for alpha
Dim pty2() As Double 'pt to define new x axis for beta
Dim ptz2() As Double 'pt to define new x axis for gamma

Dim A() As Double, B() As Double, C() As Double
Dim dir_cos() As Double
Dim dir_ang() As Double

x = -3: y = -4: z = 5
A = pt(x, y, z)

'draw the autocad line
vec_draw1 A

ptX = pt(x, 0, 0)
ptx2 = pt(x + 1, 0, 0)
ucs ptX, ptx2, A, "ucs_alpha"
ptX = pt(Abs(x), 0, 0)
dim_ang pt0, ptX, A, midpt2(ptX, A)
label_pt A, "A"

ptY = pt(0, y, 0)
pty2 = pt(0, y + 1, 0)
ucs ptY, pty2, A, "ucs_beta"
ptY = pt(0, Abs(y), 0)
dim_ang pt0, ptY, A, midpt2(ptY, A)

ptZ = pt(0, 0, z)
ptz2 = pt(0, 0, z + 1)
ucs ptZ, ptz2, A, "ucs_gamma"
ptZ = pt(0, 0, Abs(z))
dim_ang pt0, ptZ, A, midpt2(ptZ, A)

'the box is a visual aid not always required
'Dim pt1() As Double
'pt1 = pt(x / 2, y / 2, z / 2)
'solidbox pt1, Abs(x), Abs(y), Abs(z)

'here are the calculations
dir_cos = dir_cos1(A)
Debug.Print "< " & x; ", " & y & ", " & z & " >"
Debug.Print "alpha_cos " & dir_cos(0)
Debug.Print "beta_cos " & dir_cos(1)
Debug.Print "gamma_cos " & dir_cos(2)
Debug.Print

'direction cosines are the xyz values where the line
'has a length of one
'put an autocad point object at those coordinates
draw_pt dir_cos, "L=1"

dir_ang = dir_ang1(A)
Debug.Print "Alpha " & rad2deg(dir_ang(0))
Debug.Print "Beta " & rad2deg(dir_ang(1))
Debug.Print "Gamma " & rad2deg(dir_ang(2))
Debug.Print

're-set ucs to world
set_wcs

acadApp.Update
End Sub

Call the autocad dimangular method. First the ucs is changed for each of the 3 angles. UCS requires 3 points. a new origin, a point on the new x-axis, and a point on the new y-axis. the origin is variously either (x,0,0), (0,y,0), or (0,0,z), the point on the new x-axis is variously (x+1,0,0), (0,y+1,0) or (0,0,z+1). the point on the new y-axis is always the point of the vector. that establishes a right triangle in the plane of the vector and the axis to which the angular dimension is drawn.


Sub dim_ang(pt1() As Double, pt2() As Double, pt3() As Double, pt4() As Double)
 Dim dimObj As AcadDimAngular
 'Set dimObj = acadDoc.ModelSpace.AddDimAngular(AngleVertex, firstendpoint, secondendpoint, textpoint)
 Set dimObj = acadDoc.ModelSpace.AddDimAngular(pt1, pt2, pt3, pt4)
End Sub

Independently of the autocad dimangular method, the direction cosines and direction angles are calculated. Since these are always triples, the most convenient way to store them is in a 3 place array of doubles, exactly as point coordinates are stored. In fact the direction cosines are the point coordinates for any line at the point where the length of the line is one. Both direction cosines and direction angles are calculated for a vector with tail at the origin. The version to calculate with tail not at the origin would simply do the subtraction of coordinates then call this version.

Function dir_cos1(pt1() As Double) As Double()
Dim cos_alpha As Double, cos_beta As Double, cos_gamma As Double
Dim x As Double, y As Double, z As Double
Dim D As Double

D = dist1(pt1)

If D = 0 Then
dir_cos1 = pt(0, 0, 0)
Exit Function
End If

x = pt1(0): y = pt1(1): z = pt1(2)

cos_alpha = x / D
cos_beta = y / D
cos_gamma = z / D

dir_cos1 = pt(cos_alpha, cos_beta, cos_gamma)
End Function

Function dir_ang1(pt1() As Double) As Double()
Dim alpha As Double, beta As Double, gamma As Double
Dim dir_cos() As Double
Dim pt2(0 To 2) As Double

dir_cos = dir_cos1(pt1)

alpha = WorksheetFunction.Acos(dir_cos(0))
beta = WorksheetFunction.Acos(dir_cos(1))
gamma = WorksheetFunction.Acos(dir_cos(2))

pt2(0) = alpha
pt2(1) = beta
pt2(2) = gamma

dir_ang1 = pt2

End Function

testing the subroutine for values in all 8 quadrants. the angles are always measured to the positive side of the axis. therefore they are always between 0 and 180, never negative. Getting the confidence that all quadrants return true values, the next step is to develop a sub-routine that returns the angle between two lines.

3D Line – part 2

The distance formula

Function dist1(pt1() As Double) As Double
Dim x As Double, y As Double, z As Double
x = pt1(0): y = pt1(1): z = pt1(2)
dist1 = (x ^ 2 + y ^ 2 + z ^ 2) ^ (1 / 2)
End Function

Function dist2(pt1() As Double, pt2() As Double) As Double
Dim x As Double, y As Double, z As Double
x = pt2(0) - pt1(0)
y = pt2(1) - pt1(1)
z = pt2(2) - pt1(2)
dist2 = (x ^ 2 + y ^ 2 + z ^ 2) ^ (1 / 2)
End Function

test sub

Sub test17()
 Call Connect_Acad
Dim A() As Double, B() As Double
Dim x As Double, y As Double, z As Double

x = 5
y = 4
z = 3

A = pt(x, y, z)
B = pt(x, y, 0)

line1 A
txt1 midpt1(A), dec2(dist1(A))

line1 B
txt1 midpt1(B), dec2(dist1(B))

line1 pt(x, 0, 0)
txt1 midpt1(pt(x, 0, 0)), dec2(x)

line1 pt(0, y, 0)
line1 pt(0, 0, z)

line2 A, B
txt1 midpt2(A, B), dec2(dist2(A, B))

line2 pt(x, 0, 0), B
txt1 midpt2(pt(x, 0, 0), B), dec2(y)

label_pt2 A, "A", 2
label_pt2 B, "B", 2

Dim pt1() As Double
pt1 = pt(x / 2, y / 2, z / 2)
solidbox pt1, x, y, z

End Sub

text and label subs

Sub txt1(pt1() As Double, str As String, Optional height As Variant, Optional rotation As Variant)
  Dim objtxt As AcadText
  
  If IsMissing(height) Then
      height = textheight  'global var
    End If
    
  If IsMissing(rotation) Then
      rotation = 0
    End If
 
  Set objtxt = acadDoc.ModelSpace.AddText(str, pt1, height)
  objtxt.Layer = "0"
  
  If rotation <> 0 Then
  objtxt.rotation = deg2rad(CDbl(rotation))
  End If
     
 End Sub
 
Sub label_pt2(pt1() As Double, Optional str_label As String = "none", Optional prec As Integer = 4)

If IsMissing(height) Then
      height = textheight  'global var
    End If
    
Dim str As String
Dim x As Double, y As Double, z As Double
x = Round(pt1(0), prec)
y = Round(pt1(1), prec)
z = Round(pt1(2), prec)

str = "<" & x & "," & y & "," & z & ">"

If str_label <> "none" Then
str = str_label & " " & str
End If

txt1 pt1, str, textheight
End Sub
 
 Function dec2(number As Double) As Double
   Dim dbl As Double
   dbl = Round(number, 2)
   dec2 = dbl
 End Function

Acad3DSolid box sub. Transparency is a percentage 1 to 90. Higher numbers are more transparent.

Sub solidbox(pt() As Double, leng As Double, wid As Double, height As Double)

Dim objbox As Acad3DSolid
Set objbox = acadDoc.ModelSpace.AddBox(pt, leng, wid, height)
objbox.EntityTransparency = 80

acadApp.Update

End Sub

midpoint

Function midpt1(pt1() As Double) As Double()
Dim x As Double, y As Double, z As Double
x = pt1(0): y = pt1(1): z = pt1(2)
midpt1 = pt(x / 2, y / 2, z / 2)
End Function

Function midpt2(pt1() As Double, pt2() As Double) As Double()
Dim x1 As Double, y1 As Double, z1 As Double
x = (pt1(0) + pt2(0)) / 2
y = (pt1(1) + pt2(1)) / 2
z = (pt1(2) + pt2(2)) / 2
midpt2 = pt(x, y, z)
End Function

test sub


Sub test18()
Call Connect_Acad
Dim A() As Double, B() As Double, C() As Double

A = pt(2, 1, 3)
B = pt(3, -1, -2)
C = pt(0, 2, -1)

line2 A, B
line2 B, C
line2 C, A

line2 midpt2(A, B), midpt2(B, C)
line2 midpt2(B, C), midpt2(C, A)
line2 midpt2(C, A), midpt2(A, B)

label_pt2 A, "A"
label_pt2 B, "B"
label_pt2 C, "C"

label_pt2 midpt2(A, B)
label_pt2 midpt2(B, C)
label_pt2 midpt2(C, A)

acadApp.Update
End Sub

the angles to 3 decimal places

the triangles are all congruent. the middle triangle has half the perimeter of the outer triangle and one fourth the area.

Vectors in the Plane


Dim A() As Double, B() As Double
Dim C() As Double, D() As Double
Dim R() As Double

A = pt(4, 1, 0)
B = pt(2, -3, 0)
C = pt(-2, 2, 0)
D = pt(-3, -2, 0)

vec_draw1 A, "A"
vec_draw1 B, "B"
vec_draw1 C, "C"
vec_draw1 D, "D"

R = v_add(A, B)
vec_draw1 R, "A+B"

R = v_add(A, C)
vec_draw1 R, "A+C"

R = v_add(A, D)
vec_draw1 R, "A+D"

R = v_add3(A, B, C)
vec_draw1 R, "A+B+C"

R = vec_add_2(1, A, 2, B)
vec_draw1 R, "A+2B"

R = vec_add_2(1, A, -3, C)
vec_draw1 R, "A-3C"

R = v_add3(A, C, v_m(1 / 3, D))
vec_draw1 R, "A+C+1/3D"


Sub vec_draw1(pt1() As Double, Optional str_text As Variant)
'line from origin to pt1
Dim lineobj As AcadLine
Set lineobj = acadDoc.ModelSpace.AddLine(pt(0, 0, 0), pt1)

    If Not IsMissing(str_text) Then
         txt1 CStr(str_text), pt1, 0.25
    End If
End Sub

'add 2 vectors, return vector
Function v_add(pt1() As Double, pt2() As Double) As Double()
v_add = pt(pt1(0) + pt2(0), pt1(1) + pt2(1), pt1(2) + pt2(2))
End Function

'multiply scalar by vector, return vector
Function v_m(m As Double, pt1() As Double) As Double()
v_m = pt(m * pt1(0), m * pt1(1), m * pt1(2))
End Function

'add 3 vectors, return vector
Function v_add3(pt1() As Double, pt2() As Double, pt3() As Double) As Double()
v_add3 = pt(pt1(0) + pt2(0) + pt3(0), pt1(1) + pt2(1) + pt3(1), pt1(2) + pt2(2) + pt3(2))
End Function

'add 2 vectors each multiplied by a scalar, A-B would be vec_add_2(1, A, -1, B)
Function vec_add_2(m As Double, pt1() As Double, n As Double, pt2() As Double) As Double()
Dim temp1() As Double,  temp2() As Double
temp1 = v_m(m, pt1)
temp2 = v_m(n, pt2)

vec_add_2 = v_add(temp1, temp2)
End Function

it would be possible to do this on one line but it would be hard to read.

Dim temp1() As Double, temp2() As Double

temp1 = vec_add_2(2, A, -3, B)
temp2 = vec_add_2(-3, C, 4, D)
R = v_add(temp1, temp2)
vec_draw1 R, "2A-3B-3C+4D"

Length and Direction –

Function vec_len(pt1() As Double) As Double
Dim x As Double, y As Double, z As Double
x = pt1(0): y = pt1(1): z = pt1(2)
vec_len = (x ^ 2 + y ^ 2 + z ^ 2) ^ (1 / 2)
End Function

Dividing a vector by the length (multiplying by the inverse of the length) produces a vector with length one in the same direction –

The angle of a 2D vector is a directed line measured from the positive X axis. It takes a value between 0 and 360 degrees not including 360 or 0 and 2 pi not including 2 pi. The inverse tangent is used with y/x as argument. Tangent returns an angle for an undirected line between -90 to +90 or -pi/2 to +pi/2. The code is a bit tedious but its pretty straightforward. We have to trap out zero values of x, to avoid divide by zero, then check for zeros of y and interpret, then retrieve the value for the inverse tangent and interpret according to which quadrant the head of the vector is in.

Function vec_ang(pt1() As Double) As Double
'returns angle in radians
'check for zero length vector return 0 for angle
Dim x As Double, y As Double

x = pt1(0)
y = pt1(1)

If x = 0 And y = 0 Then
    vec_ang = 0
    Exit Function
End If

'get axis directions
If x = 0 Or y = 0 Then
    If y = 0 And x > 0 Then vec_ang = 0
    If x = 0 And y > 0 Then vec_ang = Pi / 2
    If y = 0 And x < 0 Then vec_ang = Pi
    If x = 0 And y < 0 Then vec_ang = 3 * Pi / 2
Exit Function
End If

'calculate m tangent
Dim m As Double
m = y / x

If x > 0 And y > 0 Then  'First Q
'Debug.Print rad2deg(Atn(m))
vec_ang = Atn(m)
End If

If x < 0 And y > 0 Then  'Second Q
'Debug.Print rad2deg(Atn(m))
vec_ang = Pi + Atn(m)
End If

If x < 0 And y < 0 Then  'Third Q
'Debug.Print rad2deg(Atn(m))
vec_ang = Pi + Atn(m)
End If

If x > 0 And y < 0 Then  'Fourth Q
'Debug.Print rad2deg(Atn(m))
vec_ang = 2 * Pi + Atn(m)
End If

End Function


Sub vec_draw3(startpt1() As Double, vectorpt2() As Double, Optional str_text As Variant)
'line from startpoint at vector distance and angle
Dim lineobj As AcadLine
Dim pt3() As Double
pt3 = v_add(startpt1, vectorpt2)
Set lineobj = acadDoc.ModelSpace.AddLine(startpt1, pt3)

If Not IsMissing(str_text) Then
         txt1 CStr(str_text), pt1, 0.25
    End If
End Sub

triangles with random vertexes and medians – (I moved them after the program drew them)


Sub vec_draw2(pt1() As Double, pt2() As Double, Optional str_text As Variant)
'simple line from pt1 to pt2
Dim lineobj As AcadLine
Set lineobj = acadDoc.ModelSpace.AddLine(pt1, pt2)

If Not IsMissing(str_text) Then
         txt1 CStr(str_text), midpt2(pt1, pt2), 0.25
    End If
End Sub

3D Line – part 1

A straight line is completely determined by the coordinates of its endpoints. A straight line having a definite length and direction but no definite location in space is a vector. In its most basic form its a single point. A vector and a point are both a 3-array of doubles. Make them a dynamic array.

The functions to calculate direction, midpt, cosines, addition and scalar multiplication of vectors all return an array of 3 doubles. The line subroutines work with arrays of 3 doubles. Where I have one point argument, assuming 0,0,0 for the other, I number the routine_1. Where I have two point arguments, I number the routine_2. Passing and assigning points cleans up the code to almost logo like clarity.

dim pt1() as double, pt2() as double
pt1 = pt(2, 4, 6)
pt2 = pt(1, 9, 10)
line1 pt1
line1 pt2
line2 pt1, pt2
line2 midpt1(pt1), midpt1(pt2)

Function pt(x As Double, y As Double, z As Double) As Double()
Dim pnt(0 To 2) As Double
pnt(0) = x: pnt(1) = y: pnt(2) = z
pt = pnt
End Function

Sub line1(pt1() As Double)
'line from origin to pt1
Dim lineobj As AcadLine
Set lineobj = acadDoc.ModelSpace.AddLine(pt(0, 0, 0), pt1)
End Sub

Sub line2(pt1() As Double, pt2() As Double)
'line from pt1 to pt2
Dim lineobj As AcadLine
Set lineobj = acadDoc.ModelSpace.AddLine(pt1, pt2)
End Sub

Sub line3(startpt1() As Double, vectorpt2() As Double)
'line from startpoint at vector distance and angle
Dim lineobj As AcadLine
Dim pt3() As Double
pt3 = add_vectors(startpt1, vectorpt2)
Set lineobj = acadDoc.ModelSpace.AddLine(startpt1, pt3)
End Sub
Function dist1(pt1() As Double) As Double
Dim x As Double, y As Double, z As Double
x = pt1(0): y = pt1(1): z = pt1(2)
dist1 = (x ^ 2 + y ^ 2 + z ^ 2) ^ (1 / 2)
End Function

Function dist2(pt1() As Double, pt2() As Double) As Double
Dim x As Double, y As Double, z As Double
x = pt2(0) - pt1(0)
y = pt2(1) - pt1(1)
z = pt2(2) - pt1(2)
dist2 = (x ^ 2 + y ^ 2 + z ^ 2) ^ (1 / 2)
End Function

Function midpt1(pt1() As Double) As Double()
Dim x As Double, y As Double, z As Double
x = pt1(0): y = pt1(1): z = pt1(2)
midpt1 = pt(x / 2, y / 2, z / 2)
End Function

Function midpt2(pt1() As Double, pt2() As Double) As Double()
Dim x1 As Double, y1 As Double, z1 As Double
x = (pt1(0) + pt2(0)) / 2
y = (pt1(1) + pt2(1)) / 2
z = (pt1(2) + pt2(2)) / 2
midpt2 = pt(x, y, z)
End Function


Function dir_cosines1(pt1() As Double) As Double()
Dim cos_alpha As Double, cos_beta As Double, cos_gamma As Double
Dim d As Double

d = dist1(pt1)

If d = 0 Then
dir_cosines1 = pt(0, 0, 0)
Exit Function
End If

cos_alpha = pt1(0) / d
cos_beta = pt1(1) / d
cos_gamma = pt1(2) / d

dir_cosines1 = pt(cos_alpha, cos_beta, cos_gamma)
End Function


Function dir_cosines2(pt1() As Double, pt2() As Double) As Double()
Dim cos_alpha As Double, cos_beta As Double, cos_gamma As Double
Dim d As Double

d = dist1(pt1)

If d = 0 Then
dir_cosines2 = pt(0, 0, 0)
Exit Function
End If

cos_alpha = (pt2(0) - pt1(0)) / d
cos_beta = (pt2(1) - pt1(1)) / d
cos_gamma = (pt2(2) - pt1(2)) / d

dir_cosines2 = pt(cos_alpha, cos_beta, cos_gamma)
End Function

Function dir_angle(dir_cosine As Double) As Double
dir_angle = WorksheetFunction.Acos(dir_cosine)
dir_angle = rad2deg(dir_angle)
End Function

Function add_vectors(pt1() As Double, pt2() As Double) As Double()
add_vectors = pt(pt1(0) + pt2(0), pt1(1) + pt2(1), pt1(2) + pt2(2))
End Function

Function mult_vector(n As Double, pt1() As Double) As Double()
mult_vector = pt(n * pt1(0), n * pt1(1), n * pt1(2))
End Function

Sub test5()
Call Connect_Acad

pt1 = pt(2, 4, 6)
pt2 = pt(1, 9, 10)
line1 pt1
line1 pt2
line2 midpt1(pt1), midpt1(pt2)

Dim cosines() As Double
cosines = dir_cosines1(pt1)
pt3 = mult_vector(10, cosines)

'makes a line 10 in length along same line as pt1
line1 pt3

'alpha angle
Debug.Print dir_angle(cosines(0))
End Sub

Sub test6()
Call Connect_Acad

pt1 = pt(5, 1, 2)
pt2 = pt(1, 2, 3)
pt3 = add_vectors(pt1, pt2)

line1 pt1
line3 pt1, pt2
line1 pt3

acadApp.Update
End Sub