As mentioned earlier, a Rail should appear as a straight line between the two Stations it connects. Thus, in subway5.bet, the pattern Rail is as follows
Rail: Line (# init:: (# do color[] -> setpaint; 2 -> width; #) #);
It is simply a Line using the same Paint as is used for the outline of Stations, and which has a width of 2 pixels.
Then comes the question of creating the rails. Again onMouseDown has to be changed.
onMouseDown:: (# do mousepos->devicetocanvas->mousepos; scan: (# (* Find out what was hit - if any *) do thePicture.ScanGOsReverse (# do (if (myCanvas[], mousepos)->go.containspoint then (if go = map[] then (* ignore *) else (if go##=Station## then (* We hit a station *) (if shiftmodified then go[]->interactiveCreateRail; else (go[],mousepos,NoModifier) -> interactivemove; if); leave scan if); if); if); #); (* No Station was hit *) mousepos->makeStation; #); #);
Firstly, in the scanning of graphical objects, now three
possibilities exist for what can be hit: Either the map, a Station, or
a Rail. Once again hitting the map is taken as if nothing was hit, but
if something else was hit, it must be determined what kind of
graphical object it was. This is done by comparing the
structure reference of go
with the
pattern reference Station##
.
If a Station has been hit, it must be determined whether it
should be moved as before, or interaction for connecting it with
another Station should be started. Here it is chosen to start
connecting it with another Station, if the mouse button press was
modified by holding down the SHIFT key, or otherwise to
move the Station as before.
Exercise: The structure comparison can be a fairly
expensive operation in BETA, and some would say, that using such tests
for types, is not a truly object-oriented programming style. Write a
variant of onMouseDown, that does not use structure comparisons.
Hint: Declare a pattern HitPicture with a virtual onHit, and let both
Station and Rail be specializations of HitPicture.
The creation of a Rail, connecting the Station with another one, is accomplished using an item called interactiveCreateRail:
interactiveCreateRail: @ (# r: ^rail; hitstation, otherstation: ^Station; ...... enter hitstation[] do &rail[] -> r[]; r.init; (r[], hitstation.position, NoModifier) -> interactiveCreateShape; (* Check if r ends in another station *) scan: thePicture.ScanGOsReverse (# do (if (myCanvas[], r.end) -> go.containspoint then (if go[] // map[] // hitstation[] then (* ignore *) else (if go##=Station## then (* r ends in another station; * connect with hitstation *) go[] -> otherStation[]; ...... r -> draw; (* It looks better if the stations * cover the ends of the rail. * Instead of lowering the rail in the * BifrostCanvas (which would put the rail * behind the map) we raise the two * stations *) hitstation[] -> bringForward; otherstation[] -> bringforward; leave scan if); if); if); #); #);
The first thing to do is to instantiate and initialize a Rail. Then a reference to this Rail is passed to interactiveCreateShape, with the position of hitStation as starting point, i.e. the feedback, which in this case is a 'rubber line', will begin in hitStation.position. The feedback is ended when the user releases the mouse button.
Then it must be determined if the mouse was released on top of another Station. This is done in the same fashion as the way it was determined if a Station was hit in onMouseDown. If the mouse was released on top of another Station, a reference to this Station is kept in otherStation. In this case, first the two Stations must be connected; this issue is considered below. Then the Rail must be drawn, and finally a few rearrangements are done to improve the appearance of the connection.
Instead of leaving the Rail on top of the two Stations after it is drawn, it looks nicer if the Rail is moved behind the Stations, see the illustration to the right. One way of doing this is to use SendBehind for the Rail. However, this will put the Rail to the very back of the Picture of myCanvas, and would thus put the Rail behind the map too. Instead the two Stations can be brought to the top of the Picture of myCanvas using BringForward
What is left is the question of how to make the two Stations know that they are connected with a Rail. At least they need to know this in order to change the Rails when the Stations are moved; this issue is considered in the next section. Since the rails are to be considered bi-directional, both Stations connected by a Rail have to know about the Rail, and where the other Station is. To accomplish this, each Station will have a list of references to Rails that connects it with other Stations. Furthermore references to the two end points of each Rail is kept in the list. These references are qualified with PredefinedShape.invalidatePoint, which is an ordinary Point, with the exception that changing it will cause some recalculations of the Shape of the Rail (the Shape is 'invalidated'). This is used internally by Bifrost, e.g. to control how often the bounding box needs to be recalculated. Using the standard List from the Container libraries (automatically INCLUDEd by Bifrost), this can be expressed as follows:
Station: Picture (# ...... rails: @list (# element:: (# r: ^rail; mypoint, otherpoint: ^PredefinedShape.invalidatepoint; #); #); ...... #);
Using this data structure, the issue of connecting the two Stations in interactiveCreateRail can be solved as follows:
interactiveCreateRail: @ (# r: ^rail; hitstation, otherstation: ^Station; e: ^Station.rails.element; enter hitstation[] do ... instantiate, init and interactively create r, as above ... (* Check if r ends in another station *) scan: thePicture.ScanGOsReverse (# do (if (myCanvas[], r.end) -> go.containspoint then (if go[] // map[] // hitstation[] then (* ignore *) else (if go##=Station## then (* r ends in another station; * connect with hitstation *) go[] -> otherstation[]; otherstation.position -> r.end; (* Small adjustment *) (* Add r to hitstation and * otherstation. *) &hitstation.rails.element[] -> e[]; r[] -> e.r[]; r.theshape.begin[] -> e.mypoint[]; r.theshape.end[] -> e.otherpoint[]; e[] -> hitstation.rails.append; &otherstation.rails.element[] -> e[]; r[] -> e.r[]; r.theshape.end[] -> e.mypoint[]; r.theshape.begin[] -> e.otherpoint[]; e[] -> otherstation.rails.append; ... draw r and rearrange as above ... leave scan if); if); if); #); #);
First a small adjustment is done: The mouse may have been released anywhere within otherStation. It looks better if the Rail goes through the center of otherStation, so the end point of r is changed to otherStation.position, which was set to the center of otherStation when otherStation was initialized. However, both hitStation and otherStation may have been moved since they were initialized, so the use of hitStation.position and otherStation.position as above is not correct if something else isn't done. What needs to be done is to update the position attribute each time a Station is moved. This can be done in the move virtual, which is called by interactiveMove. The easiest way to update position is to use the center of one of the two circles constituting the Station and applying the transformation matrix TM [2] of the Station. Notice that all moving, scaling and rotating of a graphical object is remembered by changing TM, not by changing the coordinates of the defining points of the shape of the graphical object.
Station: Picture (# ...... move:: (# do (* Move is called by interactiveMove *) (* TM describes the current transformation. * Make position be the *transformed* position *) circleoutline.center -> TM.transformpoint -> position; #); ..... #);
Now the Stations can be connected with Rails, but one problem remains: If a Station is moved after some Rails have been added to it, the Rails will not move with the Station. In the next section this problem will be addressed.
The Bifrost Graphics System - Tutorial | © 1991-2004 Mjølner Informatics |
[Modified: Tuesday October 24th 2000 at 15:07]
|