Lightweight Polylines30 Jun, 1999 By: Bill Kramer
Applications developed before AutoCAD R14 sometimes stumble when they encounter the new objects of Release 14 and later. The most common problem stems from the change in the polyline object inside AutoCAD. AutoCAD R14 introduced a new object named a lightweight polyline that replaces the complex polyline object. When you create a drawing in Release 14 and use commands to create polylines, then you are most likely creating lightweight polylines. From an AutoCAD user's perspective, the lightweight polyline is exactly like the older polyline. But from a programmer's perspective, they are very different. This month, we'll look at these differences from a programmer's point of view-both in AutoLISP and in VBA.
Let's start with an explanation of exactly what a lightweight polyline is versus a traditional polyline. The primary difference is that a lightweight polyline is addressed as a single entity object whereas the traditional polyline is a sequence of entity objects. In a traditional polyline, you find a series of vertex objects that define the attributes of each point. Each vertex has its own entity ID and a full complement of information about that object. In most polylines, the information contained in each vertex is redundant. For example, the layer and linetype assignments rarely vary (and shouldn't!) between vertices. In fact, about the only information each vertex contains that is unique is the x, y and z points and bulge factor. In some cases, the width will vary between vertices as well.
Thus, a traditional polyline object takes up a lot more space than it might need in the majority of instances where polylines are used in AutoCAD drawings. That is where a lightweight polyline comes into play. A lightweight polyline contains only the point, bulge and width information for each vertex. And, it is all contained in a single entity. A lightweight polyline has a repeating set of group codes for these values at each vertex point. This is best seen in the example that follows. First, draw a simple rectangle.
Specify first corner point or [Chamfer/Elevation/Fillet/ Thickness/Width]: 0,0
Specify other corner point: 1,1
Next, type in the expression (entget (entlast)) at the command prompt to reveal the contents of the lightweight polyline.
|Listing 1. Entity List Resulting From (ENTGET (ENTLAST)) After Drawing a
Rectangle From 0,0 to 1,1|
( (-1 . <Entity name: 165b558>) (0 . "LWPOLYLINE") (330 . <Entity name: 165b4f8>) (5 . "2B") (100 . "AcDbEntity") (67 . 0) (410 . "Model") (8 . "0") (100 . "AcDbPolyline") (90 . 4) (70 . 1) (43 . 0.0) (38 . 0.0) (39 . 0.0) (10 0.0 0.0) (40 . 0.0) (41 . 0.0) (42 . 0.0) (10 1.0 0.0) (40 . 0.0) (41 . 0.0) (42 . 0.0) (10 1.0 1.0) (40 . 0.0) (41 . 0.0) (42 . 0.0) (10 0.0 1.0) (40 . 0.0) (41 . 0.0) (42 . 0.0) (210 0.0 0.0 1.0) )
|Listing 2. The List Points in a Lightweight Polyline
;; (defun LWPOLY_SHOW (EL) (while (setq EL (member (assoc 10 EL) EL)) (print (cdr (assoc 10 EL))) (setq EL (cdr EL)) ) )
|Listing 3. Get the Nth Vertex From a Lightweight Polyline|
;; (defun LWPOLY_NTH (N EL) (setq EL (member (assoc 10 EL) EL)) (while (and EL (> N 0)) (setq EL (member (assoc 10 (cdr EL)) EL) N (1- N) ) ) (if (and EL (zerop N)) (list (assoc 10 EL) (assoc 40 EL) (assoc 41 EL) (assoc 42 EL) ) ) )
|Listing 4. Convert a Lightweight to a Traditional Polyline|
;; (defun LWPOLY_2_POLY (EL / ZV ZT LYR CLR LTY) (if (= (type EL) 'ENAME) (setq EL (entget EL)) (if (= (type (car EL)) 'ENAME) (setq EL (entget (car EL))))) (if (= (cdr (assoc 0 EL)) "LWPOLYLINE") (progn (setq ZV (assoc 38 EL) ZT (assoc 39 EL) CLR (assoc 62 EL) LTY (assoc 6 EL) ) (if (null ZV) (setq ZV 0.0)) (if (null ZT) (setq ZT 0.0)) (setq EN (list '(0 . "POLYLINE") (setq LYR (assoc 8 EL)) (assoc 210 EL) ZT (list 10 0.0 0.0 (cdr ZV)) (assoc 70 EL) )) (if CLR (setq EN (append EN (list CLR)))) (if LTY (setq EN (append EN (list LTY)))) (entmake EN) (setq EN (list '(0 . "VERTEX") LYR '(10 0 0 0) '(40 . 0.0) '(41 . 0.0) '(42 . 0.0) ZT )) (if CLR (setq EN (append EN (list CLR)))) (if LTY (setq EN (append EN (list LTY)))) (while (setq EL (member (assoc 10 EL) EL)) (setq EN (subst (append (assoc 10 EL) (list (cdr ZV))) (assoc 10 EN) EN) EN (subst (assoc 42 EL) (assoc 42 EN) EN) EN (subst (assoc 40 EL) (assoc 40 EN) EN) EN (subst (assoc 41 EL) (assoc 41 EN) EN) ) (entmake EN) (setq EL (cdr EL)) ) (entmake (list '(0 . "SEQEND") LYR)) (entlast) ) ) )
|Listing 5. VBA Utility to Convert Lightweight Polylines to Traditional
Function lwpoly_2_poly(EN As Object) As AcadPolyline Dim I As Integer, J As Integer, K As Integer If EN.EntityName = "AcDbPolyline" Then Dim Coords As Variant Coords = EN.Coordinates I = Fix((UBound(Coords) + 1) * 1.5) - 1 ReDim Coords2(I) As Double J = 0 Dim X As Double, Y As Double, Z As Double For I = LBound(Coords) To UBound(Coords) Step 2 X = Coords(I): Y = Coords(I + 1): Z = 0# Coords2(J) = X: Coords2(J + 1) = Y: Coords2(J + 2) = Z: J = J + 3 Next I Dim Coords2V As Variant Coords2V = Coords2 Dim EN2 As AcadPolyline Set EN2 = ThisDrawing.ModelSpace.AddPolyline(Coords2V) EN2.Closed = EN.Closed EN2.Color = EN.Color EN2.Linetype = EN.Linetype EN2.Layer = EN.Layer Dim B As Double, W As Double, W2 As Double For I = 0 To UBound(Coords) Step 2 J = I / 2 B = EN.GetBulge(J) EN.GetWidth J, W, W2 EN2.SetBulge J, B EN2.SetWidth J, W, W2 Next I Set lwpoly_2_poly = EN2 End If End Function
|Table 1. Standard Group Code Usage|
Group Code Value 0-9 Strings-5 is handle, 8 is layer name 10 Data Point 38 Elevation 39 Thickness 40-49 Real Numbers 50-59 Angles 60-90 Integers 100 Class Name 210 ECS Vector
Each line in the listing contains a group code followed by the associated data. Note that the listing that appears on your monitor will be different because it will all be run together. The group codes are integer numbers that help our programs to understand what the data that follows will be. These are standard group code assignments in AutoCAD that have been used not only in AutoLISP entity lists, but also within the DXF structure. Table 1 summarizes the generalized group code assignments for quick reference.
Looking at group code assignments in Listing 1, notice that there are some group codes that repeat. In the lightweight polyline object, the group codes 10, 40, 41 and 42 are repeated for each point that is part of the polyline. Group code 10 is the data point itself. Group codes 40 and 41 are the starting and ending widths of the polyline segment. The bulge factor is located with group code 42. If a segment does not have an arc, the bulge factor is zero. When there is an arc in the segment, the value seen with group code 42 is the result of calculating the tangent value of one quarter of the included angle. When the angle is clockwise, the bulge factor is negative; when the angle is counterclockwise, the bulge factor is greater than zero.
In a traditional polyline, the 10, 40, 41 and 42 group codes are seen in individual vertex entity objects-you use (ENTNEXT) to step through the polyline, and each point is a separate entity list. This makes the extraction of segment-level information much easier, and it also provides a way to uniquely identify one segment from another using entity names.
The advantage of having unique group codes in an entity list seems to be lost when working with lightweight polylines. This is because there are multiple occurrences of the 10, 40 and other codes. So, how does one work through such a complicated list structure? (Repeated use of the (ASSOC) subr will only return the same values.)
The answer is to dig into the list and destroy it as you go. Of course, use a temporary copy of the list or save a copy before doing this as you will most likely want to use the list again. Chipping away at the front destroys the list. That is, we are going to remove the list members from the front, so that when we do an (ASSOC) for one of the variables we want, it will be the next vertex and not the one we've already looked at.
Reduce the List
The expression (setq EL (member (assoc 10 EL) EL)) reduces the entity list (EL) that contains the lightweight polyline. The (ASSOC 10 EL) portion returns the first group code 10 sublist. This list is then used with the (MEMBER) subr to locate it in the list. When successful, (MEMBER) returns the located list member along with the remainder of the list following it. This has the effect of moving the group code member to the front of the list. The values of the vertex can then be accessed as normal using the (ASSOC) expression. After the application is done retrieving the vertex data, it must then take out the group code 10 so that the next time around the next point is moved to the front.
Take a look at Listing 1. When the expression just shown is used with the list set to EL, it results in a list that starts at the first group code 10 sublist (just after the group code 39). After using the value of the point, the first member of EL is removed via the (CDR) subr. Thus, the next time the (MEMBER (ASSOC)) expression is employed on EL, the list is reduced further by taking off the 40, 41 and 42 code members up to the next group code 10.
Armed with this expression, we can write a (WHILE) loop that reduces the lightweight polyline to the individual vertex members. The loop iterates as long as there are vertex points remaining. A simple loop that reports each vertex point is shown in Listing 2. The (SETQ) expression is used as the predicate test in the (WHILE) loop. When we have read through all the points in the lightweight polyline, the loop terminates as the (ASSOC) subr returns nil, which causes a nil result from the EL list because nil is found at the end of every normal list. The (WHILE) loop simply prints the value of group code 10, which is the point where the vertex is located. Because the (SETQ) expression sets the value resulting from (MEMBER) back into EL, EL destroys itself while the loop is running. The list is further reduced by using the (CDR) expression, which takes the head off and prepares the list for the next iteration. Since the entity list is a parameter to the function in Listing 2, the calling application still has an intact original of the list to use.
The problems with lightweight polylines are that they don't have unique identities for each of the vertices, and they do not have z values associated with the point lists. The function in Listing 3 can help you solve the first problem. Given an integer that indicates offset position of the vertex desired, this function returns a mini-association list containing the point, bulge and width values for that index value.
Looking inside the function in Listing 3, you'll see a (WHILE) loop. The (WHILE) loop iterates as long as the N value is greater than zero and EL still has a value. Inside the loop, the EL list value is reduced to the member starting with group code 10. Note that inside the (ASSOC) expression, the (CDR) is used to remove the previous group code 10 from the list. In the first iteration, the (CDR) removes the entity name, which is of no consequence to our programming. If N is equal to zero at the end of the iterations, then the desired vertex has been located. If N is zero, a portion of the entity list containing the points, widths and bulge is then returned as a result of the function. If N is not zero, the function returns nil.
There are applications with which the lightweight polyline will not work well. This is especially true with applications being ported from previous versions of AutoCAD or ones that need to work with the vertex-elements as individual selection picks. The answer to this problem is to convert all the lightweight polylines in the drawing into traditional polylines as the application is running. The only drawback to this approach is that the Offset command may not work well with the traditional polylines. It seems that the Offset command prefers lightweight polylines only because it can be assured that they are coplanar.
Converting Lightweight to Traditional
Listing 4 contains a function that converts a lightweight polyline into a traditional polyline. Because there are variable instances where traditional polylines are required, this function works with various parameter options. The function will accept an entity list, an entity name or a selection pick (entity name plus pick point). The result of the function is the entity name of the new traditional polyline-entity object. This utility function can be modified to meet other requirements as needed by your application, so let's look at the routine in more detail.
The parameter EL is first tested to see if it is an entity name. If so, the entity name is converted to an entity list and stored back in EL. If EL is not an entity name but came from an (ENTSEL) type function, then the entity data list is retrieved and given the entity name from the selection pick list. Otherwise, EL remains intact as supplied, and it is tested to see if it is an entity list containing the entity type LWPOLYLINE.
Once the function knows for sure that the entity list is that of a lightweight polyline, it continues by extracting the layer, z data and other header information. The header data is stored in local variables for access later in the routine. Traditional polylines involve multiple entity objects. They are started with the POLYLINE header entity, have a VERTEX entity for each point and are terminated by the SEQEND entity. In order to convert the lightweight polyline into a traditional one, we must build each of the entity components.
The fastest and best-controlled way to create new entities with AutoLISP is via the (ENTMAKE) subr. Once given the header data, the function proceeds to create a polyline header entity list storing it in EN. If the color and linetype information in the lightweight polyline has been set, those values are added to the entity list in EN so that our new object will have these same properties. The (ENTMAKE) subr is then presented with this entity list to create the new polyline.
The next step is to create a prototype VERTEX entity list. Each vertex contains the unique entries for the point list, bulge factor and widths. The prototype entity list has these values initialized to zeros. We will substitute the unique values for each instance.
Next, the function starts into a (WHILE) loop that is very similar to the ones already seen. Each group code 10 in the lightweight polyline is moved to the front of EL and the loop iterates. When we have exhausted the points in EL, the loop finishes.
Inside the (WHILE) loop, the entity list EN is changed. EN has the VERTEX prototype entity list and the values for points, bulge factor and widths are substituted with the values from the EL entity list. The (ENTMAKE) subr is then employed to create the VERTEX entity object. Note that as the entity object sequence for the traditional polyline is defined, it will not appear on the graphics screen. The entity object will not be generated on the screen until it is terminated.
When we no longer have any points left in the EL data list, the entity object sequence definition for the polyline is completed and (ENTMAKE) is used one last time to generate the SEQEND entity object. At this time, the polyline is drawn. The call to (ENTLAST) retrieves the entity name of the last entity created -our new traditional polyline.
Some modifications exist that you may want to make to this lightweight polyline conversion utility. For instance, the original polyline can be placed on a hidden layer or removed altogether. You can also return the same type of information as was provided to the entity. That is, if an entity name was supplied, an entity name is returned. If an entity selection pick was supplied, then one is returned instead of just the entity name (hint: use (NENTSELP) to build it after deleting the original lightweight polyline).
The VBA Alternative
Now, let's look at the same problem, but this time let's make use of the VBA programming solution. VBA works with entity objects in a more object-oriented sense than AutoLISP. In AutoLISP, you use entity lists; in VBA, entity objects. From the VBA programmer's perspective, the lightweight and traditional polyline objects are very much the same. In fact, a quick inspection of the properties and methods associated with the two does not reveal any significant differences (except that the coordinates to the traditional style are three-dimensional while those for lightweight polylines are only two-dimensional).
Listing 5 contains the code for a function in VBA that takes an object presumed to be an AutoCAD polyline entity. An IF test is used to see if indeed the object is a lightweight polyline. If so, it proceeds to get the coordinates. The coordinates of a polyline object are stored in a variant array-you supply a variant-type variable and AutoCAD VBA returns it with data in it. The data is an array of double precision real numbers representing the 2D points. All the points are stored together in the array with the x values being found at positions 0, 2, 4 and so forth; the y values are located at positions 1, 3, 5 and onwards. A FOR loop can be used to step through the array, skipping every other one as we get the x and y values for the points.
To create a polyline object, either a lightweight or a traditional, you must build an array of doubles and then assign them to a variant variable. Variants present an elegant way to pass information between objects in VBA. From the systems programmer's point of view, they are pointers with type definition data attached.
The conversion function then creates a new array of doubles that are large enough to contain the z values as well. To determine how large the new array must be, take the number of coordinate values in the 2D data array and multiply it by one and a half. REDIM is then used to dimension the array to the variable size required.
Next, the function takes the 2D data and puts it into the 3D data array. Each coordinate is stored in succession (the first point is located at positions 0, 1, 2, and the next point can be found at positions 3 through 5 and so forth). The FOR-NEXT loop uses the size of the coordinates variant array obtained from the lightweight polyline and steps through the array two at a time. Another counter (J) is used to keep track of the location in the 3D array we are creating.
After the FOR-NEXT loop, the coordinate array just defined is set to a variant variable. This has the net effect of making the variant point at the array, which is what is needed to create the new polyline in the Model Space of the current drawing. The properties of the lightweight polyline can now be copied to the properties of the traditional polyline. The global properties such as color, linetype and layer are set from EN to EN2-the new polyline.
Another loop is then started that can transfer the properties for each vertex. The bulge factor and width properties are moved from EN to EN2 by using the GETWIDTH and GETBULGE methods. An index is supplied to the functions to obtain the value at a specific vertex. The complementary functions are SETWIDTH and SET BULGE, and they set the appropriate values at each vertex.
When this function finishes, the new entity will be generated. Note that there is no closing or sequence-end definition that causes the graphic generation of the object. If you need to force a regeneration of the object itself in your code, then the UPDATE method is employed.
Obtaining the specific values of a coordinate in VBA is much easier than in AutoLISP. The COORDINATE property is used with an index of the exact coordinate desired. Coordinates are always returned and supplied to the COORDINATE method as an array of three double-precision real numbers. In the case of the lightweight polyline, the z value is the same as the elevation or the UCS or it will be zero. For the traditional polyline type, the z value is the same as the vertex. Note that when assigning the COORDINATE property of an entity object, you must supply a variant that is assigned to an array of three double-precision real numbers. And, when COORDINATE is used to obtain a value, the result is a variant data type that points at a three-element array of doubles.
The manipulation of the entity data in VBA is substantially easier than in AutoLISP (as seen in this simple example). Although the notion of variants can be somewhat confusing at first, they are really nothing more than pointers to variable memory locations as used by the VBA objects in AutoCAD. Their usage greatly reduces the overhead needed to implement the types of interfaces desired by programmers of VBA in AutoCAD.
So, what's your choice? In AutoCAD 2000, the VBA system is greatly improved. There is more power and a better set of tools for interfacing with the AutoCAD 2000 environment. But at the same time, Visual LISP is now part of the standard AutoCAD package, and it provides a greatly enhanced version of AutoLISP that rivals VBA in almost all regards. So, the decision is not easy as they are both great choices. No matter what direction you go, remember to keep on programmin'.