• No results found

Putting It All Together

In document AutoCAD 2000 Visual Lisp Tutorial (Page 99-136)

Planning the Overall Reactor Process

You need to define several new functions in this lesson. Rather than present you with details on all aspects of the new code, this lesson presents an over-view and points out the concepts behind the code. At the end of the lesson, you will have all the source code necessary to create a garden path applica-tion identical to the sample program you ran in Lesson 1.

NOTE When you are in the midst of developing and debugging reactor-based applications, there is always the potential of leaving AutoCAD in an unstable state. This can be caused by several situations, such as failing to remove a reactor from deleted entities. For this reason, it is recommended that before beginning Lesson 7, you should close VLISP, save any open files as you do so, exit AutoCAD, then restart both applications.

Begin by loading the project as it existed at the end of Lesson 6.

Two obvious pieces of work remain to be done in the garden path application:

Writing the object reactor callbacks.

Writing the editor reactor callbacks.

You also need to consider how to handle the global variables in your pro-gram. Often, it is desirable to have globals retain a value throughout an AutoCAD drawing session. In the case of reactors, however, this is not the case. To illustrate this, imagine a user of your garden path application has drawn several garden paths in a single drawing. After doing this, the user erases them, first one at a time, then two at a time, and so on, until all but one path is erased.

Lesson 5 introduced a global variable *reactorsToRemove*, responsible for storing pointers to the reactors for the polylines about to be erased. When

*reactorsToRemove* is declared in gp:outline-erased, the event lets you know the polyline is about to be erased. The polyline is not actually removed until the gp:command-ended event fires.

The first time the user deletes a polyline, things work just as you would expect. In gp:outline-erased, you store a pointer to the reactor. When gp:command-ended fires, you remove the tiles associated with the polyline to which the reactor is attached, and all is well. Then, the user decides to erase two paths. As a result, your application will get two calls to

gp:outline-erased, one for each polyline about to be erased. There are two potential problems you must anticipate:

Planning the Overall Reactor Process

|

97

When you setq the *reactorsToRemove* variable, you must add a pointer to a reactor to the global, making sure not to overwrite any values already stored there. This means *reactorsToRemove* must be a list structure, so you can append reactor pointers to it. You can then accumulate several reactor pointers corresponding to the number of paths the user is erasing within a single erase command.

Every time gp:command-will-start fires, indicating a new command sequence is beginning, you should reinitialize the *reactorsToRemove*

variable to nil. This is necessary so that the global is not storing reactor pointers from the previous erase command.

If you do not reinitialize the global variable or use the correct data struc-ture (in this case, a list), you will get unexpected behavior. In the case of reactors, unexpected behavior can be fatal to your AutoCAD session.

Here is the chain of events that needs to occur for users to erase two garden paths with a single erase command. Note how global variables are handled:

Initiate the erase command. This triggers the gp:command-will-start function. Set *reactorsToRemove* to nil.

Select two polylines; your application is not yet notified.

Press ENTER to erase the two selected polylines.

Your application gets a callback to gp:outline-erased for one of the polylines. Add its reactor pointer to the null global, *reactorsToRemove*. Your application gets a callback to gp:outline-erased for the second of the polylines. Append its reactor pointer to the *reactorsToRemove* glo-bal that already contains the first reactor pointer.

AutoCAD deletes the polylines.

Your callback function gp:command-ended fires. Eliminate any tiles associ-ated with the reactor pointers stored in *reactorsToRemove*.

In addition to the *reactorsToRemove* global, your application also includes a *polyToChange* global, which stores a pointer to any polyline that will be modified. Two additional globals for the application will be introduced later in this lesson.

Reacting to More User-Invoked Commands

When writing a reactor-based application, you need to handle any command that affects your objects in a significant way. One of your program design activities should be to review all possible AutoCAD editing commands and determine how your application should respond to each one. The format of the reactor-trace sheet shown near the end of Lesson 6 is very good for this purpose. Invoke the commands you expect your user to use, and write down

98

|

Lesson 7 Putting It All Together

the kind of behavior with which your application should respond. Other actions to plan for include

Determine what to do when users issue UNDO and REDO commands.

Determine what to do when users issue the OOPS command after erasing entities linked with reactors.

To prevent a very complex subject from becoming very, very complex, the tutorial does not try to cover all the possibilities that should be covered, and the functionality within this lesson is kept to an absolute minimum.

Even though you won’t be building in the complete functionality for these extra commands, examine what a few additional editing functions would require you to do:

If users stretch a polyline boundary (using the STRETCH command) sev-eral things should happen. It could be stretched in any direction, not just on the major or minor axis, so the boundary may end up in a very odd shape. In addition, you need to take into consideration how many vertices have been stretched. A situation where only one vertex is stretched will result in a polyline quite different from one in which two vertices are moved. In any case, the tiles must be erased and new positions recalcu-lated once you determine the adjustments needed to the boundary.

If users move a polyline boundary, all the tiles should be erased, then redrawn in the new location. This is a fairly simple operation, because the polyline boundary did not change its size or shape.

If users scale a polyline boundary, you need to make a decision. Should the tiles be scaled up as well, so that the path contains the same number of tiles? Or, should the tile size remain the same and the application add or remove tiles, depending on whether the polyline was scaled up or down?

If users rotate a polyline boundary, all the tiles should be erased, then redrawn in the new orientation.

To begin, though, just plan for the following:

Warn the user upon command-start that the selected edit command (such as stretch, move, or rotate) will have detrimental effects on a garden path.

If the user proceeds, erase the tiles and do not redraw them.

Remove the reactors from the path outline.

Planning the Overall Reactor Process

|

99 NOTE In addition to user-invoked AutoCAD commands, entities may also be modified or deleted through AutoLISP or ObjectARX applications. The example provided in the Garden Path tutorial does not cover programmatic manipulation of the garden path polyline boundary, such as through (entdel <polyline entity>). In this case, the editor reactor events :vlr-commandWillStart and :vlr-commandEnded will not be triggered.

Storing Information within the Reactor Objects

One other important aspect of the application you need to think about is what kind of information to attach to the object reactor that is created for each polyline entity. In Lesson 6, you added code that attached the contents of gp_PathData (the association list) to the reactor. You expanded the data carried within gp_PathData by adding a new keyed field (100) to the associ-ation list. This new sublist is a list of pointers to all the circle entities assigned to a polyline boundary.

Because of the work that needs to be done to recalculate the polyline bound-ary, four additional key values should be added to gp_pathData:

;;; StartingPoint ;

;;; (12 . BottomStartingPoint) 15---14 ;

;;; (15 . TopStartingPoint) | | ;

;;; EndingPoint 10 ----pathAngle---> 11 ;

;;; (13 . BottomEndingPoint) | | ;

;;; (14 . TopEndingPoint) 12---13 ;

;;; ; These ordered points are necessary to recalculate the polyline boundary whenever the user drags a corner grip to a new location. This information already exists within the gp:drawOutline function in gpdraw.lsp. But look at the return value of the function. Currently, only the pointer to the polyline object is returned. So you need to do three things:

Assemble the perimeter points in the format required.

Modify the function so that it returns the perimeter point lists and the pointer to the polyline.

Modify the C:GPath function so that it correctly deals with the new format of the values returned from gp:drawOutline.

100

|

Lesson 7 Putting It All Together

Assembling the perimeter point lists is simple. Look at the code in gp:drawOutline. The local variable p1 corresponds to the key value 12, p2to 13, p3 to 14, and p4 to 15. You can add the following function call to assemble this information:

(setq polyPoints(list (cons 12 p1)

(cons 13 p2) (cons 14 p3) (cons 15 p4) ))

Modifying the function so that it returns the polyline perimeter points and the polyline pointer is also easy. As the last expression within

gp:drawOutline, assemble a list of the two items of information you want to return.

(list pline polyPoints)

To add program logic to save the polyline perimeter points

1 Modify gp:drawOutline by making the changes shown in boldface in the fol-lowing code (don’t overlook the addition of the polyPoints local variable to the defun statement):

(defun gp:drawOutline (BoundaryData / PathAngle Width HalfWidth StartPt PathLength angm90 angp90 p1 p2

p3 p4 poly2Dpoints poly3Dpoints plineStyle pline polyPoints

)

;; extract the values from the list BoundaryData.

(setq PathAngle (cdr (assoc 50 BoundaryData)) Width (cdr (assoc 40 BoundaryData)) HalfWidth (/ Width 2.00)

StartPt (cdr (assoc 10 BoundaryData)) PathLength (cdr (assoc 41 BoundaryData)) angp90 (+ PathAngle (Degrees->Radians 90)) angm90 (- PathAngle (Degrees->Radians 90)) p1 (polar StartPt angm90 HalfWidth) p2 (polar p1 PathAngle PathLength) p3 (polar p2 angp90 Width)

p4 (polar p3 (+ PathAngle

(Degrees->Radians 180)) PathLength) poly2Dpoints (apply ’append

(mapcar ’3dPoint->2dPoint (list p1 p2 p3 p4)) )

poly3Dpoints (mapcar ’float (append p1 p2 p3 p4)) ;; get the polyline style.

Planning the Overall Reactor Process

|

101

plineStyle (strcase (cdr (assoc 4 BoundaryData)))

;; Add polyline to the model space using ActiveX automation.

pline (if (= plineStyle "LIGHT") ;; create a lightweight polyline.

(vla-addLightweightPolyline

*ModelSpace* ; Global Definition for Model Space (gp:list->variantArray poly2Dpoints)

;data conversion

) ;_ end of vla-addLightweightPolyline ;; or create a regular polyline.

(vla-addPolyline *ModelSpace*

(gp:list->variantArray poly3Dpoints) ;data conversion

) ;_ end of vla-addPolyline ) ;_ end of if

polyPoints (list (cons 12 p1) (cons 13 p2) (cons 14 p3) (cons 15 p4) )

) ;_ end of setq (vla-put-closed pline T) (list pline polyPoints) ) ;_ end of defun

2 Modify the C:GPath function (in gpmain.lsp). Look for the line of code that currently looks like this:

(setq PolylineName (gp:drawOutline gp_PathData)) Change it so it appears as follows:

(setq PolylineList (gp:drawOutline gp_PathData) PolylineName (car PolylineList)

gp_pathData (append gp_pathData (cadr PolylineList)) ) ;_ end of setq

The gp_PathData variable now carries all the information required by the reactor function.

3 Add PolylineList to the local variables section of the C:GPath function definition.

102

|

Lesson 7 Putting It All Together

Adding the New Reactor Functionality

In Lesson 6, you hooked up callback function gp:command-will-start to the reactor event :vlr-commandWillStart. As it currently exists, the function dis-plays some messages and initializes two global variables, *polyToChange*

and *reactorsToRemove*, to nil.

To add functionality to the gp:command-will-start callback function 1 Open your gpreact.lsp file.

2 In the gp:command-will-start function, add two variables to the setq func-tion call by modifying it as follows:

;; Reset all four reactor globals to nil.

(setq *lostAssociativity* nil *polyToChange* nil *reactorsToChange* nil *reactorsToRemove* nil)

3 Replace the remaining code in gp:command-will-start, up to the last princ function call, with the following code:

(if (member (setq currentCommandName (car command-list)) ’( "U" "UNDO" "STRETCH" "MOVE"

"ROTATE" "SCALE" "BREAK" "GRIP_MOVE"

"GRIP_ROTATE" "GRIP_SCALE" "GRIP_MIRROR") ) ;_ end of member

(progn

(setq *lostAssociativity* T) (princ "\nNOTE: The ") (princ currentCommandName)

(princ " command will break a path’s associativity .") ) ;_ end of progn

) ;_ end of if

This code checks to see if the user issued a command that breaks the associa-tivity between the tiles and the path. If the user issued such a command, the program sets the *lostAssociativity* global variable and warns the user.

As you experiment with the garden path application, you may discover addi-tional editing commands that can modify the garden path and cause the loss of associativity. Add these commands to the quoted list so that the user is aware of what will happen. When this function fires, the user has started a command but has not selected any entities to modify. The user could still cancel the command, leaving things unchanged.

Adding the New Reactor Functionality

|

103

Adding Activity to the Object Reactor Callback Functions

In Lesson 6, you registered two callback functions with object reactor events.

The gp:outline-erased function was associated with the :vlr-erased reac-tor event, and gp:outline-changed was associated with the :vlr-modified event. You need to make these functions do what they are intended to do.

To make the object reactor callback functions do what they are intended to do 1 In gpreact.lsp, change gp:outline-erased so it appears as follows:

(defun gp:outline-erased (outlinePoly reactor parameterList) (setq *reactorsToRemove*

(cons reactor *reactorsToRemove*)) (princ)

) ;_ end of defun

There is just one operation performed here. The reactor attached to the polyline is saved to a list of all reactors that need to be removed. (Remember:

though reactors are attached to entities, they are separate objects entirely, and their relationships to entities need to be managed just as carefully as reg-ular AutoCAD entities.)

2 Change gp:outline-changed to reflect the following code:

(defun gp:outline-changed (outlinePoly reactor parameterList) (if *lostAssociativity*

(setq *reactorsToRemove*

(cons reactor *reactorsToRemove*)) (setq *polytochange* outlinePoly

*reactorsToChange* (cons reactor *reactorsToChange*)) )

(princ) )

There are two categories of functions that can modify the polyline outline.

The first category contains those commands that will break the path’s associativity with its tiles. You checked for this condition in

gp:command-will-start and set the *lostAssociativity* global variable accordingly. In this case, the tiles need to be erased, and the path is then in the user’s hands. The other category is the grip mode of the STRETCH command, where associativity is retained and you need to straighten out the outline after the user has finished dragging a vertex to a new location.

The *polyToChange* variable stores a VLA-Object pointer to the polyline itself. This will be used in the gp:command-ended function when it comes time to recalculate the polyline border.

104

|

Lesson 7 Putting It All Together

Designing the gp:command-ended Callback Function

The gp:command-ended editor reactor callback function is where most action takes place. Until this function is called, the garden path border polylines are

“open for modify;” that is, users may still be manipulating the borders in AutoCAD. Within the reactor sequence, you have to wait until AutoCAD has done its part of the work before you are free to do what you want to do.

The following pseudo-code illustrates the logic of the gp:command-ended function:

Determine the condition of the polyline.

CONDITION 1 - POLYLINE ERASED (Erase command) Erase the tiles.

CONDITION 2 - LOST ASSOCIATIVITY (Move, Rotate, etc.) Erase the tiles.

CONDITION 3 - GRIP_STRETCH - REDRAW AND RE-TILE Erase the tiles.

Get the current boundary data from the polyline.

If it is a lightweight polyline, Process boundary data as 2D Else

Process boundary data as 3D End if

Redefine the polyline border (pass in parameters of the current boundary configuration, as well as the old).

Get the new boundary information and put it into the format required for setting back into the polyline entity.

Regenerate the polyline.

Redraw the tiles (force ActiveX drawing).

Put the revised boundary information back into the reactor named in *reactorsToChange*.

End function

The pseudo-code is relatively straightforward, but there are several important details buried in the pseudo-code, and they are things you would not be expected to know at this point.

Handling Multiple Entity Types

The first detail is that your application may draw two kinds of polylines: old-style and lightweight. These different polyline types return their entity data in different formats. The old-style polyline returns a list of twelve reals: four sets of X, Y, and Z points. The lightweight polyline, though, returns a list eight reals: four sets of X and Y points.

Adding the New Reactor Functionality

|

105 You need to do some calculations to determine the revised polyline boundary after a user moves one of the vertices. It will be a lot easier to do the calcula-tions if the polyline data has a consistent format.

The Lesson 7 version of the utils.lsp file contains functions to perform the necessary format conversions: xyzList->ListOfPoints extracts and formats 3D point lists into a list of lists, and xyList->ListOfPoints extracts and for-mats 2D point lists into a list of lists.

To add the code for converting polyline data into a consistent format 1 If you have a copy of utils.lsp open in a VLISP text editor window, close it.

2 Copy the version of utils.lsp from the Tutorial\VisualLISP\Lesson7 directory into your working directory.

In addition to the two functions that reformat polyline data, utils.lsp con-tains additional utility functions needed in handling user alterations to the garden path.

3 Open utils.lsp in a VLISP text editor window and review the new code.

Using ActiveX Methods in Reactor Callback Functions

The second detail appearing in the pseudo-code shows up near the end, at the step for redrawing the tiles. Here is the pseudo-code statement:

Redraw the tiles (force ActiveX drawing)

The parenthetical phrase says it all: force ActiveX drawing. Why is this required? Why can’t the application use the object creation preference stored in the association sublist?

The answer is you cannot use the command function for entity creation within a reactor callback function. This has to do with some internal workings of AutoCAD. You need to force the tile drawing routine to use ActiveX. You will hear more about this issue later in this lesson.

Handling Nonlinear Reactor Sequences

The final important detail deals with a quirk in the command/reactor sequence in AutoCAD when users modify a polyline by using the specialized GRIP commands. These commands, such as GRIP_MOVE and GRIP_ROTATE, are available from a shortcut menu after you select the grip of an object and right-click. The reactor sequence is not as linear as a simple MOVE or ERASE command. In effect, the user is changing to a different command while in the midst of another. To demonstrate this situation, you can load the code

106

|

Lesson 7 Putting It All Together

from Lesson 6 that traces the sequence of reactor events. Or simply review the following annotated VLISP Console window output to see what happens:

;; To start, select the polyline and some of the circles by using a

;; crossing selection box. The items in the selection set--

;; the chosen circles and the polyline--are now shown with grips on.

;; To initiate the sequence, click on one of the polyline grips:

(GP:COMMAND-WILL-START #<VLR-Command-reactor> (GRIP_STRETCH))

;; Now change the command to a move by right-clicking and choosing

;; MOVE from the pop-up menu. Notice that the command-ended

;; reactor fires in order to close out the GRIP_STRETCH command

;; without having fired an object reactor event:

;; without having fired an object reactor event:

In document AutoCAD 2000 Visual Lisp Tutorial (Page 99-136)

Related documents