AfraLISP - Learn AutoLISP for AutoCAD productivity

Polyline Bulges - Part 1

by Stig Madsen

Bulges are something that women have (mostly to please the opposite sex it seems) and something that guys try to get by placing socks in strategic places. At least until they get older. Which is the time they tend to develop bulges in not so strategic places. In other words: bulges are all about curvature.

In AutoCAD, bulges are used in shapes and in arc segments of polylines. This article only deals with polyline bulges, and because polyline bulges are describing circular arcs, let's first look at the geometry of a circular arc.

Geometry of a circular arc

Because a circular arc describes a portion of the circumference of a circle, it has all the attributes of a circle:

  • Radius (r) is the same as in the circle the arc is a portion of.
  • Center point (P) is also the same as in the circle.
  • Included angle (θ). In a circle, this angle is 360 degrees.
  • Arc length (le). The arc length is equal to the perimeter in a full circle.

Adding to these attributes are some that are specific for an arc:

  • Start point and end point (P1 and P2) a.k.a. vertices (although sometimes it is practical to talk about specific points that a circle passes through, there are no distinct vertices on the circumference of a circle).
  • Chord length (c). An infinite amount of chords can be described by both circles and arcs, but for an arc there is only one distinct chord that passes through its vertices (for a circle, there is only one distinct chord that passes through the center, the diameter, but it doesn't describe any specific vertices).
  • Given two fixed vertices, there is also a specific midpoint (P3) of an arc.
  • The apothem (a). This line starts at the center and is perpendicular to the chord.
  • The sagitta (s) a.k.a. height of the arc. This line is drawn from the midpoint of an arc and perpendicular to its chord.

Except for the arc itself, an arc can describe two distinct geometric forms: Circular segment and circular sector. Both figures includes all of the attributes above, but for doing calculations with bulges, we'll mostly use the piece of pie that the arc cuts out of a circle, the circular sector.

So, what is a bulge for a circular arc and how is it defined? In AutoCAD's online help reference, it says about bulges for polylines:

The bulge is the tangent of 1/4 of the included angle for the arc between the selected vertex and the next vertex in the polyline's vertex list. A negative bulge value indicates that the arc goes clockwise from the selected vertex to the next vertex. A bulge of 0 indicates a straight segment, and a bulge of 1 is a semicircle.

What does this mean and how can an arc be defined without even knowing the radius - or at least a chord length? It says that the only information given for arc segments in polylines are two vertices and a bulge.

Well, it also says that the bulge has something to do with the tangent of a quarter of the included angle of an arc. That must be a clue of how to obtain the angle. In fact, once you have a bulge value, you can very quickly retrieve the included angle by inverting the above statement. Simply use the built-in function ATAN to get an angle and multiply it by 4 in order to get the included angle:

(* 4.0 (atan 0.57735))
2.09439

So, a bulge of 0.57735 is describing an included angle of 2.09439 radians (which is 120.0 degrees, by the way). Try it out for yourself. Start drawing a lightweight polyline, type "A" for arc, then "A" again for Angle and "120.0" for the included angle. Drop the endpoint somewhere, leave the polyline command and type this at the command line:

Command: (setq ent (entget (entlast)))
((-1 . < Entity name: 7ef844f8>) (0 . "LWPOLYLINE") (330 . < Entity name: 
7ef5ccf8>) (5 . "31F") (100 . "AcDbEntity") (67 . 0) (410 . "Model") (8 . "0") 
(100 . "AcDbPolyline") (90 . 2) (70 . 0) (43 . 0.0) (38 . 0.0) (39 . 0.0) (10 
-566.044 916.408) (40 . 0.0) (41 . 0.0) (42 . 0.57735) (10 -485.424 1075.7) (40 
. 0.0) (41 . 0.0) (42 . -7.40144) (210 0.0 0.0 1.0))
Command: (setq bulge (cdr (assoc 42 ent)))
0.57735

Now you have a bulge value for the arc segment in the polyline, and you can try out the formula above.

OK, fine. But why is the bulge 1/4 of the included angle and where does the tangent fit in? There are many ways to explain this. One is shown below. The figures show a circle with a central angle describing an arc and we'll try to show that the yellow angles ε and σ are exactly one quarter of the cyan central angle θ.

Bulge is one quarter of the included angle

If the full angle is cut in half - as shown with the blue angle η at figure 2 - we get an isosceles triangle (green) where the angles φ and τ are equal. Because the sum of angles in a triangle is always 180 degrees, we now know that the angles φ and τ are:

φ = τ = (180 - θ/2)/2  =>  φ = 90 - θ/4

Now look at the chord from P1 to P2 in figure 3. Together with the red legs of angle θ it also forms an isosceles triangle, and therefore γ is equal to ξ. The top angle is the full angle of θ, so γ and ξ become equal to:

γ = ξ = (180 - θ)/2  =>   γ = 90 - θ/2

Thus, the yellow angle ε must be the magenta angle φ minus the orange angle γ. In other words, ε is a quarter of the included angle θ:

ε = (90 - θ/4) - (90 - θ/2)  =>  ε = θ/2 - θ/4 = θ/4

The bulge is describing how much the arc "bulges out" from the vertices, i.e. the height of the arc (the sagitta (s), or the distance P3 to P4 in figure 4). The height forms a leg of a right-angled triangle that has an exact angle of 1/4 of the included angle (see the yellow triangle P-P2-P3 in figure 4) and because tangent is describing the ratio between the legs in a right-angled triangle, it's easy to describe the geometry with this one angle:

sin ε/cos ε = tan ε

One quarter of the included angle

We could also find tangent of angle ε by simply dividing the opposite leg with the adjacant leg — which means the sagitta, s, divided by half the distance of the chord, c, — but not knowing s and having the tangent of ε already, we would rather want to find s:

s = c/2 * tan(ε)

Given that bulge = tan(ε), we get

s = c/2 * bulge

Radius of the arc can now be found with this formula:

r = ((c/2)2+s2)/2*s

The sign of a particular bulge is important for the way it's defined in relation to the vertices. If a bulge is positive it means that the arc is measured counterclockwise from the starting vertex to the end vertex. If a bulge is negative it means that the arc runs the other way round, — it's measured clockwise. The system variable ANGDIR has no influence on this.

Therefore all the formulas above has to be concerned about the absolute value of the bulge instead of the actual value — or you might end up with a negative radius. In the code below we will find the center point. There are many ways to do this, but the method that is chosen here relies on the angles that were defined previously. Subsequently, we will need it to test whether the bulge is postive or negative and act accordingly.

Remember that the orange angle γ in fig. 3 was found to be 90 degrees minus half of the included angle? What happens if we add (or subtract, depending on the arc direction) this angle to the angle between the two known vertices P1 and P2? We get the angle towards the center. Knowing the angle, the radius and the start point of the arc we can find the center point with POLAR.

(setq gamma (/ (- pi theta) 2.0)
      phi   (if (>= bulge 0)
              (+ (angle p1 p2) gamma)
              (- (angle p1 p2) gamma)
            )
      p     (polar p1 phi r)
)

Another way to find the direction towards the center is to use good old Pythagorus. We already know radius and the chord length, so by using radius as the hypothenuse and half the chord length as a leg in a right-angled triangle, where the apothem is the second leg, it's possible to draw the apothem and find the center point.

(setq c|2  (/ c 2.0))
(setq a    (sqrt (- (expt r 2.0) (expt c|2 2.0)))
      midp (polar p1 (angle p1 p2) c|2)
      p2   (if (>= bulge 0)
             (polar midp (+ (angle p1 p2) (/ pi 2.0)) a)
             (polar midp (- (angle p1 p2) (/ pi 2.0)) a)
           )
)

By now, enough angles and distances are known to also use other trigonometric functions in order to find the center point without using POLAR, but that has to remain a home assignment for now. Let's get some code up'n'running, utilizing the formulas and methods we just went over. Later we will repeat some of the formulas to use with bulges.

First function will be an ordinary pick-a-polyline function. It contains no magic. The user is merely asked to pick a lightweight polyline and, if successful, it returns a list of all segments on the form (vertex1 bulge vertex2). These segments will later be used to analyze each arc segment in the polyline. Although it only accepts lightweight polylines, there's nothing to prevent you from adjusting it to also accept old-style polylines.

(defun getPolySegs (/ ent entl p1 pt bulge seg ptlst)
  (setvar "ERRNO" 0)
  ;; repeat request for polyline until user either picks
  ;; a polyline or exits without picking
  (while (and (not ent) (/= (getvar "ERRNO") 52))
    (if (and (setq ent (car (entsel "\nSelect polyline: ")))
             (/= (cdr (assoc 0 (setq entl (entget ent)))) "LWPOLYLINE")
        )
      (setq ent nil)
    )
  )
  (cond (ent
         ;; save start point if polyline is closed
         (if (= (logand (cdr (assoc 70 entl)) 1) 1)
           (setq p1 (cdr (assoc 10 entl)))
         )
         ;; run thru entity list to collect list of segments
         (while (setq entl (member (assoc 10 entl) entl))
           ;; if segment then add to list
           (if (and pt bulge)
             (setq seg (list pt bulge))
           )
           ;; save next point and bulge
           (setq pt    (cdr (assoc 10 entl))
                 bulge (cdr (assoc 42 entl))
           )
           ;; if segment is build then add last point to segment
           ;; and add segment to list
           (if seg
             (setq seg (append seg (list pt))
                   ptlst (cons seg ptlst))
           )
           ;; reduce list and clear temporary segment
           (setq entl  (cdr entl)
                 seg   nil
           )
         )
        )
  )
  ;; if polyline is closed then add closing segment to list
  (if p1 (setq ptlst (cons (list pt bulge p1) ptlst)))
  ;; reverse and return list of segments
  (reverse ptlst)
)

Next function will be our workhorse. It will use everything we now know about retrieving included angle, height of arc, chord length, radius and center point.
The function accepts a list of arguments on the form that corresponds to the segment sublists from the previous function - (vertex1 bulge vertex2). If the argument is acceptable, it will print out information about the arc segment. We'll let the comments in the code take over any further explanation.

(defun getArcInfo (segment / a p1 bulge p2 c c|2 gamma midp p phi r r2 s theta)
  ;; assign variables to values in argument
  (mapcar 'set '(p1 bulge p2) segment)
  ;; find included angle
  ;; remember that bulge is negative if drawn clockwise
  (setq theta (* 4.0 (atan (abs bulge))))
  ;; output included angle
  (princ (strcat "\n Included angle: "
                 (rtos theta)
                 " rad ("
                 (angtos theta 0)
                 " degrees)"
         )
  )
  ;; find height of the arc
  (setq c (distance p1 p2)
        s (* (/ c 2.0) (abs bulge))
  )
  ;; output height of arc
  (princ (strcat "\n Height of arc:  " (rtos s)))
  ;; output chord length
  (princ (strcat "\n Chord length:   " (rtos c)))
  ;; If this function is used without making sure that the segment
  ;; is not simply a line segment (bulge = 0.0), it will produce
  ;; a division-by-zero error in the following. Therefore we want
  ;; to be sure that it doesn't process line segments.
  (cond ((not (equal bulge 0.0 1E-6))
         ;; find radius of arc
         ;; first find half the chord length
         (setq c|2 (/ c 2.0)
               ;; find radius with Pythagoras (used as output)
               r   (/ (+ (expt c|2 2.0) (expt s 2.0)) (* s 2.0))
               ;; find radius with trigonometry
               r2  (/ c|2 (sin (/ theta 2.0)))
         )
         (princ (strcat "\n Radius of arc:  " (rtos r)))
         ;; find center point of arc with angle arithmetic
         ;; (used as output)
         (setq gamma (/ (- pi theta) 2.0)
               phi   (if (>= bulge 0)
                       (+ (angle p1 p2) gamma)
                       (- (angle p1 p2) gamma)
                     )
               p     (polar p1 phi r)
         )
         ;; find center point of arc with Pythagoras
         (setq a    (sqrt (- (expt r 2.0) (expt c|2 2.0)))
               midp (polar p1 (angle p1 p2) c|2)
               p2   (if (>= bulge 0)
                      (polar midp (+ (angle p1 p2) (/ pi 2.0)) a)
                      (polar midp (- (angle p1 p2) (/ pi 2.0)) a)
                    )
         )
         ;; output coordinates of center point
         (princ (strcat "\n Center of arc:  "
                        (rtos (car p))
                        ","
                        (rtos (cadr p))
                )
         )
        )
        (T (princ "\n Segment has no arc info"))
  )
  (princ)
)

To try out these two functions, first draw a lightweight polyline with a couple of arc segments. At the command line, call getPolysegs and assign a variable to the returned list:

Command: (setq myPoly (getPolySegs))
Select polyline: [select a polyline]

If a lightweight polyline was selected, it will return a list of segments. If, for example, the second segment contains a bulge value different from 0.0 then you can call the latter function like this:

Command: (getArcInfo (nth 1 myPoly))
 Included angle: 1.9265 rad (110.3830 degrees)
 Height of arc:  855.2904
 Chord length:   3272.6317
 Radius of arc:  1992.9203
 Center of arc:  34915.2223,21409.8733

The last function in this article will bind the two functions together and explore each arc segment in the selected polyline. It will appear in part two — along with some useful formulas for dealing with bulges.